diff --git a/.eslintrc.js b/.eslintrc.js index b303a9fefb691..75580f115f48a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -850,6 +850,10 @@ module.exports = { name: 'semver', message: 'Please use "semver/*/{function}" instead', }, + { + name: '@kbn/rule-data-utils', + message: `Import directly from @kbn/rule-data-utils/* submodules in public/common code`, + }, ], }, ], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d5c569ac9d552..bd5aa47c79034 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -397,7 +397,7 @@ # Security Solution sub teams /x-pack/plugins/cases @elastic/security-threat-hunting /x-pack/plugins/timelines @elastic/security-threat-hunting -/x-pack/test/case_api_integration @elastic/security-threat-hunting +/x-pack/test/cases_api_integration @elastic/security-threat-hunting /x-pack/plugins/lists @elastic/security-detections-response ## Security Solution sub teams - security-onboarding-and-lifecycle-mgt @@ -415,11 +415,15 @@ /x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/ @elastic/security-onboarding-and-lifecycle-mgt /x-pack/test/security_solution_endpoint/apps/endpoint/ @elastic/security-onboarding-and-lifecycle-mgt +## Security Solution sub teams - security-telemetry (Data Engineering) +x-pack/plugins/security_solution/server/usage/ @elastic/security-telemetry +x-pack/plugins/security_solution/server/lib/telemetry/ @elastic/security-telemetry + ## Security Solution sub teams - security-engineering-productivity -x-pack/plugins/security_solution/cypress/ccs_integration -x-pack/plugins/security_solution/cypress/upgrade_integration -x-pack/plugins/security_solution/cypress/README.md -x-pack/test/security_solution_cypress +x-pack/plugins/security_solution/cypress/ccs_integration @elastic/security-engineering-productivity +x-pack/plugins/security_solution/cypress/upgrade_integration @elastic/security-engineering-productivity +x-pack/plugins/security_solution/cypress/README.md @elastic/security-engineering-productivity +x-pack/test/security_solution_cypress @elastic/security-engineering-productivity # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics diff --git a/dev_docs/contributing/how_we_use_github.mdx b/dev_docs/contributing/how_we_use_github.mdx index 3b455b7682c33..38391874b87bf 100644 --- a/dev_docs/contributing/how_we_use_github.mdx +++ b/dev_docs/contributing/how_we_use_github.mdx @@ -46,7 +46,7 @@ In order to assist with developer tooling we ask that all Elastic engineers use 1. Update the git config for your current repository to commit with your `@elastic.co` email: ```bash - git config --local user.email YOUR_ELASTIC_EMAIL@elastic.co + git config user.email YOUR_ELASTIC_EMAIL@elastic.co ``` 1. Create a commit using the new email address diff --git a/docs/apm/correlations.asciidoc b/docs/apm/correlations.asciidoc index 2165994b8372f..ea62f034fb89b 100644 --- a/docs/apm/correlations.asciidoc +++ b/docs/apm/correlations.asciidoc @@ -38,7 +38,7 @@ in a table below the chart. The table is sorted by correlation coefficients that range from 0 to 1. Attributes with higher correlation values are more likely to contribute to high latency transactions. By default, the attribute with the highest correlation value is added to the chart. To see the latency distribution -for other attributes, hover over their row in the table. +for other attributes, select their row in the table. If a correlated attribute seems noteworthy, use the **Filter** quick links: @@ -47,12 +47,14 @@ the selected value. * `-` creates a new query in the {apm-app} to filter out transactions containing the selected value. -In this example screenshot, transactions with the field -`labels.orderPriceRange` and value `large` are skewed to the right with slower -response times than the overall latency distribution. If you select the `+` -filter in the appropriate row of the table, it creates a new query in the -{apm-app} for transactions with this attribute. With the "noise" now filtered -out, you can begin viewing sample traces to continue your investigation. +You can also click the icon beside the field name to view and filter its most +popular values. + +In this example screenshot, there are transactions that are skewed to the right +with slower response times than the overall latency distribution. If you select +the `+` filter in the appropriate row of the table, it creates a new query in +the {apm-app} for transactions with this attribute. With the "noise" now +filtered out, you can begin viewing sample traces to continue your investigation. [discrete] [[correlations-error-rate]] @@ -67,25 +69,23 @@ is determined by its {ecs-ref}/ecs-event.html#field-event-outcome[event.outcome] value. For example, APM agents set the `event.outcome` to `failure` when an HTTP transaction returns a `5xx` status code. -// The chart highlights the failed transactions in the overall latency distribution for the transaction group. -If there are attributes that have a statistically significant correlation with -failed transactions, they are listed in a table. The table is sorted by scores, -which are mapped to high, medium, or low impact levels. Attributes with high -impact levels are more likely to contribute to failed transactions. -// By default, the attribute with the highest score is added to the chart. To see a different attribute in the chart, hover over its row in the table. +The chart highlights the failed transactions in the overall latency distribution +for the transaction group. If there are attributes that have a statistically +significant correlation with failed transactions, they are listed in a table. +The table is sorted by scores, which are mapped to high, medium, or low impact +levels. Attributes with high impact levels are more likely to contribute to +failed transactions. By default, the attribute with the highest score is added +to the chart. To see a different attribute in the chart, select its row in the +table. -For example, in the screenshot below, the field -`kubernetes.pod.name` and value `frontend-node-59dff47885-fl5lb` has a medium -impact level and existed in 19% of the failed transactions. +For example, in the screenshot below, there are attributes such as a specific +node and pod name that have medium impact on the failed transactions. [role="screenshot"] image::apm/images/correlations-failed-transactions.png[Failed transaction correlations] -TIP: Some details, such as the failure and success percentages, are available -only when the -<> -advanced setting is enabled. - Select the `+` filter to create a new query in the {apm-app} for transactions -with this attribute. You might do his for multiple attributes--each time -filtering out more and more noise and bringing you closer to a diagnosis. \ No newline at end of file +with one or more of these attributes. If you are unfamiliar with a field, click +the icon beside its name to view its most popular values and optionally filter +on those values too. Each time that you add another attribute, it is filtering +out more and more noise and bringing you closer to a diagnosis. \ No newline at end of file diff --git a/docs/apm/images/correlations-failed-transactions.png b/docs/apm/images/correlations-failed-transactions.png index 3258b44f7097b..18fd3ff728544 100644 Binary files a/docs/apm/images/correlations-failed-transactions.png and b/docs/apm/images/correlations-failed-transactions.png differ diff --git a/docs/apm/images/correlations-hover.png b/docs/apm/images/correlations-hover.png index 80c1fa41adbdf..9731517b32c43 100644 Binary files a/docs/apm/images/correlations-hover.png and b/docs/apm/images/correlations-hover.png differ diff --git a/docs/developer/contributing/development-github.asciidoc b/docs/developer/contributing/development-github.asciidoc index 44736bf4027c1..48b1abf6c712c 100644 --- a/docs/developer/contributing/development-github.asciidoc +++ b/docs/developer/contributing/development-github.asciidoc @@ -53,7 +53,7 @@ In order to assist with developer tooling we ask that all Elastic engineers use + ["source","shell"] ----------- -git config --local user.email YOUR_ELASTIC_EMAIL@elastic.co +git config user.email YOUR_ELASTIC_EMAIL@elastic.co ----------- 4. Create a commit using the new email address + diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 599e8c54643ce..38a73ec92313f 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -191,3 +191,6 @@ Specifies the default timeout for the all rule types tasks. The time is formatte `[ms,s,m,h,d,w,M,Y]` + For example, `20m`, `24h`, `7d`, `1w`. Default: `60s`. + +`xpack.alerting.cancelAlertsOnRuleTimeout`:: +Specifies whether to skip writing alerts and scheduling actions if rule execution is cancelled due to timeout. Default: `true`. This setting can be overridden by individual rule types. \ No newline at end of file diff --git a/docs/user/production-considerations/security-production-considerations.asciidoc b/docs/user/production-considerations/security-production-considerations.asciidoc index a23911375639f..0a7a3988f1c7b 100644 --- a/docs/user/production-considerations/security-production-considerations.asciidoc +++ b/docs/user/production-considerations/security-production-considerations.asciidoc @@ -17,7 +17,10 @@ For more information on {kib}'s security controls, see <>. +parties. See +{ref}/security-basic-setup-https.html#encrypt-kibana-http[encrypt HTTP client communications for {kib}]. + +encrypt-kibana-http [float] [[configuring-kibana-shield]] diff --git a/docs/user/security/securing-communications/elasticsearch-mutual-tls.asciidoc b/docs/user/security/securing-communications/elasticsearch-mutual-tls.asciidoc index 57ecb1705e007..1bc1f579ecbc7 100644 --- a/docs/user/security/securing-communications/elasticsearch-mutual-tls.asciidoc +++ b/docs/user/security/securing-communications/elasticsearch-mutual-tls.asciidoc @@ -5,36 +5,42 @@ Mutual TLS with {es} ++++ -Secure Sockets Layer (SSL) and Transport Layer Security (TLS) provide encryption for data-in-transit. While these terms are often used -interchangeably, {kib} supports only TLS, which supersedes the old SSL protocols. - -TLS requires X.509 certificates to authenticate the communicating parties and perform encryption of data-in-transit. Each certificate -contains a public key and has and an associated -- but separate -- private key; these keys are used for cryptographic operations. {kib} -supports certificates and private keys in PEM or PKCS#12 format. - -In a standard TLS configuration, the server presents a signed certificate to authenticate itself to the client. In a mutual TLS -configuration, the client also presents a signed certificate to authenticate itself to the server. - -When {es} {security-features} is enabled on your cluster, each request that {kib} (the client) makes to {es} (the server) must be -authenticated. Most requests made by end users through {kib} to {es} are authenticated by using the credentials of the logged-in user. There -are, however, a few internal requests that {kib} needs to make to {es}. For this reason, you must configure credentials for {kib} to use for -those requests. - -If {kib} has `elasticsearch.username` and `elasticsearch.password` configured, it will attempt to use these to authenticate to {es} via the -{ref}/native-realm.html[native realm]. However, {kib} also supports mutual TLS authentication with {es} via a {ref}/pki-realm.html[Public -Key Infrastructure (PKI) realm]. To do so, {es} needs to verify the signature on the {kib} client certificate, and it also needs to map the -client certificate's distinguished name (DN) to the appropriate `kibana_system` role. - -NOTE: Using a PKI realm is a gold feature. For a comparison of the Elastic license levels, see https://www.elastic.co/subscriptions[the -subscription page]. - -To configure {kib} and {es} to use mutual TLS authentication: - -. <>. - -. <>. -+ -This entails generating a "server certificate" for {es} to use on the HTTP layer. +Secure Sockets Layer (SSL) and Transport Layer Security (TLS) provide encryption +for data-in-transit. While these terms are often used interchangeably, {kib} +supports only TLS, which supersedes the old SSL protocols. + +TLS requires X.509 certificates to authenticate the communicating parties and +perform encryption of data-in-transit. Each certificate contains a public key +and has and an associated -- but separate -- private key; these keys are used +for cryptographic operations. {kib} supports certificates and private keys in +PEM or PKCS#12 format. + +In a standard TLS configuration, the server presents a signed certificate to +authenticate itself to the client. In a mutual TLS configuration, the client +also presents a signed certificate to authenticate itself to the server. + +{es} {security-features} are enabled on your cluster by default, so each request +that {kib} (the client) makes to {es} (the server) is authenticated. Most +requests made by end users through {kib} to {es} are authenticated by using the +credentials of the logged-in user. + +To {ref}/configuring-stack-security.html#stack-start-with-security[enroll {kib} with an {es} cluster], you pass a generated enrollment token. This token +configures {kib} to authenticate with {es} using a +{ref}/service-accounts.html#service-accounts-tokens[service account token]. +{kib} also supports mutual TLS authentication with {es} via a +{ref}/pki-realm.html[Public Key Infrastructure (PKI) realm]. With this setup, +{es} needs to verify the signature on the {kib} client certificate, and it also +needs to map the client certificate's distinguished name (DN) to the appropriate +`kibana_system` role. + +NOTE: Using a PKI realm is a gold feature. For a comparison of the Elastic +license levels, see https://www.elastic.co/subscriptions[the subscription page]. + +[discrete] +==== Configure {kib} and {es} to use mutual TLS authentication + +If you haven't already, start {kib} and connect it to {es} using the +{ref}/configuring-stack-security.html#stack-start-with-security[enrollment token]. . Obtain a client certificate and private key for {kib}. + @@ -43,15 +49,14 @@ This entails generating a "server certificate" for {es} to use on the HTTP layer NOTE: This is not the same as the <> that {kib} will present to web browsers. -You may choose to generate a client certificate and private key using the {ref}/certutil.html[`elasticsearch-certutil`] tool. If you -followed the {es} documentation for {ref}/configuring-tls.html#node-certificates[generating node certificates], then you likely have already -set up a certificate authority (CA) to sign the {es} server certificate. You may choose to use the same CA to sign the {kib} client -certificate. For example: +You may choose to generate a client certificate and private key using the {ref}/certutil.html[`elasticsearch-certutil`] tool. If you followed the {es} documentation for {ref}/security-basic-setup.html#generate-certificates[generating the certificates authority], then you already have a certificate authority (CA) to sign +the {es} server certificate. You may choose to use the same CA to sign the {kib} +client certificate. For example: [source,sh] --------------------------------------------------------------------------------- +---- bin/elasticsearch-certutil cert -ca elastic-stack-ca.p12 -name kibana-client -dns --------------------------------------------------------------------------------- +---- This will generate a client certificate and private key in a PKCS#12 file named `kibana-client.p12`. In this example, the client certificate has a Common Name (CN) of `"kibana-client"` and a subject alternative name (SAN) of `""`. The SAN may be required if @@ -67,9 +72,9 @@ If you followed the instructions to generate a client certificate, then you will certificate chain from this file. For example: [source,sh] --------------------------------------------------------------------------------- +---- openssl pkcs12 -in kibana-client.p12 -cacerts -nokeys -out kibana-ca.crt --------------------------------------------------------------------------------- +---- This will produce a PEM-formatted file named `kibana-ca.crt` that contains the CA certificate from the PKCS#12 file. -- @@ -81,11 +86,11 @@ By default, {es} provides a native realm for authenticating with a username and and a native realm (for end users), you must configure each realm in `elasticsearch.yml`: [source,yaml] --------------------------------------------------------------------------------- +---- xpack.security.authc.realms.pki.realm1.order: 1 xpack.security.authc.realms.pki.realm1.certificate_authorities: "/path/to/kibana-ca.crt" xpack.security.authc.realms.native.realm2.order: 2 --------------------------------------------------------------------------------- +---- -- . Configure {es} to request client certificates. @@ -95,9 +100,9 @@ By default, {es} will not request a client certificate when establishing a TLS c certificate authentication in `elasticsearch.yml`: [source,yaml] --------------------------------------------------------------------------------- +---- xpack.security.http.ssl.client_authentication: "optional" --------------------------------------------------------------------------------- +---- -- . Restart {es}. @@ -124,16 +129,16 @@ You need to specify the information required to access your client certificate a Specify your PKCS#12 file in `kibana.yml`: [source,yaml] --------------------------------------------------------------------------------- +---- elasticsearch.ssl.keystore.path: "/path/to/kibana-client.p12" --------------------------------------------------------------------------------- +---- If your PKCS#12 file is encrypted, add the decryption password to your <>: [source,yaml] --------------------------------------------------------------------------------- +---- bin/kibana-keystore add elasticsearch.ssl.keystore.password --------------------------------------------------------------------------------- +---- TIP: If your PKCS#12 file isn't protected with a password, depending on how it was generated, you may need to set `elasticsearch.ssl.keystore.password` to an empty string. @@ -145,17 +150,17 @@ TIP: If your PKCS#12 file isn't protected with a password, depending on how it w Specify your certificate and private key in `kibana.yml`: [source,yaml] --------------------------------------------------------------------------------- +---- elasticsearch.ssl.certificate: "/path/to/kibana-client.crt" elasticsearch.ssl.key: "/path/to/kibana-client.key" --------------------------------------------------------------------------------- +---- If your private key is encrypted, add the decryption password to your <>: [source,yaml] --------------------------------------------------------------------------------- +---- bin/kibana-keystore add elasticsearch.ssl.keyPassphrase --------------------------------------------------------------------------------- +---- -- . Configure {kib} _not_ to use a username and password for {es}. diff --git a/docs/user/security/securing-communications/index.asciidoc b/docs/user/security/securing-communications/index.asciidoc deleted file mode 100644 index ac014f7abe52a..0000000000000 --- a/docs/user/security/securing-communications/index.asciidoc +++ /dev/null @@ -1,16 +0,0 @@ -[[configuring-tls-communication]] -=== Encrypt communications in {kib} -++++ -Encrypt communications -++++ - -Secure Sockets Layer (SSL) and Transport Layer Security (TLS) provide encryption for data-in-transit. While these terms are often used -interchangeably, {kib} supports only TLS, which supersedes the old SSL protocols. - -Browsers send traffic to {kib} and {kib} sends traffic to {es}. These communication channels are configured separately to use TLS. - -TLS requires X.509 certificates to authenticate the communicating parties and perform encryption of data-in-transit. Each certificate -contains a public key and has an associated -- but separate -- private key; these keys are used for cryptographic operations. {kib} -supports certificates and private keys in PEM or PKCS#12 format. - -See {ref}/security-basic-setup-https.html[Set up basic security for the Elastic Stack] to encrypt HTTP communications for {es} and {kib}. \ No newline at end of file diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index bdb36a6fe117c..875830ab88b46 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -5,42 +5,25 @@ Configure security ++++ -{kib} users have to log in when {stack-security-features} are enabled on your -cluster. You configure roles for your {kib} users to control what data those -users can access. +When you start {es} for the first time, {stack-security-features} are enabled on +your cluster and TLS is configured automatically. The security configuration +process generates a password for the `elastic` user and an enrollment token for +{kib}. +{ref}/configuring-stack-security.html[Start the {stack} with security enabled] +and then enroll {kib} as part of the configuration process. -Most requests made through {kib} to {es} are authenticated by using the -credentials of the logged-in user. There are, however, a few internal requests -that the {kib} server needs to make to the {es} cluster. For this reason, you -must configure credentials for the {kib} server to use for those requests. +You can then log in to {kib} as the `elastic` user to create additional roles +and users. -With {security-features} enabled, if you load a {kib} dashboard that accesses -data in an index that you are not authorized to view, you get an error that -indicates the index does not exist. The {security-features} do not currently -provide a way to control which users can load which dashboards. +NOTE: When a user is not authorized to view data in an index (such as an {es} +index), the entire index will be inaccessible and not display in {kib}. -To use {kib} with {security-features}: +[discrete] +[[security-configure-settings]] +=== Configure security settings -. {ref}/configuring-security.html[Configure security in {es}]. - -. Configure {kib} to use the appropriate built-in user. -+ --- -Update the following settings in the `kibana.yml` configuration -file: - -[source,yaml] ------------------------------------------------ -elasticsearch.username: "kibana_system" -elasticsearch.password: "kibanapassword" ------------------------------------------------ - -The {kib} server submits requests as this user to access the cluster monitoring -APIs and the `.kibana` index. The server does _not_ need access to user indices. - -NOTE: The password for the built-in `kibana_system` user is typically set as part of the security configuration process on {es}. For more -information, see {ref}/built-in-users.html[Built-in users]. --- +Set an encryption key so that sessions are not invalidated. You can optionally +configure additional security settings and authentication. . Set the `xpack.security.encryptionKey` property in the `kibana.yml` configuration file. You can use any text string that is 32 characters or longer @@ -48,36 +31,33 @@ as the encryption key. + -- [source,yaml] --------------------------------------------------------------------------------- +---- xpack.security.encryptionKey: "something_at_least_32_characters" --------------------------------------------------------------------------------- +---- For more information, see <>. -- -. Configure {kib}'s session expiration settings. Set both the idle timeout and lifespan settings: -+ --- -[source,yaml] --------------------------------------------------------------------------------- -xpack.security.session.idleTimeout: "1h" -xpack.security.session.lifespan: "30d" --------------------------------------------------------------------------------- - -For more information, see <>. --- - -. Optional: <>. +. Optional: <>. . Optional: <>. . Restart {kib}. -. Temporarily log in to {kib} using the built-in `elastic` superuser so you can create new users and assign roles. If you are running {kib} -locally, go to `https://localhost:5601` to view the login page. +[discrete] +[[security-create-roles]] +=== Create roles and users +Configure roles for your {kib} users to control what data those users can +access. + +. Temporarily log in to {kib} using the built-in `elastic` superuser so you can +create new users and assign roles. If you are running {kib} locally, go to +`https://localhost:5601` to view the login page. + -NOTE: The password for the built-in `elastic` user is typically set as part of the security configuration process on {es}. For more -information, see {ref}/built-in-users.html[Built-in users]. +NOTE: The password for the built-in `elastic` user is generated as part of the +security configuration process on {es}. If you need to reset the password for +the `elastic` user or other built-in users, run the +{ref}/reset-password.html[`elasticsearch-reset-password`] tool. . [[kibana-roles]]Create roles and users to grant access to {kib}. + @@ -120,7 +100,6 @@ NOTE: This must be a user who has been assigned < { "calls": Array [ Array [ Object { - "_source": "true", + "_source": true, "index": "bar", "query": undefined, "rest_total_hits_as_int": true, @@ -136,7 +136,7 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { ], Array [ Object { - "_source": "true", + "_source": true, "index": "foo", "query": undefined, "rest_total_hits_as_int": true, diff --git a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts index 4bd44b649afd2..b1b713f6f3cbf 100644 --- a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts @@ -38,7 +38,7 @@ export function createGenerateDocRecordsStream({ index, scroll: SCROLL_TIMEOUT, size: SCROLL_SIZE, - _source: 'true', + _source: true, query, rest_total_hits_as_int: true, }, diff --git a/packages/kbn-rule-data-utils/BUILD.bazel b/packages/kbn-rule-data-utils/BUILD.bazel index bbc2d524102db..730e907aafc65 100644 --- a/packages/kbn-rule-data-utils/BUILD.bazel +++ b/packages/kbn-rule-data-utils/BUILD.bazel @@ -20,6 +20,10 @@ filegroup( NPM_MODULE_EXTRA_FILES = [ "package.json", + "alerts_as_data_rbac/package.json", + "alerts_as_data_severity/package.json", + "alerts_as_data_status/package.json", + "technical_field_names/package.json", ] RUNTIME_DEPS = [ diff --git a/packages/kbn-rule-data-utils/alerts_as_data_rbac/package.json b/packages/kbn-rule-data-utils/alerts_as_data_rbac/package.json new file mode 100644 index 0000000000000..7f06391bc9590 --- /dev/null +++ b/packages/kbn-rule-data-utils/alerts_as_data_rbac/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/alerts_as_data_rbac", + "types": "../target_types/alerts_as_data_rbac" +} \ No newline at end of file diff --git a/packages/kbn-rule-data-utils/alerts_as_data_severity/package.json b/packages/kbn-rule-data-utils/alerts_as_data_severity/package.json new file mode 100644 index 0000000000000..b053fc29e47c2 --- /dev/null +++ b/packages/kbn-rule-data-utils/alerts_as_data_severity/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/alerts_as_data_severity", + "types": "../target_types/alerts_as_data_severity" +} \ No newline at end of file diff --git a/packages/kbn-rule-data-utils/alerts_as_data_status/package.json b/packages/kbn-rule-data-utils/alerts_as_data_status/package.json new file mode 100644 index 0000000000000..4f621d252a2dc --- /dev/null +++ b/packages/kbn-rule-data-utils/alerts_as_data_status/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/alerts_as_data_status", + "types": "../target_types/alerts_as_data_status" +} \ No newline at end of file diff --git a/packages/kbn-rule-data-utils/technical_field_names/package.json b/packages/kbn-rule-data-utils/technical_field_names/package.json new file mode 100644 index 0000000000000..cbac9db58f5f8 --- /dev/null +++ b/packages/kbn-rule-data-utils/technical_field_names/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/technical_field_names", + "types": "../target_types/technical_field_names" +} \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel b/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel index 24819bdd16a33..c827eb19f7a3d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel @@ -37,7 +37,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/elastic-datemath", + "//packages/elastic-datemath:npm_module_types", "@npm//fp-ts", "@npm//io-ts", "@npm//moment", diff --git a/packages/kbn-ui-shared-deps-src/BUILD.bazel b/packages/kbn-ui-shared-deps-src/BUILD.bazel index d1c9115c0515b..f4597c1ee66b1 100644 --- a/packages/kbn-ui-shared-deps-src/BUILD.bazel +++ b/packages/kbn-ui-shared-deps-src/BUILD.bazel @@ -41,7 +41,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/elastic-datemath", + "//packages/elastic-datemath:npm_module_types", "//packages/elastic-safer-lodash-set", "//packages/kbn-analytics", "//packages/kbn-babel-preset", diff --git a/src/core/server/logging/appenders/file/file_appender.test.mocks.ts b/src/core/server/logging/appenders/file/file_appender.test.mocks.ts index 2c2a2015b6fd3..48173d683e9a4 100644 --- a/src/core/server/logging/appenders/file/file_appender.test.mocks.ts +++ b/src/core/server/logging/appenders/file/file_appender.test.mocks.ts @@ -19,4 +19,5 @@ jest.mock('../../layouts/layouts', () => { }); export const mockCreateWriteStream = jest.fn(); -jest.mock('fs', () => ({ createWriteStream: mockCreateWriteStream })); +export const mockMkdirSync = jest.fn(); +jest.mock('fs', () => ({ createWriteStream: mockCreateWriteStream, mkdirSync: mockMkdirSync })); diff --git a/src/core/server/logging/appenders/file/file_appender.test.ts b/src/core/server/logging/appenders/file/file_appender.test.ts index 081cb16afd2ff..f3a5d483d5a7b 100644 --- a/src/core/server/logging/appenders/file/file_appender.test.ts +++ b/src/core/server/logging/appenders/file/file_appender.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { mockCreateWriteStream } from './file_appender.test.mocks'; +import { mockCreateWriteStream, mockMkdirSync } from './file_appender.test.mocks'; import { LogRecord, LogLevel } from '@kbn/logging'; import { FileAppender } from './file_appender'; @@ -15,6 +15,7 @@ const tickMs = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) beforeEach(() => { mockCreateWriteStream.mockReset(); + mockMkdirSync.mockReset(); }); test('`createConfigSchema()` creates correct schema.', () => { @@ -49,8 +50,10 @@ test('file stream is created only once and only after first `append()` is called }); const mockPath = 'mock://path/file.log'; + const mockDir = 'mock://path'; const appender = new FileAppender({ format: () => '' }, mockPath); + expect(mockMkdirSync).not.toHaveBeenCalled(); expect(mockCreateWriteStream).not.toHaveBeenCalled(); appender.append({ @@ -61,12 +64,17 @@ test('file stream is created only once and only after first `append()` is called pid: 5355, }); + expect(mockMkdirSync).toHaveBeenCalledTimes(1); + expect(mockMkdirSync).toHaveBeenCalledWith(mockDir, { + recursive: true, + }); expect(mockCreateWriteStream).toHaveBeenCalledTimes(1); expect(mockCreateWriteStream).toHaveBeenCalledWith(mockPath, { encoding: 'utf8', flags: 'a', }); + mockMkdirSync.mockClear(); mockCreateWriteStream.mockClear(); appender.append({ context: 'context-2', @@ -76,6 +84,7 @@ test('file stream is created only once and only after first `append()` is called pid: 5355, }); + expect(mockMkdirSync).not.toHaveBeenCalled(); expect(mockCreateWriteStream).not.toHaveBeenCalled(); }); diff --git a/src/core/server/logging/appenders/file/file_appender.ts b/src/core/server/logging/appenders/file/file_appender.ts index 3c22a37038bcc..1c0f842f6c630 100644 --- a/src/core/server/logging/appenders/file/file_appender.ts +++ b/src/core/server/logging/appenders/file/file_appender.ts @@ -8,7 +8,8 @@ import { schema } from '@kbn/config-schema'; import { LogRecord, Layout, DisposableAppender } from '@kbn/logging'; -import { createWriteStream, WriteStream } from 'fs'; +import { createWriteStream, WriteStream, mkdirSync } from 'fs'; +import { dirname } from 'path'; import { Layouts, LayoutConfigType } from '../../layouts/layouts'; @@ -47,6 +48,7 @@ export class FileAppender implements DisposableAppender { */ public append(record: LogRecord) { if (this.outputStream === undefined) { + this.ensureDirectory(this.path); this.outputStream = createWriteStream(this.path, { encoding: 'utf8', flags: 'a', @@ -73,4 +75,8 @@ export class FileAppender implements DisposableAppender { }); }); } + + private ensureDirectory(path: string) { + mkdirSync(dirname(path), { recursive: true }); + } } diff --git a/src/core/server/logging/appenders/rolling_file/rolling_file_manager.ts b/src/core/server/logging/appenders/rolling_file/rolling_file_manager.ts index e96c57578f908..df9c531fa2e7f 100644 --- a/src/core/server/logging/appenders/rolling_file/rolling_file_manager.ts +++ b/src/core/server/logging/appenders/rolling_file/rolling_file_manager.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { createWriteStream, WriteStream } from 'fs'; +import { createWriteStream, WriteStream, mkdirSync } from 'fs'; +import { dirname } from 'path'; import { RollingFileContext } from './rolling_file_context'; /** @@ -40,6 +41,7 @@ export class RollingFileManager { private ensureStreamOpen() { if (this.outputStream === undefined) { + this.ensureDirectory(this.filePath); this.outputStream = createWriteStream(this.filePath, { encoding: 'utf8', flags: 'a', @@ -49,4 +51,8 @@ export class RollingFileManager { } return this.outputStream!; } + + private ensureDirectory(path: string) { + mkdirSync(dirname(path), { recursive: true }); + } } diff --git a/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.mocks.ts b/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.mocks.ts index 691bfc5030c93..5559a2be5b8d2 100644 --- a/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.mocks.ts +++ b/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.mocks.ts @@ -9,15 +9,17 @@ import moment from 'moment'; import type { EventLoopDelaysMonitor } from './event_loop_delays_monitor'; import type { IntervalHistogram } from '../types'; -function createMockHistogram(overwrites: Partial = {}): IntervalHistogram { +function createMockRawNsDataHistogram( + overwrites: Partial = {} +): IntervalHistogram { const now = Date.now(); - return { + const mockedRawCollectedDataInNs = { min: 9093120, max: 53247999, - mean: 11993238.600747818, + mean: 11993238, exceeds: 0, - stddev: 1168191.9357543814, + stddev: 1168191, fromTimestamp: moment(now).toISOString(), lastUpdatedAt: moment(now).toISOString(), percentiles: { @@ -28,6 +30,31 @@ function createMockHistogram(overwrites: Partial = {}): Inter }, ...overwrites, }; + return mockedRawCollectedDataInNs; +} + +function createMockMonitorDataMsHistogram( + overwrites: Partial = {} +): IntervalHistogram { + const now = Date.now(); + + const mockedRawCollectedDataInMs = { + min: 9.09312, + max: 53.247999, + mean: 11.993238, + exceeds: 0, + stddev: 1.168191, + fromTimestamp: moment(now).toISOString(), + lastUpdatedAt: moment(now).toISOString(), + percentiles: { + '50': 12.607487, + '75': 12.615679, + '95': 12.648447, + '99': 12.713983, + }, + ...overwrites, + }; + return mockedRawCollectedDataInMs; } function createMockEventLoopDelaysMonitor() { @@ -40,12 +67,12 @@ function createMockEventLoopDelaysMonitor() { stop: jest.fn(), }); - mockCollect.mockReturnValue(createMockHistogram()); + mockCollect.mockReturnValue(createMockMonitorDataMsHistogram()); // this must mock the return value of the public collect method from this monitor. return new MockEventLoopDelaysMonitor(); } export const mocked = { - createHistogram: createMockHistogram, + createHistogram: createMockRawNsDataHistogram, // raw data as received from Node.js perf_hooks.monitorEventLoopDelay([options]) createEventLoopDelaysMonitor: createMockEventLoopDelaysMonitor, }; diff --git a/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.test.ts b/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.test.ts index 3e88dbca8f4e7..052f291f76889 100644 --- a/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.test.ts +++ b/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.test.ts @@ -9,7 +9,7 @@ /* eslint-disable dot-notation */ jest.mock('perf_hooks'); import { monitorEventLoopDelay } from 'perf_hooks'; -import { EventLoopDelaysMonitor } from './event_loop_delays_monitor'; +import { EventLoopDelaysMonitor, nsToMs } from './event_loop_delays_monitor'; import { mocked } from './event_loop_delays_monitor.mocks'; describe('EventLoopDelaysMonitor', () => { @@ -39,10 +39,20 @@ describe('EventLoopDelaysMonitor', () => { expect(eventLoopDelaysMonitor['loopMonitor'].percentile).toHaveBeenNthCalledWith(3, 95); expect(eventLoopDelaysMonitor['loopMonitor'].percentile).toHaveBeenNthCalledWith(4, 99); - // mocked perf_hook returns `mocked.createHistogram()`. + // mocked perf_hook returns `mocked.createNsHistogram()` that returns data in ns. + // The `collect` function returns the data in ms. // This ensures that the wiring of the `collect` function is correct. const mockedHistogram = mocked.createHistogram(); - expect(histogramData).toEqual(mockedHistogram); + + expect(histogramData.min).toEqual(nsToMs(mockedHistogram.min)); + expect(histogramData.max).toEqual(nsToMs(mockedHistogram.max)); + expect(histogramData.mean).toEqual(nsToMs(mockedHistogram.mean)); + expect(histogramData.stddev).toEqual(nsToMs(mockedHistogram.stddev)); + expect(histogramData.exceeds).toEqual(nsToMs(mockedHistogram.exceeds)); + expect(histogramData.percentiles['50']).toEqual(nsToMs(mockedHistogram.percentiles['50'])); + expect(histogramData.percentiles['75']).toEqual(nsToMs(mockedHistogram.percentiles['75'])); + expect(histogramData.percentiles['95']).toEqual(nsToMs(mockedHistogram.percentiles['95'])); + expect(histogramData.percentiles['99']).toEqual(nsToMs(mockedHistogram.percentiles['99'])); }); test('#reset resets histogram data', () => { diff --git a/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.ts b/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.ts index 0f3035c14a923..d36d5428be4d9 100644 --- a/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.ts +++ b/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.ts @@ -10,6 +10,18 @@ import type { IntervalHistogram as PerfIntervalHistogram } from 'perf_hooks'; import { monitorEventLoopDelay } from 'perf_hooks'; import type { IntervalHistogram } from '../types'; +/** + * Nanosecond to milisecond conversion unit + */ +export const ONE_MILLISECOND_AS_NANOSECONDS = 1_000_000; + +/** + * Converts time metric from ns to ms + **/ +export function nsToMs(metric: number) { + return metric / ONE_MILLISECOND_AS_NANOSECONDS; +} + export class EventLoopDelaysMonitor { private readonly loopMonitor: PerfIntervalHistogram; private fromTimestamp: Date; @@ -28,26 +40,36 @@ export class EventLoopDelaysMonitor { * Collect gathers event loop delays metrics from nodejs perf_hooks.monitorEventLoopDelay * the histogram calculations start from the last time `reset` was called or this * EventLoopDelaysMonitor instance was created. + * + * Returns metrics in milliseconds. + * @returns {IntervalHistogram} */ + public collect(): IntervalHistogram { const lastUpdated = new Date(); this.loopMonitor.disable(); - const { min, max, mean, exceeds, stddev } = this.loopMonitor; + const { + min: minNs, + max: maxNs, + mean: meanNs, + exceeds: exceedsNs, + stddev: stddevNs, + } = this.loopMonitor; const collectedData: IntervalHistogram = { - min, - max, - mean, - exceeds, - stddev, + min: nsToMs(minNs), + max: nsToMs(maxNs), + mean: nsToMs(meanNs), + exceeds: nsToMs(exceedsNs), + stddev: nsToMs(stddevNs), fromTimestamp: this.fromTimestamp.toISOString(), lastUpdatedAt: lastUpdated.toISOString(), percentiles: { - 50: this.loopMonitor.percentile(50), - 75: this.loopMonitor.percentile(75), - 95: this.loopMonitor.percentile(95), - 99: this.loopMonitor.percentile(99), + 50: nsToMs(this.loopMonitor.percentile(50)), + 75: nsToMs(this.loopMonitor.percentile(75)), + 95: nsToMs(this.loopMonitor.percentile(95)), + 99: nsToMs(this.loopMonitor.percentile(99)), }, }; diff --git a/src/core/server/metrics/types.ts b/src/core/server/metrics/types.ts index 550a60d0d295a..20594f5122f08 100644 --- a/src/core/server/metrics/types.ts +++ b/src/core/server/metrics/types.ts @@ -70,7 +70,7 @@ export interface OpsMetrics { /** * an IntervalHistogram object that samples and reports the event loop delay over time. - * The delays will be reported in nanoseconds. + * The delays will be reported in milliseconds. * * @public */ diff --git a/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.test.ts b/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.test.ts index 8a99d28b40de1..0ddb858f91bc0 100644 --- a/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.test.ts +++ b/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.test.ts @@ -98,12 +98,10 @@ describe('checkForUnknownDocs', () => { const result = await task(); expect(Either.isRight(result)).toBe(true); - expect((result as Either.Right).right).toEqual({ - unknownDocs: [], - }); + expect((result as Either.Right).right).toEqual({}); }); - it('resolves with `Either.right` when unknown docs are found', async () => { + it('resolves with `Either.left` when unknown docs are found', async () => { const client = elasticsearchClientMock.createInternalClient( elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { @@ -124,8 +122,9 @@ describe('checkForUnknownDocs', () => { const result = await task(); - expect(Either.isRight(result)).toBe(true); - expect((result as Either.Right).right).toEqual({ + expect(Either.isLeft(result)).toBe(true); + expect((result as Either.Left).left).toEqual({ + type: 'unknown_docs_found', unknownDocs: [ { id: '12', type: 'foo' }, { id: '14', type: 'bar' }, @@ -151,8 +150,9 @@ describe('checkForUnknownDocs', () => { const result = await task(); - expect(Either.isRight(result)).toBe(true); - expect((result as Either.Right).right).toEqual({ + expect(Either.isLeft(result)).toBe(true); + expect((result as Either.Left).left).toEqual({ + type: 'unknown_docs_found', unknownDocs: [{ id: '12', type: 'unknown' }], }); }); diff --git a/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.ts b/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.ts index cfeda0548b16a..6dd8fbda73c95 100644 --- a/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.ts +++ b/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.ts @@ -32,6 +32,7 @@ export interface CheckForUnknownDocsFoundDoc { /** @internal */ export interface UnknownDocsFound { + type: 'unknown_docs_found'; unknownDocs: CheckForUnknownDocsFoundDoc[]; } @@ -41,7 +42,10 @@ export const checkForUnknownDocs = indexName, unusedTypesQuery, knownTypes, - }: CheckForUnknownDocsParams): TaskEither.TaskEither => + }: CheckForUnknownDocsParams): TaskEither.TaskEither< + RetryableEsClientError | UnknownDocsFound, + {} + > => () => { const query = createUnknownDocQuery(unusedTypesQuery, knownTypes); @@ -54,9 +58,14 @@ export const checkForUnknownDocs = }) .then((response) => { const { hits } = response.body.hits; - return Either.right({ - unknownDocs: hits.map((hit) => ({ id: hit._id, type: hit._source?.type ?? 'unknown' })), - }); + if (hits.length) { + return Either.left({ + type: 'unknown_docs_found' as const, + unknownDocs: hits.map((hit) => ({ id: hit._id, type: hit._source?.type ?? 'unknown' })), + }); + } else { + return Either.right({}); + } }) .catch(catchRetryableEsClientErrors); }; diff --git a/src/core/server/saved_objects/migrations/actions/index.ts b/src/core/server/saved_objects/migrations/actions/index.ts index 158d97f3b7c27..4e88e9c448d40 100644 --- a/src/core/server/saved_objects/migrations/actions/index.ts +++ b/src/core/server/saved_objects/migrations/actions/index.ts @@ -80,6 +80,7 @@ export type { } from './update_and_pickup_mappings'; export { updateAndPickupMappings } from './update_and_pickup_mappings'; +import type { UnknownDocsFound } from './check_for_unknown_docs'; export type { CheckForUnknownDocsParams, UnknownDocsFound, @@ -141,6 +142,7 @@ export interface ActionErrorTypeMap { remove_index_not_a_concrete_index: RemoveIndexNotAConcreteIndex; documents_transform_failed: DocumentsTransformFailed; request_entity_too_large_exception: RequestEntityTooLargeException; + unknown_docs_found: UnknownDocsFound; } /** diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts index c8ff79351aadb..1effabe7bfc96 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts @@ -139,7 +139,6 @@ describe('migrateRawDocsSafely', () => { ]); const task = migrateRawDocsSafely({ serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - knownTypes: new Set(['a', 'c']), migrateDoc: transform, rawDocs: [ { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, @@ -184,7 +183,6 @@ describe('migrateRawDocsSafely', () => { ]); const task = migrateRawDocsSafely({ serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - knownTypes: new Set(['a', 'c']), migrateDoc: transform, rawDocs: [ { _id: 'foo:b', _source: { type: 'a', a: { name: 'AAA' } } }, @@ -206,7 +204,6 @@ describe('migrateRawDocsSafely', () => { ]); const task = migrateRawDocsSafely({ serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - knownTypes: new Set(['a', 'c']), migrateDoc: transform, rawDocs: [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }], }); @@ -240,7 +237,6 @@ describe('migrateRawDocsSafely', () => { }); const task = migrateRawDocsSafely({ serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - knownTypes: new Set(['a', 'c']), migrateDoc: transform, rawDocs: [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }], // this is the raw doc }); @@ -256,43 +252,4 @@ describe('migrateRawDocsSafely', () => { } `); }); - - test('skips documents of unknown types', async () => { - const transform = jest.fn((doc: any) => [ - set(_.cloneDeep(doc), 'attributes.name', 'HOI!'), - ]); - const task = migrateRawDocsSafely({ - serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - knownTypes: new Set(['a']), - migrateDoc: transform, - rawDocs: [ - { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, - { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, - ], - }); - - const result = (await task()) as Either.Right; - expect(result._tag).toEqual('Right'); - expect(result.right.processedDocs).toEqual([ - { - _id: 'a:b', - _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, - }, - { - _id: 'c:d', - // name field is not migrated on unknown type - _source: { type: 'c', c: { name: 'DDD' } }, - }, - ]); - - const obj1 = { - id: 'b', - type: 'a', - attributes: { name: 'AAA' }, - migrationVersion: {}, - references: [], - }; - expect(transform).toHaveBeenCalledTimes(1); - expect(transform).toHaveBeenNthCalledWith(1, obj1); - }); }); diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts index 65ea21a6778d5..fb9176d111c8e 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts @@ -25,13 +25,16 @@ export interface DocumentsTransformFailed { readonly corruptDocumentIds: string[]; readonly transformErrors: TransformErrorObjects[]; } + export interface DocumentsTransformSuccess { readonly processedDocs: SavedObjectsRawDoc[]; } + export interface TransformErrorObjects { readonly rawId: string; readonly err: TransformSavedObjectDocumentError | Error; } + type MigrateFn = ( doc: SavedObjectUnsanitizedDoc ) => Promise>>; @@ -83,7 +86,6 @@ export async function migrateRawDocs( interface MigrateRawDocsSafelyDeps { serializer: SavedObjectsSerializer; - knownTypes: ReadonlySet; migrateDoc: MigrateAndConvertFn; rawDocs: SavedObjectsRawDoc[]; } @@ -97,7 +99,6 @@ interface MigrateRawDocsSafelyDeps { */ export function migrateRawDocsSafely({ serializer, - knownTypes, migrateDoc, rawDocs, }: MigrateRawDocsSafelyDeps): TaskEither.TaskEither< @@ -111,10 +112,7 @@ export function migrateRawDocsSafely({ const corruptSavedObjectIds: string[] = []; const options = { namespaceTreatment: 'lax' as const }; for (const raw of rawDocs) { - // Do not transform documents of unknown types - if (raw?._source?.type && !knownTypes.has(raw._source.type)) { - processedDocs.push(raw); - } else if (serializer.isRawSavedObject(raw, options)) { + if (serializer.isRawSavedObject(raw, options)) { try { const savedObject = convertToRawAddMigrationVersion(raw, options, serializer); processedDocs.push( diff --git a/src/core/server/saved_objects/migrations/integration_tests/7_13_0_unknown_types.test.ts b/src/core/server/saved_objects/migrations/integration_tests/7_13_0_unknown_types.test.ts index aea84cea22862..0791b0db30c93 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/7_13_0_unknown_types.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/7_13_0_unknown_types.test.ts @@ -11,16 +11,7 @@ import fs from 'fs/promises'; import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; import { Root } from '../../../root'; -import JSON5 from 'json5'; -import { ElasticsearchClient } from '../../../elasticsearch'; -import { Env } from '@kbn/config'; -import { REPO_ROOT } from '@kbn/utils'; -import { getEnvOptions } from '../../../config/mocks'; -import { retryAsync } from '../test_helpers/retry_async'; -import { LogRecord } from '@kbn/logging'; -const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; -const targetIndex = `.kibana_${kibanaVersion}_001`; const logFilePath = Path.join(__dirname, '7_13_unknown_types.log'); async function removeLogFile() { @@ -63,152 +54,54 @@ describe('migration v2', () => { await new Promise((resolve) => setTimeout(resolve, 10000)); }); - it('logs a warning and completes the migration with unknown docs retained', async () => { - root = createRoot(); - esServer = await startES(); - await root.preboot(); - await root.setup(); - await root.start(); - - let unknownDocsWarningLog: LogRecord; - - await retryAsync( - async () => { - const logFileContent = await fs.readFile(logFilePath, 'utf-8'); - const records = logFileContent - .split('\n') - .filter(Boolean) - .map((str) => JSON5.parse(str)); - - unknownDocsWarningLog = records.find((rec) => - rec.message.startsWith(`[.kibana] CHECK_UNKNOWN_DOCUMENTS`) - ); - - expect( - unknownDocsWarningLog.message.startsWith( - '[.kibana] CHECK_UNKNOWN_DOCUMENTS Upgrades will fail for 8.0+ because documents were found for unknown saved ' + - 'object types. To ensure that upgrades will succeed in the future, either re-enable plugins or delete ' + - `these documents from the "${targetIndex}" index after the current upgrade completes.` - ) - ).toBeTruthy(); - }, - { retryAttempts: 10, retryDelayMs: 200 } - ); - - const unknownDocs = [ - { type: 'space', id: 'space:default' }, - { type: 'space', id: 'space:first' }, - { type: 'space', id: 'space:second' }, - { type: 'space', id: 'space:third' }, - { type: 'space', id: 'space:forth' }, - { type: 'space', id: 'space:fifth' }, - { type: 'space', id: 'space:sixth' }, - { type: 'foo', id: 'P2SQfHkBs3dBRGh--No5' }, - { type: 'foo', id: 'QGSZfHkBs3dBRGh-ANoD' }, - { type: 'foo', id: 'QWSZfHkBs3dBRGh-hNob' }, - ]; - - unknownDocs.forEach(({ id, type }) => { - expect(unknownDocsWarningLog.message).toEqual( - expect.stringContaining(`- "${id}" (type: "${type}")`) - ); - }); - - const client: ElasticsearchClient = esServer.es.getKibanaEsClient(); - const { body: response } = await client.indices.getSettings({ - index: targetIndex, - }); - const settings = response[targetIndex].settings as estypes.IndicesIndexStatePrefixedSettings; - expect(settings.index).not.toBeUndefined(); - expect(settings.index!.blocks?.write).not.toEqual('true'); - - // Ensure that documents for unknown types were preserved in target index in an unmigrated state - const spaceDocs = await fetchDocs(client, targetIndex, 'space'); - expect(spaceDocs.map((s) => s.id)).toEqual( - expect.arrayContaining([ - 'space:default', - 'space:first', - 'space:second', - 'space:third', - 'space:forth', - 'space:fifth', - 'space:sixth', - ]) - ); - spaceDocs.forEach((d) => { - expect(d.migrationVersion.space).toEqual('6.6.0'); - expect(d.coreMigrationVersion).toEqual('7.13.0'); - }); - const fooDocs = await fetchDocs(client, targetIndex, 'foo'); - expect(fooDocs.map((f) => f.id)).toEqual( - expect.arrayContaining([ - 'P2SQfHkBs3dBRGh--No5', - 'QGSZfHkBs3dBRGh-ANoD', - 'QWSZfHkBs3dBRGh-hNob', - ]) - ); - fooDocs.forEach((d) => { - expect(d.migrationVersion.foo).toEqual('7.13.0'); - expect(d.coreMigrationVersion).toEqual('7.13.0'); - }); - }); - - it('migrates outdated documents when types are re-enabled', async () => { + it('fails the migration if unknown types are found in the source index', async () => { // Start kibana with foo and space types disabled root = createRoot(); esServer = await startES(); await root.preboot(); await root.setup(); - await root.start(); - // Shutdown and start Kibana again with space type registered to ensure space docs get migrated - await root.shutdown(); - root = createRoot(); - await root.preboot(); - const coreSetup = await root.setup(); - coreSetup.savedObjects.registerType({ - name: 'space', - hidden: false, - mappings: { properties: {} }, - namespaceType: 'agnostic', - migrations: { - '6.6.0': (d) => d, - [kibanaVersion]: (d) => d, - }, - }); - await root.start(); - - const client: ElasticsearchClient = esServer.es.getKibanaEsClient(); - const spacesDocsMigrated = await fetchDocs(client, targetIndex, 'space'); - expect(spacesDocsMigrated.map((s) => s.id)).toEqual( - expect.arrayContaining([ - 'space:default', - 'space:first', - 'space:second', - 'space:third', - 'space:forth', - 'space:fifth', - 'space:sixth', - ]) - ); - spacesDocsMigrated.forEach((d) => { - expect(d.migrationVersion.space).toEqual(kibanaVersion); // should be migrated - expect(d.coreMigrationVersion).toEqual(kibanaVersion); - }); - - // Make sure unmigrated foo docs are also still there in an unmigrated state - const fooDocsUnmigrated = await fetchDocs(client, targetIndex, 'foo'); - expect(fooDocsUnmigrated.map((f) => f.id)).toEqual( - expect.arrayContaining([ - 'P2SQfHkBs3dBRGh--No5', - 'QGSZfHkBs3dBRGh-ANoD', - 'QWSZfHkBs3dBRGh-hNob', - ]) - ); - fooDocsUnmigrated.forEach((d) => { - expect(d.migrationVersion.foo).toEqual('7.13.0'); // should still not be migrated - expect(d.coreMigrationVersion).toEqual('7.13.0'); - }); + try { + await root.start(); + expect('should have thrown').toEqual('but it did not'); + } catch (err) { + const errorMessage = err.message; + + expect( + errorMessage.startsWith( + 'Unable to complete saved object migrations for the [.kibana] index: Migration failed because documents ' + + 'were found for unknown saved object types. To proceed with the migration, please delete these documents from the ' + + '".kibana_7.13.0_001" index.' + ) + ).toBeTruthy(); + + const unknownDocs = [ + { type: 'space', id: 'space:default' }, + { type: 'space', id: 'space:first' }, + { type: 'space', id: 'space:second' }, + { type: 'space', id: 'space:third' }, + { type: 'space', id: 'space:forth' }, + { type: 'space', id: 'space:fifth' }, + { type: 'space', id: 'space:sixth' }, + { type: 'foo', id: 'P2SQfHkBs3dBRGh--No5' }, + { type: 'foo', id: 'QGSZfHkBs3dBRGh-ANoD' }, + { type: 'foo', id: 'QWSZfHkBs3dBRGh-hNob' }, + ]; + + unknownDocs.forEach(({ id, type }) => { + expect(errorMessage).toEqual(expect.stringContaining(`- "${id}" (type: "${type}")`)); + }); + + const client = esServer.es.getClient(); + const { body: response } = await client.indices.getSettings( + { index: '.kibana_7.13.0_001' }, + { meta: true } + ); + const settings = response['.kibana_7.13.0_001'] + .settings as estypes.IndicesIndexStatePrefixedSettings; + expect(settings.index).not.toBeUndefined(); + expect(settings.index!.blocks?.write).not.toEqual('true'); + } }); }); @@ -242,26 +135,3 @@ function createRoot() { } ); } - -async function fetchDocs(esClient: ElasticsearchClient, index: string, type: string) { - const { body } = await esClient.search({ - index, - size: 10000, - body: { - query: { - bool: { - should: [ - { - term: { type }, - }, - ], - }, - }, - }, - }); - - return body.hits.hits.map((h) => ({ - ...h._source, - id: h._id, - })); -} diff --git a/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts index e444a3b1a8bdb..3d8dcad08149c 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts @@ -109,7 +109,7 @@ describe('migration v2', () => { await root.preboot(); await root.setup(); await expect(root.start()).rejects.toMatchInlineSnapshot( - `[Error: Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715275 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.]` + `[Error: Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715274 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.]` ); await retryAsync( @@ -122,7 +122,7 @@ describe('migration v2', () => { expect( records.find((rec) => rec.message.startsWith( - `Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715275 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.` + `Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715274 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.` ) ) ).toBeDefined(); @@ -159,7 +159,7 @@ function createRoot(options: { maxBatchSizeBytes?: number }) { }, }, { - oss: true, + oss: false, } ); } diff --git a/src/core/server/saved_objects/migrations/integration_tests/collects_corrupt_docs.test.ts b/src/core/server/saved_objects/migrations/integration_tests/collects_corrupt_docs.test.ts index e330653089c6e..e5f0206b091fd 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/collects_corrupt_docs.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/collects_corrupt_docs.test.ts @@ -170,7 +170,7 @@ function createRoot() { }, }, { - oss: true, + oss: false, } ); } diff --git a/src/core/server/saved_objects/migrations/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana_migrator.ts index fa1172c0684a7..2e2c4e2c63c04 100644 --- a/src/core/server/saved_objects/migrations/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana_migrator.ts @@ -170,7 +170,6 @@ export class KibanaMigrator { transformRawDocs: (rawDocs: SavedObjectsRawDoc[]) => migrateRawDocsSafely({ serializer: this.serializer, - knownTypes: new Set(this.typeRegistry.getAllTypes().map((t) => t.name)), migrateDoc: this.documentMigrator.migrateAndConvert, rawDocs, }), diff --git a/src/core/server/saved_objects/migrations/model/extract_errors.test.ts b/src/core/server/saved_objects/migrations/model/extract_errors.test.ts index c2daadcd342ac..a028c40ca6597 100644 --- a/src/core/server/saved_objects/migrations/model/extract_errors.test.ts +++ b/src/core/server/saved_objects/migrations/model/extract_errors.test.ts @@ -25,7 +25,7 @@ describe('extractUnknownDocFailureReason', () => { '.kibana_15' ) ).toMatchInlineSnapshot(` - "Upgrades will fail for 8.0+ because documents were found for unknown saved object types. To ensure that upgrades will succeed in the future, either re-enable plugins or delete these documents from the \\".kibana_15\\" index after the current upgrade completes. + "Migration failed because documents were found for unknown saved object types. To proceed with the migration, please delete these documents from the \\".kibana_15\\" index. The documents with unknown types are: - \\"unknownType:12\\" (type: \\"unknownType\\") - \\"anotherUnknownType:42\\" (type: \\"anotherUnknownType\\") diff --git a/src/core/server/saved_objects/migrations/model/extract_errors.ts b/src/core/server/saved_objects/migrations/model/extract_errors.ts index 3dabb09043376..95d10603caa80 100644 --- a/src/core/server/saved_objects/migrations/model/extract_errors.ts +++ b/src/core/server/saved_objects/migrations/model/extract_errors.ts @@ -38,16 +38,15 @@ export function extractTransformFailuresReason( export function extractUnknownDocFailureReason( unknownDocs: CheckForUnknownDocsFoundDoc[], - targetIndex: string + sourceIndex: string ): string { return ( - `Upgrades will fail for 8.0+ because documents were found for unknown saved object types. ` + - `To ensure that upgrades will succeed in the future, either re-enable plugins or delete these documents from the ` + - `"${targetIndex}" index after the current upgrade completes.\n` + + `Migration failed because documents were found for unknown saved object types. ` + + `To proceed with the migration, please delete these documents from the "${sourceIndex}" index.\n` + `The documents with unknown types are:\n` + unknownDocs.map((doc) => `- "${doc.id}" (type: "${doc.type}")\n`).join('') + `You can delete them using the following command:\n` + - `curl -X POST "{elasticsearch}/${targetIndex}/_bulk?pretty" -H 'Content-Type: application/json' -d'\n` + + `curl -X POST "{elasticsearch}/${sourceIndex}/_bulk?pretty" -H 'Content-Type: application/json' -d'\n` + unknownDocs.map((doc) => `{ "delete" : { "_id" : "${doc.id}" } }\n`).join('') + `'` ); diff --git a/src/core/server/saved_objects/migrations/model/model.test.ts b/src/core/server/saved_objects/migrations/model/model.test.ts index 7cd5f63640d1d..5ca6713ca163f 100644 --- a/src/core/server/saved_objects/migrations/model/model.test.ts +++ b/src/core/server/saved_objects/migrations/model/model.test.ts @@ -717,7 +717,7 @@ describe('migrations v2 model', () => { }, } as const; - test('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK if action succeeds and no unknown docs are found', () => { + test('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK if action succeeds', () => { const checkUnknownDocumentsSourceState: CheckUnknownDocumentsState = { ...baseState, controlState: 'CHECK_UNKNOWN_DOCUMENTS', @@ -725,7 +725,7 @@ describe('migrations v2 model', () => { sourceIndexMappings: mappingsWithUnknownType, }; - const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.right({ unknownDocs: [] }); + const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.right({}); const newState = model(checkUnknownDocumentsSourceState, res); expect(newState.controlState).toEqual('SET_SOURCE_WRITE_BLOCK'); @@ -765,7 +765,7 @@ describe('migrations v2 model', () => { expect(newState.logs).toEqual([]); }); - test('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK and adds log if action succeeds and unknown docs were found', () => { + test('CHECK_UNKNOWN_DOCUMENTS -> FATAL if action fails and unknown docs were found', () => { const checkUnknownDocumentsSourceState: CheckUnknownDocumentsState = { ...baseState, controlState: 'CHECK_UNKNOWN_DOCUMENTS', @@ -773,51 +773,20 @@ describe('migrations v2 model', () => { sourceIndexMappings: mappingsWithUnknownType, }; - const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.right({ + const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.left({ + type: 'unknown_docs_found', unknownDocs: [ { id: 'dashboard:12', type: 'dashboard' }, { id: 'foo:17', type: 'foo' }, ], }); const newState = model(checkUnknownDocumentsSourceState, res); - expect(newState.controlState).toEqual('SET_SOURCE_WRITE_BLOCK'); + expect(newState.controlState).toEqual('FATAL'); expect(newState).toMatchObject({ - controlState: 'SET_SOURCE_WRITE_BLOCK', - sourceIndex: Option.some('.kibana_3'), - targetIndex: '.kibana_7.11.0_001', - }); - - // This snapshot asserts that we disable the unknown saved object - // type. Because it's mappings are disabled, we also don't copy the - // `_meta.migrationMappingPropertyHashes` for the disabled type. - expect(newState.targetIndexMappings).toMatchInlineSnapshot(` - Object { - "_meta": Object { - "migrationMappingPropertyHashes": Object { - "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", - }, - }, - "properties": Object { - "disabled_saved_object_type": Object { - "dynamic": false, - "properties": Object {}, - }, - "new_saved_object_type": Object { - "properties": Object { - "value": Object { - "type": "text", - }, - }, - }, - }, - } - `); - - expect(newState.logs[0]).toMatchObject({ - level: 'warning', - message: expect.stringContaining( - 'Upgrades will fail for 8.0+ because documents were found for unknown saved object types' + controlState: 'FATAL', + reason: expect.stringContaining( + 'Migration failed because documents were found for unknown saved object types' ), }); }); diff --git a/src/core/server/saved_objects/migrations/model/model.ts b/src/core/server/saved_objects/migrations/model/model.ts index 522a43a737cb7..e9efb72bca6f5 100644 --- a/src/core/server/saved_objects/migrations/model/model.ts +++ b/src/core/server/saved_objects/migrations/model/model.ts @@ -11,7 +11,6 @@ import * as Option from 'fp-ts/lib/Option'; import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { AliasAction, isLeftTypeof } from '../actions'; -import { MigrationLog } from '../types'; import { AllActionStates, State } from '../state'; import type { ResponseType } from '../next'; import { disableUnknownTypeMappingFields } from '../core'; @@ -352,24 +351,17 @@ export const model = (currentState: State, resW: ResponseType): { add: { index: target, alias: stateP.versionAlias } }, { remove_index: { index: stateP.tempIndex } }, ]), - - logs: [ - ...stateP.logs, - ...(res.right.unknownDocs.length > 0 - ? ([ - { - level: 'warning', - message: `CHECK_UNKNOWN_DOCUMENTS ${extractUnknownDocFailureReason( - res.right.unknownDocs, - target - )}`, - }, - ] as MigrationLog[]) - : []), - ], }; } else { - return throwBadResponse(stateP, res); + if (isLeftTypeof(res.left, 'unknown_docs_found')) { + return { + ...stateP, + controlState: 'FATAL', + reason: extractUnknownDocFailureReason(res.left.unknownDocs, stateP.sourceIndex.value), + }; + } else { + return throwBadResponse(stateP, res.left); + } } } else if (stateP.controlState === 'SET_SOURCE_WRITE_BLOCK') { const res = resW as ExcludeRetryableEsError>; diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index d538690fb1920..9be58f1b71861 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -1825,7 +1825,7 @@ export class SavedObjectsRepository { index: this.getIndexForType(type), refresh, require_alias: true, - _source: 'true', + _source: true, body: { script: { source: ` diff --git a/src/dev/bazel/index.bzl b/src/dev/bazel/index.bzl index 83d6361ff95f7..fcd4212bd5329 100644 --- a/src/dev/bazel/index.bzl +++ b/src/dev/bazel/index.bzl @@ -6,14 +6,16 @@ # Side Public License, v 1. # -"""Public API interface for Bazel custom rules. +"""Public API interface for all Kibana Bazel custom rules. Please do not import from any other files when looking to use a custom rule """ load("//src/dev/bazel:jsts_transpiler.bzl", _jsts_transpiler = "jsts_transpiler") -load("//src/dev/bazel:ts_project.bzl", _ts_project = "ts_project") load("//src/dev/bazel:pkg_npm.bzl", _pkg_npm = "pkg_npm") +load("//src/dev/bazel/pkg_npm_types:index.bzl", _pkg_npm_types = "pkg_npm_types") +load("//src/dev/bazel:ts_project.bzl", _ts_project = "ts_project") jsts_transpiler = _jsts_transpiler pkg_npm = _pkg_npm +pkg_npm_types = _pkg_npm_types ts_project = _ts_project diff --git a/src/dev/bazel/pkg_npm_types/BUILD.bazel b/src/dev/bazel/pkg_npm_types/BUILD.bazel new file mode 100644 index 0000000000000..f30d0f8cb8324 --- /dev/null +++ b/src/dev/bazel/pkg_npm_types/BUILD.bazel @@ -0,0 +1,28 @@ +package(default_visibility = ["//visibility:public"]) + +load("@build_bazel_rules_nodejs//internal/node:node.bzl", "nodejs_binary") + +filegroup( + name = "packager_all_files", + srcs = glob([ + "packager/*", + ]), +) + +exports_files( + [ + "package_json.mustache", + ], + visibility = ["//visibility:public"] +) + +nodejs_binary( + name = "_packager", + data = [ + "@npm//@bazel/typescript", + "@npm//@microsoft/api-extractor", + "@npm//mustache", + ":packager_all_files" + ], + entry_point = ":packager/index.js", +) diff --git a/src/dev/bazel/pkg_npm_types/index.bzl b/src/dev/bazel/pkg_npm_types/index.bzl new file mode 100644 index 0000000000000..578ecdd885d15 --- /dev/null +++ b/src/dev/bazel/pkg_npm_types/index.bzl @@ -0,0 +1,15 @@ +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0 and the Server Side Public License, v 1; you may not use this file except +# in compliance with, at your election, the Elastic License 2.0 or the Server +# Side Public License, v 1. +# + +"""Public API interface for pkg_npm_types rule. +Please do not import from any other files when looking to this rule +""" + +load(":pkg_npm_types.bzl", _pkg_npm_types = "pkg_npm_types") + +pkg_npm_types = _pkg_npm_types diff --git a/src/dev/bazel/pkg_npm_types/package_json.mustache b/src/dev/bazel/pkg_npm_types/package_json.mustache new file mode 100644 index 0000000000000..2229345252e3f --- /dev/null +++ b/src/dev/bazel/pkg_npm_types/package_json.mustache @@ -0,0 +1,8 @@ +{ + "name": "{{{NAME}}}", + "description": "Generated by Bazel", + "types": "./index.d.ts", + "private": true, + "license": "MIT", + "version": "1.1.0" +} diff --git a/src/dev/bazel/pkg_npm_types/packager/create_api_extraction.js b/src/dev/bazel/pkg_npm_types/packager/create_api_extraction.js new file mode 100644 index 0000000000000..d5f7e0c33ff1c --- /dev/null +++ b/src/dev/bazel/pkg_npm_types/packager/create_api_extraction.js @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +const { format, parseTsconfig } = require('@bazel/typescript'); +const { Extractor, ExtractorConfig } = require('@microsoft/api-extractor'); +const fs = require('fs'); +const path = require('path'); + +function createApiExtraction( + tsConfig, + entryPoint, + dtsBundleOut, + apiReviewFolder, + acceptApiUpdates = false +) { + const [parsedConfig, errors] = parseTsconfig(tsConfig); + if (errors && errors.length) { + console.error(format('', errors)); + return 1; + } + const pkgJson = path.resolve(path.dirname(entryPoint), 'package.json'); + if (!fs.existsSync(pkgJson)) { + fs.writeFileSync( + pkgJson, + JSON.stringify({ + name: 'GENERATED-BY-BAZEL', + description: 'This is a dummy package.json as API Extractor always requires one.', + types: './index.d.ts', + private: true, + license: 'SSPL-1.0 OR Elastic License 2.0', + version: '1.0.0', + }) + ); + } + // API extractor doesn't always support the version of TypeScript used in the repo + // example: at the moment it is not compatable with 3.2 + // to use the internal TypeScript we shall not create a program but rather pass a parsed tsConfig. + const parsedTsConfig = parsedConfig.config; + const extractorOptions = { + localBuild: acceptApiUpdates, + }; + const configObject = { + compiler: { + overrideTsconfig: parsedTsConfig, + }, + projectFolder: path.resolve(path.dirname(tsConfig)), + mainEntryPointFilePath: path.resolve(entryPoint), + apiReport: { + enabled: !!apiReviewFolder, + // TODO(alan-agius4): remove this folder name when the below issue is solved upstream + // See: https://github.com/microsoft/web-build-tools/issues/1470 + reportFileName: (apiReviewFolder && path.resolve(apiReviewFolder)) || 'invalid', + }, + docModel: { + enabled: false, + }, + dtsRollup: { + enabled: !!dtsBundleOut, + untrimmedFilePath: dtsBundleOut && path.resolve(dtsBundleOut), + }, + tsdocMetadata: { + enabled: false, + }, + }; + const options = { + configObject, + packageJson: undefined, + packageJsonFullPath: pkgJson, + configObjectFullPath: undefined, + }; + const extractorConfig = ExtractorConfig.prepare(options); + const { succeeded } = Extractor.invoke(extractorConfig, extractorOptions); + // API extractor errors are emitted by it's logger. + return succeeded ? 0 : 1; +} + +module.exports.createApiExtraction = createApiExtraction; diff --git a/src/dev/bazel/pkg_npm_types/packager/generate_package_json.js b/src/dev/bazel/pkg_npm_types/packager/generate_package_json.js new file mode 100644 index 0000000000000..d4a478a262e5b --- /dev/null +++ b/src/dev/bazel/pkg_npm_types/packager/generate_package_json.js @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const fs = require('fs'); +const Mustache = require('mustache'); +const path = require('path'); + +function generatePackageJson(outputBasePath, packageJsonTemplatePath, rawPackageJsonTemplateArgs) { + const packageJsonTemplateArgsInTuples = rawPackageJsonTemplateArgs.reduce( + (a, v) => { + const lastTupleIdx = a.length - 1; + const lastTupleSize = a[lastTupleIdx].length; + + if (lastTupleSize < 2) { + a[lastTupleIdx].push(v); + + return a; + } + + return a.push([v]); + }, + [[]] + ); + const packageJsonTemplateArgs = Object.fromEntries(new Map(packageJsonTemplateArgsInTuples)); + + try { + const template = fs.readFileSync(packageJsonTemplatePath); + const renderedTemplate = Mustache.render(template.toString(), packageJsonTemplateArgs); + fs.writeFileSync(path.resolve(outputBasePath, 'package.json'), renderedTemplate); + } catch (e) { + console.error(e); + return 1; + } + + return 0; +} + +module.exports.generatePackageJson = generatePackageJson; diff --git a/src/dev/bazel/pkg_npm_types/packager/index.js b/src/dev/bazel/pkg_npm_types/packager/index.js new file mode 100644 index 0000000000000..cda299a99d76f --- /dev/null +++ b/src/dev/bazel/pkg_npm_types/packager/index.js @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const { createApiExtraction } = require('./create_api_extraction'); +const { generatePackageJson } = require('./generate_package_json'); +const path = require('path'); + +const DEBUG = false; + +if (require.main === module) { + if (DEBUG) { + console.error(` +pkg_npm_types packager: running with + cwd: ${process.cwd()} + argv: + ${process.argv.join('\n ')} + `); + } + + // layout args + const [ + outputBasePath, + packageJsonTemplatePath, + stringifiedPackageJsonTemplateArgs, + tsConfig, + entryPoint, + ] = process.argv.slice(2); + const dtsBundleOutput = path.resolve(outputBasePath, 'index.d.ts'); + + // generate pkg json output + const generatePackageJsonRValue = generatePackageJson( + outputBasePath, + packageJsonTemplatePath, + stringifiedPackageJsonTemplateArgs.split(',') + ); + // create api extraction output + const createApiExtractionRValue = createApiExtraction(tsConfig, entryPoint, dtsBundleOutput); + + // setup correct exit code + process.exitCode = generatePackageJsonRValue || createApiExtractionRValue; +} diff --git a/src/dev/bazel/pkg_npm_types/pkg_npm_types.bzl b/src/dev/bazel/pkg_npm_types/pkg_npm_types.bzl new file mode 100644 index 0000000000000..a40624d31e38b --- /dev/null +++ b/src/dev/bazel/pkg_npm_types/pkg_npm_types.bzl @@ -0,0 +1,148 @@ +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0 and the Server Side Public License, v 1; you may not use this file except +# in compliance with, at your election, the Elastic License 2.0 or the Server +# Side Public License, v 1. +# + +load("@npm//@bazel/typescript/internal:ts_config.bzl", "TsConfigInfo") +load("@build_bazel_rules_nodejs//:providers.bzl", "run_node", "LinkablePackageInfo", "declaration_info") +load("@build_bazel_rules_nodejs//internal/linker:link_node_modules.bzl", "module_mappings_aspect") + + +#### TODO +# Implement a way to produce source maps for api extractor +# summarised types as referenced at (https://github.com/microsoft/rushstack/issues/1886#issuecomment-933997910) + +def _deps_inputs(ctx): + """Returns all transitively referenced files on deps """ + deps_files_depsets = [] + for dep in ctx.attr.deps: + # Collect whatever is in the "data" + deps_files_depsets.append(dep.data_runfiles.files) + + # Only collect DefaultInfo files (not transitive) + deps_files_depsets.append(dep.files) + + deps_files = depset(transitive = deps_files_depsets).to_list() + return deps_files + +def _calculate_entrypoint_path(ctx): + return _join(ctx.bin_dir.path, ctx.label.package, _get_types_outdir_name(ctx), ctx.attr.entrypoint_name) + +def _get_types_outdir_name(ctx): + base_out_folder = _join(ctx.bin_dir.path, ctx.label.package) + type_dep_path = ctx.files.deps[0].path + type_dep_path_without_base_out = type_dep_path.replace(base_out_folder + "/", "", 1) + types_outdir_name = type_dep_path_without_base_out.split("/")[0] + return types_outdir_name + +def _join(*elements): + segments = [f for f in elements if f] + if len(segments): + return "/".join(segments) + return "." + +def _tsconfig_inputs(ctx): + """Returns all transitively referenced tsconfig files from "tsconfig" """ + all_inputs = [] + if TsConfigInfo in ctx.attr.tsconfig: + all_inputs.extend(ctx.attr.tsconfig[TsConfigInfo].deps) + else: + all_inputs.append(ctx.file.tsconfig) + return all_inputs + +def _pkg_npm_types_impl(ctx): + # input declarations + deps_inputs = _deps_inputs(ctx) + tsconfig_inputs = _tsconfig_inputs(ctx) + inputs = ctx.files.srcs[:] + inputs.extend(tsconfig_inputs) + inputs.extend(deps_inputs) + inputs.append(ctx.file._generated_package_json_template) + + # output dir declaration + package_path = ctx.label.package + package_dir = ctx.actions.declare_directory(ctx.label.name) + outputs = [package_dir] + + # gathering template args + template_args = [ + "NAME", ctx.attr.package_name + ] + + # layout api extractor arguments + extractor_args = ctx.actions.args() + + ## general args layout + ### [0] = base output dir + ### [1] = generated package json template input file path + ### [2] = stringified template args + ### [3] = tsconfig input file path + ### [4] = entry point from provided types to summarise + extractor_args.add(package_dir.path) + extractor_args.add(ctx.file._generated_package_json_template.path) + extractor_args.add_joined(template_args, join_with = ",", omit_if_empty = False) + extractor_args.add(tsconfig_inputs[0]) + extractor_args.add(_calculate_entrypoint_path(ctx)) + + run_node( + ctx, + inputs = inputs, + arguments = [extractor_args], + outputs = outputs, + mnemonic = "AssembleNpmTypesPackage", + progress_message = "Assembling npm types package %s" % package_dir.short_path, + executable = "_packager", + ) + + # this is a tree artifact, so correctly build the return + package_dir_depset = depset([package_dir]) + + return [ + DefaultInfo( + files = package_dir_depset, + runfiles = ctx.runfiles([package_dir]), + ), + declaration_info( + declarations = depset([package_dir]) + ), + LinkablePackageInfo( + package_name = ctx.attr.package_name, + package_path = "", + path = package_dir.path, + files = package_dir_depset, + ) + ] + +pkg_npm_types = rule( + implementation = _pkg_npm_types_impl, + attrs = { + "deps": attr.label_list( + doc = """Other targets which are the base types to summarise from""", + allow_files = True, + aspects = [module_mappings_aspect], + ), + "entrypoint_name": attr.string( + doc = """Entrypoint name of the types files group to summarise""", + default = "index.d.ts", + ), + "package_name": attr.string(), + "srcs": attr.label_list( + doc = """Files inside this directory which are inputs for the types to summarise.""", + allow_files = True, + ), + "tsconfig": attr.label(mandatory = True, allow_single_file = [".json"]), + "_packager": attr.label( + doc = "Target that executes the npm types package assembler binary", + executable = True, + cfg = "host", + default = Label("//src/dev/bazel/pkg_npm_types:_packager"), + ), + "_generated_package_json_template": attr.label( + allow_single_file = True, + default = "package_json.mustache", + ), + }, +) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index a9a54bf6794b2..8ce8e2cb40700 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -199,6 +199,7 @@ kibana_vars=( xpack.alerting.invalidateApiKeysTask.interval xpack.alerting.invalidateApiKeysTask.removalDelay xpack.alerting.defaultRuleTaskTimeout + xpack.alerting.cancelAlertsOnRuleTimeout xpack.alerts.healthCheck.interval xpack.alerts.invalidateApiKeysTask.interval xpack.alerts.invalidateApiKeysTask.removalDelay diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 700c011d3e7e7..f5c1755b228e9 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -57,7 +57,6 @@ export const IGNORE_FILE_GLOBS = [ // TODO fix file names in APM to remove these 'x-pack/plugins/apm/public/**/*', - 'x-pack/plugins/apm/scripts/**/*', 'x-pack/plugins/maps/server/fonts/**/*', diff --git a/src/plugins/discover/public/application/discover_router.tsx b/src/plugins/discover/public/application/discover_router.tsx index 6f88c28b52bf9..66ad0cccd03c7 100644 --- a/src/plugins/discover/public/application/discover_router.tsx +++ b/src/plugins/discover/public/application/discover_router.tsx @@ -9,6 +9,7 @@ import { Redirect, Route, Router, Switch } from 'react-router-dom'; import React from 'react'; import { History } from 'history'; +import { EuiErrorBoundary } from '@elastic/eui'; import { KibanaContextProvider } from '../../../kibana_react/public'; import { ContextAppRoute } from './context'; import { SingleDocRoute } from './doc'; @@ -25,29 +26,31 @@ export const discoverRouter = (services: DiscoverServices, history: History) => return ( - - - } - /> - ( - - )} - /> - } - /> - } /> - } /> - - - + + + + } + /> + ( + + )} + /> + } + /> + } /> + } /> + + + + ); }; diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index b42ea3f3fd149..abb6b01e77feb 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -38,6 +38,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams { * An observable which can be used to re-run the expression without destroying the component */ reload$?: Observable; + onRender$?: (item: number) => void; debounce?: number; } @@ -66,6 +67,7 @@ export default function ReactExpressionRenderer({ expression, onEvent, onData$, + onRender$, reload$, debounce, ...expressionLoaderOptions @@ -155,6 +157,7 @@ export default function ReactExpressionRenderer({ ...defaultState, isEmpty: false, })); + onRender$?.(item); }) ); diff --git a/src/plugins/home/common/instruction_variant.ts b/src/plugins/home/common/instruction_variant.ts index 66c841cdc8b56..f19aa7849586b 100644 --- a/src/plugins/home/common/instruction_variant.ts +++ b/src/plugins/home/common/instruction_variant.ts @@ -32,8 +32,8 @@ export const INSTRUCTION_VARIANT = { const DISPLAY_MAP = { [INSTRUCTION_VARIANT.ESC]: 'Elastic Cloud', [INSTRUCTION_VARIANT.OSX]: 'macOS', - [INSTRUCTION_VARIANT.DEB]: 'DEB', - [INSTRUCTION_VARIANT.RPM]: 'RPM', + [INSTRUCTION_VARIANT.DEB]: 'Linux DEB', + [INSTRUCTION_VARIANT.RPM]: 'Linux RPM', [INSTRUCTION_VARIANT.DOCKER]: 'Docker', [INSTRUCTION_VARIANT.WINDOWS]: 'Windows', [INSTRUCTION_VARIANT.NODE]: 'Node.js', diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index e6c9b7bc5a061..75fb3a6114c6c 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -54,6 +54,15 @@ const confirmModalOptionsDelete = { }), }; +const securityDataView = i18n.translate( + 'indexPatternManagement.editIndexPattern.badge.securityDataViewTitle', + { + defaultMessage: 'Security Data View', + } +); + +const securitySolution = 'security-solution'; + export const EditIndexPattern = withRouter( ({ indexPattern, history, location }: EditIndexPatternProps) => { const { application, uiSettings, overlays, chrome, data } = @@ -145,12 +154,17 @@ export const EditIndexPattern = withRouter( defaultIndex={defaultIndex} > {showTagsSection && ( - + {Boolean(indexPattern.timeFieldName) && ( {timeFilterHeader} )} + {indexPattern.id && indexPattern.id.indexOf(securitySolution) === 0 && ( + + {securityDataView} + + )} {tags.map((tag: any) => ( {tag.name} diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index 89230ae03a923..e8ce4b468f22f 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -57,6 +57,8 @@ const securityDataView = i18n.translate( } ); +const securitySolution = 'security-solution'; + interface Props extends RouteComponentProps { canSave: boolean; showCreateDialog?: boolean; @@ -123,10 +125,9 @@ export const IndexPatternTable = ({   - {index.id && index.id === 'security-solution' && ( - {securityDataView} + {index.id && index.id.indexOf(securitySolution) === 0 && ( + {securityDataView} )} - {index.tags && index.tags.map(({ key: tagKey, name: tagName }) => ( {tagName} diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts index 4661441a15a6b..c6e7604723a7a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts @@ -32,7 +32,7 @@ export const MONITOR_EVENT_LOOP_DELAYS_RESET = 24 * 60 * 60 * 1000; export const MONITOR_EVENT_LOOP_DELAYS_START = 1 * 60 * 1000; /** - * Mean event loop delay threshold for logging a warning. + * Mean event loop delay threshold in ms for logging a warning. */ export const MONITOR_EVENT_LOOP_WARN_THRESHOLD = 350; @@ -45,8 +45,3 @@ export const MONITOR_EVENT_LOOP_THRESHOLD_START = 1 * 60 * 1000; * Check the event loop utilization every 30 seconds */ export const MONITOR_EVENT_LOOP_THRESHOLD_INTERVAL = 30 * 1000; - -/** - * Nanosecond to milisecond conversion unit - */ -export const ONE_MILLISECOND_AS_NANOSECONDS = 1000000; diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts index 757e96e5602f0..7ac0bd408c49b 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts @@ -59,19 +59,19 @@ export const eventLoopDelaysUsageSchema: MakeSchemaFrom eventLoopDelaysMonitor.stop()) ) .subscribe(async () => { - const { mean } = eventLoopDelaysMonitor.collect(); - const meanDurationMs = moment - .duration(mean / ONE_MILLISECOND_AS_NANOSECONDS) - .asMilliseconds(); + const { mean: meanMS } = eventLoopDelaysMonitor.collect(); - if (meanDurationMs > warnThreshold) { + if (meanMS > warnThreshold) { logger.warn( - `Average event loop delay threshold exceeded ${warnThreshold}ms. Received ${meanDurationMs}ms. ` + + `Average event loop delay threshold exceeded ${warnThreshold}ms. Received ${meanMS}ms. ` + `See https://ela.st/kibana-scaling-considerations for more information about scaling Kibana.` ); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx index 75f36e20eb084..ed79135f1f09f 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -74,6 +74,8 @@ interface TableState { activeAction?: SavedObjectsManagementAction; } +const MAX_PAGINATED_ITEM = 10000; + export class Table extends PureComponent { state: TableState = { isSearchTextValid: true, @@ -150,10 +152,12 @@ export class Table extends PureComponent { allowedTypes, } = this.props; + const cappedTotalItemCount = Math.min(totalItemCount, MAX_PAGINATED_ITEM); + const pagination = { pageIndex, pageSize, - totalItemCount, + totalItemCount: cappedTotalItemCount, pageSizeOptions: [5, 10, 20, 50], }; @@ -321,6 +325,7 @@ export class Table extends PureComponent { ); const activeActionContents = this.state.activeAction?.render() ?? null; + const exceededResultCount = totalItemCount > MAX_PAGINATED_ITEM; return ( @@ -392,6 +397,18 @@ export class Table extends PureComponent { /> {queryParseError} + {exceededResultCount && ( + <> + + + + + + )}
{ savedObjectsClient = savedObjectsClientMock.create(); }); - it('calls the saved object client with the correct parameters', async () => { + it('calls `client.createPointInTimeFinder` with the correct parameters', async () => { + const query: SavedObjectsFindOptions = { + type: ['some-type', 'another-type'], + }; + + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [], + total: 1, + per_page: 20, + page: 1, + }); + + await findAll(savedObjectsClient, query); + + expect(savedObjectsClient.createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.createPointInTimeFinder).toHaveBeenCalledWith(query); + }); + + it('returns the results from the PIT search', async () => { const query: SavedObjectsFindOptions = { type: ['some-type', 'another-type'], }; @@ -41,45 +59,40 @@ describe('findAll', () => { const results = await findAll(savedObjectsClient, query); expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.find).toHaveBeenCalledWith({ - ...query, - page: 1, - }); + expect(savedObjectsClient.find).toHaveBeenCalledWith( + expect.objectContaining({ + ...query, + }) + ); expect(results).toEqual([createObj(1), createObj(2)]); }); - it('recursively call find until all objects are fetched', async () => { + it('works when the PIT search returns multiple batches', async () => { const query: SavedObjectsFindOptions = { type: ['some-type', 'another-type'], + perPage: 2, }; const objPerPage = 2; - savedObjectsClient.find.mockImplementation(({ page }) => { - const firstInPage = (page! - 1) * objPerPage + 1; + let callCount = 0; + savedObjectsClient.find.mockImplementation(({}) => { + callCount++; + const firstInPage = (callCount - 1) * objPerPage + 1; return Promise.resolve({ - saved_objects: [createObj(firstInPage), createObj(firstInPage + 1)], + saved_objects: + callCount > 3 + ? [createObj(firstInPage)] + : [createObj(firstInPage), createObj(firstInPage + 1)], total: objPerPage * 3, per_page: objPerPage, - page: page!, + page: callCount!, }); }); const results = await findAll(savedObjectsClient, query); - expect(savedObjectsClient.find).toHaveBeenCalledTimes(3); - expect(savedObjectsClient.find).toHaveBeenCalledWith({ - ...query, - page: 1, - }); - expect(savedObjectsClient.find).toHaveBeenCalledWith({ - ...query, - page: 2, - }); - expect(savedObjectsClient.find).toHaveBeenCalledWith({ - ...query, - page: 3, - }); - expect(results).toEqual(times(6, (num) => createObj(num + 1))); + expect(savedObjectsClient.find).toHaveBeenCalledTimes(4); + expect(results).toEqual(times(7, (num) => createObj(num + 1))); }); }); diff --git a/src/plugins/saved_objects_management/server/lib/find_all.ts b/src/plugins/saved_objects_management/server/lib/find_all.ts index 08681758752be..8d908234c6961 100644 --- a/src/plugins/saved_objects_management/server/lib/find_all.ts +++ b/src/plugins/saved_objects_management/server/lib/find_all.ts @@ -6,30 +6,20 @@ * Side Public License, v 1. */ -import { SavedObjectsClientContract, SavedObject, SavedObjectsFindOptions } from 'src/core/server'; +import { + SavedObjectsClientContract, + SavedObject, + SavedObjectsCreatePointInTimeFinderOptions, +} from 'src/core/server'; export const findAll = async ( client: SavedObjectsClientContract, - findOptions: SavedObjectsFindOptions + findOptions: SavedObjectsCreatePointInTimeFinderOptions ): Promise => { - return recursiveFind(client, findOptions, 1, []); -}; - -const recursiveFind = async ( - client: SavedObjectsClientContract, - findOptions: SavedObjectsFindOptions, - page: number, - allObjects: SavedObject[] -): Promise => { - const objects = await client.find({ - ...findOptions, - page, - }); - - allObjects.push(...objects.saved_objects); - if (allObjects.length < objects.total) { - return recursiveFind(client, findOptions, page + 1, allObjects); + const finder = client.createPointInTimeFinder(findOptions); + const results: SavedObject[] = []; + for await (const result of finder.find()) { + results.push(...result.saved_objects); } - - return allObjects; + return results; }; diff --git a/src/plugins/saved_objects_management/server/routes/index.test.ts b/src/plugins/saved_objects_management/server/routes/index.test.ts index 3ec6afe1c0bbc..9c3d235a83e36 100644 --- a/src/plugins/saved_objects_management/server/routes/index.test.ts +++ b/src/plugins/saved_objects_management/server/routes/index.test.ts @@ -24,7 +24,7 @@ describe('registerRoutes', () => { expect(httpSetup.createRouter).toHaveBeenCalledTimes(1); expect(router.get).toHaveBeenCalledTimes(3); - expect(router.post).toHaveBeenCalledTimes(3); + expect(router.post).toHaveBeenCalledTimes(2); expect(router.get).toHaveBeenCalledWith( expect.objectContaining({ @@ -56,11 +56,5 @@ describe('registerRoutes', () => { }), expect.any(Function) ); - expect(router.post).toHaveBeenCalledWith( - expect.objectContaining({ - path: '/api/kibana/management/saved_objects/scroll/export', - }), - expect.any(Function) - ); }); }); diff --git a/src/plugins/saved_objects_management/server/routes/index.ts b/src/plugins/saved_objects_management/server/routes/index.ts index b5b461575604d..5370088d27977 100644 --- a/src/plugins/saved_objects_management/server/routes/index.ts +++ b/src/plugins/saved_objects_management/server/routes/index.ts @@ -11,7 +11,6 @@ import { ISavedObjectsManagement } from '../services'; import { registerFindRoute } from './find'; import { registerBulkGetRoute } from './bulk_get'; import { registerScrollForCountRoute } from './scroll_count'; -import { registerScrollForExportRoute } from './scroll_export'; import { registerRelationshipsRoute } from './relationships'; import { registerGetAllowedTypesRoute } from './get_allowed_types'; @@ -25,7 +24,6 @@ export function registerRoutes({ http, managementServicePromise }: RegisterRoute registerFindRoute(router, managementServicePromise); registerBulkGetRoute(router, managementServicePromise); registerScrollForCountRoute(router); - registerScrollForExportRoute(router); registerRelationshipsRoute(router, managementServicePromise); registerGetAllowedTypesRoute(router); } diff --git a/src/plugins/saved_objects_management/server/routes/scroll_count.ts b/src/plugins/saved_objects_management/server/routes/scroll_count.ts index 89a895adf6008..9a021be4c0cde 100644 --- a/src/plugins/saved_objects_management/server/routes/scroll_count.ts +++ b/src/plugins/saved_objects_management/server/routes/scroll_count.ts @@ -7,7 +7,7 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter, SavedObjectsFindOptions } from 'src/core/server'; +import { IRouter, SavedObjectsCreatePointInTimeFinderOptions } from 'src/core/server'; import { chain } from 'lodash'; import { findAll } from '../lib'; @@ -42,7 +42,7 @@ export const registerScrollForCountRoute = (router: IRouter) => { .value(); const client = getClient({ includedHiddenTypes }); - const findOptions: SavedObjectsFindOptions = { + const findOptions: SavedObjectsCreatePointInTimeFinderOptions = { type: typesToInclude, perPage: 1000, }; diff --git a/src/plugins/saved_objects_management/server/routes/scroll_export.ts b/src/plugins/saved_objects_management/server/routes/scroll_export.ts deleted file mode 100644 index 8d11437af661b..0000000000000 --- a/src/plugins/saved_objects_management/server/routes/scroll_export.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { schema } from '@kbn/config-schema'; -import { IRouter } from 'src/core/server'; -import { chain } from 'lodash'; -import { findAll } from '../lib'; - -export const registerScrollForExportRoute = (router: IRouter) => { - router.post( - { - path: '/api/kibana/management/saved_objects/scroll/export', - validate: { - body: schema.object({ - typesToInclude: schema.arrayOf(schema.string()), - }), - }, - }, - router.handleLegacyErrors(async (context, req, res) => { - const { typesToInclude } = req.body; - const { getClient, typeRegistry } = context.core.savedObjects; - const includedHiddenTypes = chain(typesToInclude) - .uniq() - .filter( - (type) => typeRegistry.isHidden(type) && typeRegistry.isImportableAndExportable(type) - ) - .value(); - - const client = getClient({ includedHiddenTypes }); - - const objects = await findAll(client, { - perPage: 1000, - type: typesToInclude, - }); - - return res.ok({ - body: objects.map((hit) => { - return { - _id: hit.id, - _source: hit.attributes, - _meta: { - savedObjectVersion: 2, - }, - _migrationVersion: hit.migrationVersion, - _references: hit.references || [], - }; - }), - }); - }) - ); -}; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 60c5bbd4346ec..d40bf9aa5d610 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -7075,19 +7075,19 @@ "min": { "type": "long", "_meta": { - "description": "The minimum recorded event loop delay." + "description": "The minimum recorded event loop delay in ms." } }, "max": { "type": "long", "_meta": { - "description": "The maximum recorded event loop delay." + "description": "The maximum recorded event loop delay in ms." } }, "mean": { "type": "long", "_meta": { - "description": "The mean of the recorded event loop delays." + "description": "The mean of the recorded event loop delays in ms." } }, "exceeds": { @@ -7099,7 +7099,7 @@ "stddev": { "type": "long", "_meta": { - "description": "The standard deviation of the recorded event loop delays." + "description": "The standard deviation of the recorded event loop delays in ms." } }, "percentiles": { @@ -7107,25 +7107,25 @@ "50": { "type": "long", "_meta": { - "description": "The 50th accumulated percentile distribution" + "description": "The 50th accumulated percentile distribution in ms" } }, "75": { "type": "long", "_meta": { - "description": "The 75th accumulated percentile distribution" + "description": "The 75th accumulated percentile distribution in ms" } }, "95": { "type": "long", "_meta": { - "description": "The 95th accumulated percentile distribution" + "description": "The 95th accumulated percentile distribution in ms" } }, "99": { "type": "long", "_meta": { - "description": "The 99th accumulated percentile distribution" + "description": "The 99th accumulated percentile distribution in ms" } } } @@ -9301,4 +9301,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap index bc66c1940ac72..0f3b0e8ef5576 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap @@ -61,6 +61,7 @@ exports[`CategoryAxisPanel component should init with the default set of props 1 "truncate": 0, } } + disableSingleLayerAxisControls={false} setAxisLabel={[Function]} /> diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap index 05e2532073eaf..620e361946443 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap @@ -154,6 +154,7 @@ exports[`MetricsAxisOptions component should init with the default set of props } onPositionChanged={[Function]} setCategoryAxis={[Function]} + useMultiLayerAxis={false} /> `; diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap index 175ec043cdbea..d6e95dfdb87b9 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap @@ -27,7 +27,6 @@ exports[`LabelOptions component should init with the default set of props 1`] = /> - + + + - + + + diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/category_axis_panel.test.tsx b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/category_axis_panel.test.tsx index 066f053d4e186..9d18200ae7eb9 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/category_axis_panel.test.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/category_axis_panel.test.tsx @@ -11,6 +11,7 @@ import { shallow } from 'enzyme'; import { CategoryAxisPanel, CategoryAxisPanelProps } from './category_axis_panel'; import { CategoryAxis } from '../../../../types'; import { LabelOptions } from './label_options'; +import { TruncateLabelsOption } from '../../common'; import { categoryAxis } from './mocks'; import { Position } from '@elastic/charts'; @@ -29,6 +30,7 @@ describe('CategoryAxisPanel component', () => { axis, onPositionChanged, setCategoryAxis, + useMultiLayerAxis: false, }; }); @@ -55,4 +57,16 @@ describe('CategoryAxisPanel component', () => { expect(setCategoryAxis).toHaveBeenLastCalledWith({ ...axis, position: value }); expect(onPositionChanged).toBeCalledWith(value); }); + + it('should disable label options with multilayer axis', () => { + const comp = shallow(); + const labelOptions = comp.find(LabelOptions).dive(); + const rotateLabelsOption = labelOptions.find({ paramName: 'rotate' }); + const filterLabelOption = labelOptions.find({ paramName: 'filter' }); + const truncateLabelOption = labelOptions.find(TruncateLabelsOption); + + expect(rotateLabelsOption.prop('disabled')).toBe(true); + expect(filterLabelOption.prop('disabled')).toBe(true); + expect(truncateLabelOption.prop('disabled')).toBe(true); + }); }); diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx index ee5cc950ff66b..c3d1d2dcfee4d 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/category_axis_panel.tsx @@ -25,9 +25,15 @@ export interface CategoryAxisPanelProps { axis: CategoryAxis; onPositionChanged: (position: Position) => void; setCategoryAxis: (value: CategoryAxis) => void; + useMultiLayerAxis: boolean; } -function CategoryAxisPanel({ axis, onPositionChanged, setCategoryAxis }: CategoryAxisPanelProps) { +function CategoryAxisPanel({ + axis, + onPositionChanged, + setCategoryAxis, + useMultiLayerAxis, +}: CategoryAxisPanelProps) { const setAxis = useCallback( (paramName: T, value: CategoryAxis[T]) => { const updatedAxis = { @@ -95,6 +101,7 @@ function CategoryAxisPanel({ axis, onPositionChanged, setCategoryAxis }: Categor axisLabels={axis.labels} axisFilterCheckboxName={`xAxisFilterLabelsCheckbox${axis.id}`} setAxisLabel={setAxisLabel} + disableSingleLayerAxisControls={useMultiLayerAxis} /> )} diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.test.tsx b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.test.tsx index 4996018d21120..6a32b1f75016b 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.test.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.test.tsx @@ -31,6 +31,11 @@ jest.mock('./category_axis_panel', () => ({ jest.mock('./value_axes_panel', () => ({ ValueAxesPanel: () => 'ValueAxesPanel', })); +jest.mock('../../../../services', () => ({ + getUISettings: jest.fn(() => ({ + get: jest.fn((key: string, defaultOverride?: unknown) => defaultOverride), + })), +})); const SERIES_PARAMS = 'seriesParams'; const VALUE_AXES = 'valueAxes'; diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.tsx b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.tsx index dc7e13634d6d6..0397eec7b2c73 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.tsx @@ -10,8 +10,10 @@ import React, { useState, useEffect, useCallback } from 'react'; import { cloneDeep, get } from 'lodash'; import { EuiSpacer } from '@elastic/eui'; +import { Position } from '@elastic/charts'; -import { IAggConfig } from '../../../../../../../data/public'; +import { BUCKET_TYPES, IAggConfig } from '../../../../../../../data/public'; +import { getUISettings } from '../../../../services'; import { VisParams, ValueAxis, SeriesParam, CategoryAxis } from '../../../../types'; import { ValidationVisOptionsProps } from '../../common'; @@ -27,6 +29,7 @@ import { mapPositionOpposingOpposite, } from './utils'; import { getSeriesParams } from '../../../../utils/get_series_params'; +import { LEGACY_TIME_AXIS } from '../../../../../../../charts/common'; export type SetParamByIndex =

( axesName: 'valueAxes' | 'seriesParams', @@ -287,6 +290,20 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) { updateAxisTitle(updatedSeries); }, [firstValueAxesId, setValue, stateParams.seriesParams, updateAxisTitle, aggs, schemaName]); + const isTimeViz = aggs.aggs.some( + (agg) => + agg.schema === 'segment' && agg.enabled && agg.type?.name === BUCKET_TYPES.DATE_HISTOGRAM + ); + const xAxisIsHorizontal = + stateParams.categoryAxes[0].position === Position.Bottom || + stateParams.categoryAxes[0].position === Position.Top; + const useLegacyTimeAxis = getUISettings().get(LEGACY_TIME_AXIS, false); + const linearOrStackedBars = stateParams.seriesParams.every( + ({ mode, type }) => type !== 'histogram' || (type === 'histogram' && mode === 'stacked') + ); + const useMultiLayerAxis = + xAxisIsHorizontal && isTimeViz && !useLegacyTimeAxis && linearOrStackedBars; + return isTabSelected ? ( <> ) { axis={stateParams.categoryAxes[0]} onPositionChanged={onCategoryAxisPositionChanged} setCategoryAxis={setCategoryAxis} + useMultiLayerAxis={useMultiLayerAxis} /> ) : null; diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/label_options.tsx b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/label_options.tsx index ef48d8b6d7880..ed7ac178d3d8b 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/label_options.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/label_options.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useMemo } from 'react'; -import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -23,9 +23,15 @@ export interface LabelOptionsProps { axisLabels: Labels; axisFilterCheckboxName: string; setAxisLabel: SetAxisLabel; + disableSingleLayerAxisControls?: boolean; } -function LabelOptions({ axisLabels, axisFilterCheckboxName, setAxisLabel }: LabelOptionsProps) { +function LabelOptions({ + axisLabels, + axisFilterCheckboxName, + setAxisLabel, + disableSingleLayerAxisControls, +}: LabelOptionsProps) { const setAxisLabelRotate = useCallback( (paramName: 'rotate', value: Labels['rotate']) => { setAxisLabel(paramName, Number(value)); @@ -34,6 +40,15 @@ function LabelOptions({ axisLabels, axisFilterCheckboxName, setAxisLabel }: Labe ); const rotateOptions = useMemo(getRotateOptions, []); + const multilayerAxisTooltipText = disableSingleLayerAxisControls + ? i18n.translate( + 'visTypeXy.controls.pointSeries.categoryAxis.axisLabelsOptionsMultilayer.disabled', + { + defaultMessage: 'This option can be configured only with non-time-based axes', + } + ) + : undefined; + const axisLabelControlDisabled = !axisLabels.show || disableSingleLayerAxisControls; return ( <> @@ -56,39 +71,43 @@ function LabelOptions({ axisLabels, axisFilterCheckboxName, setAxisLabel }: Labe value={axisLabels.show} setValue={setAxisLabel} /> - - + + + - + + + diff --git a/test/accessibility/apps/dashboard.ts b/test/accessibility/apps/dashboard.ts index 54eb5e7df4178..847f7b9eff3e9 100644 --- a/test/accessibility/apps/dashboard.ts +++ b/test/accessibility/apps/dashboard.ts @@ -45,6 +45,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('add a visualization', async () => { + await testSubjects.setValue('savedObjectFinderSearchInput', '[Flights]'); await testSubjects.click('savedObjectTitle[Flights]-Delay-Buckets'); await a11y.testAppSnapshot(); }); @@ -85,6 +86,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('Add one more saved object to cancel it', async () => { + await testSubjects.setValue('savedObjectFinderSearchInput', '[Flights]'); await testSubjects.click('savedObjectTitle[Flights]-Destination-Weather'); await a11y.testAppSnapshot(); }); diff --git a/test/api_integration/apis/saved_objects_management/scroll_count.ts b/test/api_integration/apis/saved_objects_management/scroll_count.ts index 088b26d8205da..ffb275e8656f0 100644 --- a/test/api_integration/apis/saved_objects_management/scroll_count.ts +++ b/test/api_integration/apis/saved_objects_management/scroll_count.ts @@ -18,77 +18,132 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); describe('scroll_count', () => { - before(async () => { - await esArchiver.load( - 'test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count' - ); - }); - after(async () => { - await esArchiver.unload( - 'test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count' - ); - }); + describe('with less than 10k objects', () => { + before(async () => { + await esArchiver.load( + 'test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count' + ); + }); + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count' + ); + }); + + it('returns the count for each included types', async () => { + const res = await supertest + .post(apiUrl) + .send({ + typesToInclude: defaultTypes, + }) + .expect(200); - it('returns the count for each included types', async () => { - const res = await supertest - .post(apiUrl) - .send({ - typesToInclude: defaultTypes, - }) - .expect(200); - - expect(res.body).to.eql({ - dashboard: 2, - 'index-pattern': 1, - search: 1, - visualization: 2, + expect(res.body).to.eql({ + dashboard: 2, + 'index-pattern': 1, + search: 1, + visualization: 2, + }); }); - }); - it('only returns count for types to include', async () => { - const res = await supertest - .post(apiUrl) - .send({ - typesToInclude: ['dashboard', 'search'], - }) - .expect(200); - - expect(res.body).to.eql({ - dashboard: 2, - search: 1, + it('only returns count for types to include', async () => { + const res = await supertest + .post(apiUrl) + .send({ + typesToInclude: ['dashboard', 'search'], + }) + .expect(200); + + expect(res.body).to.eql({ + dashboard: 2, + search: 1, + }); }); - }); - it('filters on title when `searchString` is provided', async () => { - const res = await supertest - .post(apiUrl) - .send({ - typesToInclude: defaultTypes, - searchString: 'Amazing', - }) - .expect(200); - - expect(res.body).to.eql({ - dashboard: 1, - visualization: 1, - 'index-pattern': 0, - search: 0, + it('filters on title when `searchString` is provided', async () => { + const res = await supertest + .post(apiUrl) + .send({ + typesToInclude: defaultTypes, + searchString: 'Amazing', + }) + .expect(200); + + expect(res.body).to.eql({ + dashboard: 1, + visualization: 1, + 'index-pattern': 0, + search: 0, + }); + }); + + it('includes all requested types even when none match the search', async () => { + const res = await supertest + .post(apiUrl) + .send({ + typesToInclude: ['dashboard', 'search', 'visualization'], + searchString: 'nothing-will-match', + }) + .expect(200); + + expect(res.body).to.eql({ + dashboard: 0, + visualization: 0, + search: 0, + }); }); }); - it('includes all requested types even when none match the search', async () => { - const res = await supertest - .post(apiUrl) - .send({ - typesToInclude: ['dashboard', 'search', 'visualization'], - searchString: 'nothing-will-match', - }) - .expect(200); - - expect(res.body).to.eql({ - dashboard: 0, - visualization: 0, - search: 0, + describe('scroll_count with more than 10k objects', () => { + const importVisualizations = async ({ + startIdx = 1, + endIdx, + }: { + startIdx?: number; + endIdx: number; + }) => { + const fileChunks: string[] = []; + for (let i = startIdx; i <= endIdx; i++) { + const id = `test-vis-${i}`; + fileChunks.push( + JSON.stringify({ + type: 'visualization', + id, + attributes: { + title: `My visualization (${i})`, + uiStateJSON: '{}', + visState: '{}', + }, + references: [], + }) + ); + } + + await supertest + .post(`/api/saved_objects/_import`) + .attach('file', Buffer.from(fileChunks.join('\n'), 'utf8'), 'export.ndjson') + .expect(200); + }; + + before(async () => { + await importVisualizations({ startIdx: 1, endIdx: 6000 }); + await importVisualizations({ startIdx: 6001, endIdx: 12000 }); + }); + after(async () => { + await esArchiver.emptyKibanaIndex(); + }); + + it('returns the correct count for each included types', async () => { + const res = await supertest + .post(apiUrl) + .send({ + typesToInclude: ['visualization'], + }) + .expect(200); + + expect(res.body).to.eql({ + visualization: 12000, + }); }); }); }); diff --git a/test/examples/embeddables/adding_children.ts b/test/examples/embeddables/adding_children.ts index ee06622a33f51..dcc7467689a18 100644 --- a/test/examples/embeddables/adding_children.ts +++ b/test/examples/embeddables/adding_children.ts @@ -23,6 +23,8 @@ export default function ({ getService }: PluginFunctionalProviderContext) { await testSubjects.click('embeddablePanelToggleMenuIcon'); await testSubjects.click('embeddablePanelAction-ACTION_ADD_PANEL'); await testSubjects.waitForDeleted('savedObjectFinderLoadingIndicator'); + await testSubjects.click('savedObjectFinderFilterButton'); + await testSubjects.click('savedObjectFinderFilter-todo'); await testSubjects.click('savedObjectTitleGarbage'); await testSubjects.moveMouseTo('euiFlyoutCloseButton'); await flyout.ensureClosed('dashboardAddPanel'); diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.js index cf30b6f4ccf0d..95b0bbb7ed03b 100644 --- a/test/functional/apps/management/_mgmt_import_saved_objects.js +++ b/test/functional/apps/management/_mgmt_import_saved_objects.js @@ -39,6 +39,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.savedObjects.clickConfirmChanges(); await PageObjects.savedObjects.clickImportDone(); await PageObjects.savedObjects.waitTableIsLoaded(); + await PageObjects.savedObjects.searchForObject('mysaved'); //instead of asserting on count- am asserting on the titles- which is more accurate than count. const objects = await PageObjects.savedObjects.getRowTitles(); diff --git a/x-pack/examples/exploratory_view_example/public/app.tsx b/x-pack/examples/exploratory_view_example/public/app.tsx index 9ad37b6fdbfef..4b8ed22723f89 100644 --- a/x-pack/examples/exploratory_view_example/public/app.tsx +++ b/x-pack/examples/exploratory_view_example/public/app.tsx @@ -40,7 +40,7 @@ export const App = (props: { reportDefinitions: { 'monitor.id': ['ALL_VALUES'], }, - breakdown: 'observer.geo.name', + breakdown: 'monitor.type', operationType: 'average', dataType: 'synthetics', seriesType: 'line', diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index 9c4f27fa945be..3646dbddb347d 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -96,6 +96,7 @@ The following table describes the properties of the `options` object. |producer|The id of the application producing this rule type.|string| |minimumLicenseRequired|The value of a minimum license. Most of the rules are licensed as "basic".|string| |ruleTaskTimeout|The length of time a rule can run before being cancelled due to timeout. By default, this value is "5m".|string| +|cancelAlertsOnRuleTimeout|Whether to skip writing alerts and scheduling actions if a rule execution is cancelled due to timeout. By default, this value is set to "true".|boolean| |useSavedObjectReferences.extractReferences|(Optional) When developing a rule type, you can choose to implement hooks for extracting saved object references from rule parameters. This hook will be invoked when a rule is created or updated. Implementing this hook is optional, but if an extract hook is implemented, an inject hook must also be implemented.|Function |useSavedObjectReferences.injectReferences|(Optional) When developing a rule type, you can choose to implement hooks for injecting saved object references into rule parameters. This hook will be invoked when a rule is retrieved (get or find). Implementing this hook is optional, but if an inject hook is implemented, an extract hook must also be implemented.|Function |isExportable|Whether the rule type is exportable from the Saved Objects Management UI.|boolean| diff --git a/x-pack/plugins/alerting/common/alert.ts b/x-pack/plugins/alerting/common/alert.ts index bf0c8e382c9d4..4431f185ac9ca 100644 --- a/x-pack/plugins/alerting/common/alert.ts +++ b/x-pack/plugins/alerting/common/alert.ts @@ -30,6 +30,7 @@ export enum AlertExecutionStatusErrorReasons { Execute = 'execute', Unknown = 'unknown', License = 'license', + Timeout = 'timeout', } export interface AlertExecutionStatus { diff --git a/x-pack/plugins/alerting/server/config.test.ts b/x-pack/plugins/alerting/server/config.test.ts index 63d93b9d67769..a96612beac412 100644 --- a/x-pack/plugins/alerting/server/config.test.ts +++ b/x-pack/plugins/alerting/server/config.test.ts @@ -12,6 +12,7 @@ describe('config validation', () => { const config: Record = {}; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { + "cancelAlertsOnRuleTimeout": true, "defaultRuleTaskTimeout": "5m", "healthCheck": Object { "interval": "60m", diff --git a/x-pack/plugins/alerting/server/config.ts b/x-pack/plugins/alerting/server/config.ts index 277f0c7297df9..8b1b664534379 100644 --- a/x-pack/plugins/alerting/server/config.ts +++ b/x-pack/plugins/alerting/server/config.ts @@ -21,6 +21,7 @@ export const configSchema = schema.object({ defaultValue: DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT, }), defaultRuleTaskTimeout: schema.string({ validate: validateDurationSchema, defaultValue: '5m' }), + cancelAlertsOnRuleTimeout: schema.boolean({ defaultValue: true }), }); export type AlertsConfig = TypeOf; diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index 6419a3ccc5c90..a8da891a3dd14 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -39,6 +39,7 @@ describe('Alerting Plugin', () => { }, maxEphemeralActionsPerAlert: 10, defaultRuleTaskTimeout: '5m', + cancelAlertsOnRuleTimeout: true, }); plugin = new AlertingPlugin(context); @@ -73,6 +74,7 @@ describe('Alerting Plugin', () => { }, maxEphemeralActionsPerAlert: 10, defaultRuleTaskTimeout: '5m', + cancelAlertsOnRuleTimeout: true, }); plugin = new AlertingPlugin(context); @@ -145,7 +147,7 @@ describe('Alerting Plugin', () => { }); }); - it('should apply default config value for ruleTaskTimeout', async () => { + it('should apply default config value for ruleTaskTimeout if no value is specified', async () => { const ruleType = { ...sampleAlertType, minimumLicenseRequired: 'basic', @@ -153,6 +155,35 @@ describe('Alerting Plugin', () => { await setup.registerType(ruleType); expect(ruleType.ruleTaskTimeout).toBe('5m'); }); + + it('should apply value for ruleTaskTimeout if specified', async () => { + const ruleType = { + ...sampleAlertType, + minimumLicenseRequired: 'basic', + ruleTaskTimeout: '20h', + } as AlertType; + await setup.registerType(ruleType); + expect(ruleType.ruleTaskTimeout).toBe('20h'); + }); + + it('should apply default config value for cancelAlertsOnRuleTimeout if no value is specified', async () => { + const ruleType = { + ...sampleAlertType, + minimumLicenseRequired: 'basic', + } as AlertType; + await setup.registerType(ruleType); + expect(ruleType.cancelAlertsOnRuleTimeout).toBe(true); + }); + + it('should apply value for cancelAlertsOnRuleTimeout if specified', async () => { + const ruleType = { + ...sampleAlertType, + minimumLicenseRequired: 'basic', + cancelAlertsOnRuleTimeout: false, + } as AlertType; + await setup.registerType(ruleType); + expect(ruleType.cancelAlertsOnRuleTimeout).toBe(false); + }); }); }); @@ -169,6 +200,7 @@ describe('Alerting Plugin', () => { }, maxEphemeralActionsPerAlert: 10, defaultRuleTaskTimeout: '5m', + cancelAlertsOnRuleTimeout: true, }); const plugin = new AlertingPlugin(context); @@ -210,6 +242,7 @@ describe('Alerting Plugin', () => { }, maxEphemeralActionsPerAlert: 10, defaultRuleTaskTimeout: '5m', + cancelAlertsOnRuleTimeout: true, }); const plugin = new AlertingPlugin(context); @@ -265,6 +298,7 @@ describe('Alerting Plugin', () => { }, maxEphemeralActionsPerAlert: 100, defaultRuleTaskTimeout: '5m', + cancelAlertsOnRuleTimeout: true, }); const plugin = new AlertingPlugin(context); diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index bd3eab19d220d..8be96170e664a 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -75,6 +75,7 @@ export const EVENT_LOG_ACTIONS = { newInstance: 'new-instance', recoveredInstance: 'recovered-instance', activeInstance: 'active-instance', + executeTimeout: 'execute-timeout', }; export const LEGACY_EVENT_LOG_ACTIONS = { resolvedInstance: 'resolved-instance', @@ -285,14 +286,13 @@ export class AlertingPlugin { if (!(alertType.minimumLicenseRequired in LICENSE_TYPE)) { throw new Error(`"${alertType.minimumLicenseRequired}" is not a valid license type`); } - if (!alertType.ruleTaskTimeout) { - alertingConfig.then((config) => { - alertType.ruleTaskTimeout = config.defaultRuleTaskTimeout; - ruleTypeRegistry.register(alertType); - }); - } else { + + alertingConfig.then((config) => { + alertType.ruleTaskTimeout = alertType.ruleTaskTimeout ?? config.defaultRuleTaskTimeout; + alertType.cancelAlertsOnRuleTimeout = + alertType.cancelAlertsOnRuleTimeout ?? config.cancelAlertsOnRuleTimeout; ruleTypeRegistry.register(alertType); - } + }); }, getSecurityHealth: async () => { return await getSecurityHealth( @@ -375,21 +375,24 @@ export class AlertingPlugin { return alertingAuthorizationClientFactory!.create(request); }; - taskRunnerFactory.initialize({ - logger, - getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch), - getRulesClientWithRequest, - spaceIdToNamespace, - actionsPlugin: plugins.actions, - encryptedSavedObjectsClient, - basePathService: core.http.basePath, - eventLogger: this.eventLogger!, - internalSavedObjectsRepository: core.savedObjects.createInternalRepository(['alert']), - executionContext: core.executionContext, - ruleTypeRegistry: this.ruleTypeRegistry!, - kibanaBaseUrl: this.kibanaBaseUrl, - supportsEphemeralTasks: plugins.taskManager.supportsEphemeralTasks(), - maxEphemeralActionsPerAlert: this.config.then((config) => config.maxEphemeralActionsPerAlert), + this.config.then((config) => { + taskRunnerFactory.initialize({ + logger, + getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch), + getRulesClientWithRequest, + spaceIdToNamespace, + actionsPlugin: plugins.actions, + encryptedSavedObjectsClient, + basePathService: core.http.basePath, + eventLogger: this.eventLogger!, + internalSavedObjectsRepository: core.savedObjects.createInternalRepository(['alert']), + executionContext: core.executionContext, + ruleTypeRegistry: this.ruleTypeRegistry!, + kibanaBaseUrl: this.kibanaBaseUrl, + supportsEphemeralTasks: plugins.taskManager.supportsEphemeralTasks(), + maxEphemeralActionsPerAlert: config.maxEphemeralActionsPerAlert, + cancelAlertsOnRuleTimeout: config.cancelAlertsOnRuleTimeout, + }); }); this.eventLogService!.registerSavedObjectProvider('alert', (request) => { diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 244dcb85b13e9..fc5c5cf8897f0 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -99,7 +99,7 @@ const createExecutionHandlerParams: jest.Mocked< stateVal: 'My other {{state.value}} goes here', }, supportsEphemeralTasks: false, - maxEphemeralActionsPerAlert: Promise.resolve(10), + maxEphemeralActionsPerAlert: 10, }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index 652e032a1cbb0..d93d8cd6d1312 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -56,7 +56,7 @@ export interface CreateExecutionHandlerOptions< request: KibanaRequest; alertParams: AlertTypeParams; supportsEphemeralTasks: boolean; - maxEphemeralActionsPerAlert: Promise; + maxEphemeralActionsPerAlert: number; } interface ExecutionHandlerOptions { @@ -157,7 +157,7 @@ export function createExecutionHandler< const alertLabel = `${alertType.id}:${alertId}: '${alertName}'`; const actionsClient = await actionsPlugin.getActionsClientWithRequest(request); - let ephemeralActionsToSchedule = await maxEphemeralActionsPerAlert; + let ephemeralActionsToSchedule = maxEphemeralActionsPerAlert; for (const action of actions) { if ( !actionsPlugin.isActionExecutable(action.id, action.actionTypeId, { notifyUsage: true }) diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 07c4d0371c718..f70cbaa13f7d1 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -107,7 +107,8 @@ describe('Task Runner', () => { ruleTypeRegistry, kibanaBaseUrl: 'https://localhost:5601', supportsEphemeralTasks: false, - maxEphemeralActionsPerAlert: new Promise((resolve) => resolve(10)), + maxEphemeralActionsPerAlert: 10, + cancelAlertsOnRuleTimeout: true, }; function testAgainstEphemeralSupport( @@ -285,7 +286,7 @@ describe('Task Runner', () => { expect(call.services).toBeTruthy(); const logger = taskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(2); + 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, @@ -432,7 +433,7 @@ describe('Task Runner', () => { `); const logger = customTaskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(3); + 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, @@ -648,7 +649,7 @@ describe('Task Runner', () => { expect(actionsClient.ephemeralEnqueuedExecution).toHaveBeenCalledTimes(0); const logger = taskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(4); + expect(logger.debug).toHaveBeenCalledTimes(5); expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( 2, @@ -848,7 +849,7 @@ describe('Task Runner', () => { expect(enqueueFunction).toHaveBeenCalledTimes(1); const logger = customTaskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(4); + expect(logger.debug).toHaveBeenCalledTimes(5); expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( 2, @@ -1537,7 +1538,7 @@ describe('Task Runner', () => { `); const logger = customTaskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(4); + expect(logger.debug).toHaveBeenCalledTimes(5); expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( 2, @@ -4339,7 +4340,7 @@ describe('Task Runner', () => { expect(call.services).toBeTruthy(); const logger = taskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(2); + 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, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 8b93d3fa17211..f651f41ef0c1e 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -82,6 +82,7 @@ export class TaskRunner< private context: TaskRunnerContext; private logger: Logger; private taskInstance: AlertTaskInstance; + private ruleName: string | null; private alertType: NormalizedAlertType< Params, ExtractedParams, @@ -92,6 +93,7 @@ export class TaskRunner< RecoveryActionGroupId >; private readonly ruleTypeRegistry: RuleTypeRegistry; + private cancelled: boolean; constructor( alertType: NormalizedAlertType< @@ -109,8 +111,10 @@ export class TaskRunner< this.context = context; this.logger = context.logger; this.alertType = alertType; + this.ruleName = null; this.taskInstance = taskInstanceToAlertTaskInstance(taskInstance); this.ruleTypeRegistry = context.ruleTypeRegistry; + this.cancelled = false; } async getApiKeyForAlertPermissions(alertId: string, spaceId: string) { @@ -201,6 +205,39 @@ export class TaskRunner< }); } + private async updateRuleExecutionStatus( + alertId: string, + namespace: string | undefined, + executionStatus: AlertExecutionStatus + ) { + const client = this.context.internalSavedObjectsRepository; + const attributes = { + executionStatus: alertExecutionStatusToRaw(executionStatus), + }; + + try { + await partiallyUpdateAlert(client, alertId, attributes, { + ignore404: true, + namespace, + refresh: false, + }); + } catch (err) { + this.logger.error( + `error updating rule execution status for ${this.alertType.id}:${alertId} ${err.message}` + ); + } + } + + private shouldLogAndScheduleActionsForAlerts() { + // if execution hasn't been cancelled, return true + if (!this.cancelled) { + return true; + } + + // if execution has been cancelled, return true if EITHER alerting config or rule type indicate to proceed with scheduling actions + return !this.context.cancelAlertsOnRuleTimeout || !this.alertType.cancelAlertsOnRuleTimeout; + } + async executeAlertInstance( alertInstanceId: string, alertInstance: AlertInstance, @@ -355,19 +392,21 @@ export class TaskRunner< recoveredAlerts: recoveredAlertInstances, }); - generateNewAndRecoveredInstanceEvents({ - eventLogger, - originalAlertInstances, - currentAlertInstances: instancesWithScheduledActions, - recoveredAlertInstances, - alertId, - alertLabel, - namespace, - ruleType: alertType, - rule: alert, - }); + if (this.shouldLogAndScheduleActionsForAlerts()) { + generateNewAndRecoveredInstanceEvents({ + eventLogger, + originalAlertInstances, + currentAlertInstances: instancesWithScheduledActions, + recoveredAlertInstances, + alertId, + alertLabel, + namespace, + ruleType: alertType, + rule: alert, + }); + } - if (!muteAll) { + if (!muteAll && this.shouldLogAndScheduleActionsForAlerts()) { const mutedInstanceIdsSet = new Set(mutedInstanceIds); scheduleActionsForRecoveredInstances({ @@ -422,7 +461,14 @@ export class TaskRunner< ) ); } else { - this.logger.debug(`no scheduling of actions for alert ${alertLabel}: alert is muted.`); + if (muteAll) { + this.logger.debug(`no scheduling of actions for alert ${alertLabel}: alert is muted.`); + } + if (!this.shouldLogAndScheduleActionsForAlerts()) { + this.logger.debug( + `no scheduling of actions for alert ${alertLabel}: alert execution has been cancelled.` + ); + } } return { @@ -487,6 +533,8 @@ export class TaskRunner< throw new ErrorWithReason(AlertExecutionStatusErrorReasons.Read, err); } + this.ruleName = alert.name; + try { this.ruleTypeRegistry.ensureRuleTypeEnabled(alert.alertTypeId); } catch (err) { @@ -596,21 +644,13 @@ export class TaskRunner< eventLogger.logEvent(event); - const client = this.context.internalSavedObjectsRepository; - const attributes = { - executionStatus: alertExecutionStatusToRaw(executionStatus), - }; - - try { - await partiallyUpdateAlert(client, alertId, attributes, { - ignore404: true, - namespace, - refresh: false, - }); - } catch (err) { - this.logger.error( - `error updating alert execution status for ${this.alertType.id}:${alertId} ${err.message}` + if (!this.cancelled) { + this.logger.debug( + `Updating rule task for ${this.alertType.id} rule with id ${alertId} - ${JSON.stringify( + executionStatus + )}` ); + await this.updateRuleExecutionStatus(alertId, namespace, executionStatus); } return { @@ -646,6 +686,72 @@ export class TaskRunner< }), }; } + + async cancel(): Promise { + if (this.cancelled) { + return; + } + + this.cancelled = true; + + // Write event log entry + const { + params: { alertId, spaceId }, + } = this.taskInstance; + const namespace = this.context.spaceIdToNamespace(spaceId); + + this.logger.debug( + `Cancelling rule type ${this.alertType.id} with id ${alertId} - execution exceeded rule type timeout of ${this.alertType.ruleTaskTimeout}` + ); + + const eventLogger = this.context.eventLogger; + const event: IEvent = { + '@timestamp': new Date().toISOString(), + event: { + action: EVENT_LOG_ACTIONS.executeTimeout, + kind: 'alert', + category: [this.alertType.producer], + }, + message: `rule: ${this.alertType.id}:${alertId}: '${ + this.ruleName ?? '' + }' execution cancelled due to timeout - exceeded rule type timeout of ${ + this.alertType.ruleTaskTimeout + }`, + kibana: { + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: 'alert', + id: alertId, + type_id: this.alertType.id, + namespace, + }, + ], + }, + rule: { + id: alertId, + license: this.alertType.minimumLicenseRequired, + category: this.alertType.id, + ruleset: this.alertType.producer, + ...(this.ruleName ? { name: this.ruleName } : {}), + }, + }; + eventLogger.logEvent(event); + + // Update the rule saved object with execution status + const executionStatus: AlertExecutionStatus = { + lastExecutionDate: new Date(), + status: 'error', + error: { + reason: AlertExecutionStatusErrorReasons.Timeout, + message: `${this.alertType.id}:${alertId}: execution cancelled due to timeout - exceeded rule type timeout of ${this.alertType.ruleTaskTimeout}`, + }, + }; + this.logger.debug( + `Updating rule task for ${this.alertType.id} rule with id ${alertId} - execution error due to timeout` + ); + await this.updateRuleExecutionStatus(alertId, namespace, executionStatus); + } } interface TrackAlertDurationsParams< diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts new file mode 100644 index 0000000000000..95cb356af3c1a --- /dev/null +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -0,0 +1,721 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import sinon from 'sinon'; +import { + AlertExecutorOptions, + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, +} from '../types'; +import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager/server'; +import { TaskRunnerContext } from './task_runner_factory'; +import { TaskRunner } from './task_runner'; +import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; +import { + loggingSystemMock, + savedObjectsRepositoryMock, + httpServiceMock, + executionContextServiceMock, +} from '../../../../../src/core/server/mocks'; +import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; +import { actionsMock, actionsClientMock } from '../../../actions/server/mocks'; +import { alertsMock, rulesClientMock } from '../mocks'; +import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; +import { IEventLogger } from '../../../event_log/server'; +import { Alert, RecoveredActionGroup } from '../../common'; +import { UntypedNormalizedAlertType } from '../rule_type_registry'; +import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; + +const ruleType: jest.Mocked = { + id: 'test', + name: 'My test rule', + actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + recoveryActionGroup: RecoveredActionGroup, + executor: jest.fn(), + producer: 'alerts', + cancelAlertsOnRuleTimeout: true, + ruleTaskTimeout: '5m', +}; + +let fakeTimer: sinon.SinonFakeTimers; + +describe('Task Runner Cancel', () => { + let mockedTaskInstance: ConcreteTaskInstance; + + beforeAll(() => { + fakeTimer = sinon.useFakeTimers(); + mockedTaskInstance = { + id: '', + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + schedule: { interval: '10s' }, + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: {}, + taskType: 'alerting:test', + params: { + alertId: '1', + }, + ownerId: null, + }; + }); + + afterAll(() => fakeTimer.restore()); + + const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient(); + const services = alertsMock.createAlertServices(); + const actionsClient = actionsClientMock.create(); + const rulesClient = rulesClientMock.create(); + const ruleTypeRegistry = ruleTypeRegistryMock.create(); + + type TaskRunnerFactoryInitializerParamsType = jest.Mocked & { + actionsPlugin: jest.Mocked; + eventLogger: jest.Mocked; + executionContext: ReturnType; + }; + + const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType = { + getServices: jest.fn().mockReturnValue(services), + actionsPlugin: actionsMock.createStart(), + getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), + encryptedSavedObjectsClient, + logger: loggingSystemMock.create().get(), + executionContext: executionContextServiceMock.createInternalStartContract(), + spaceIdToNamespace: jest.fn().mockReturnValue(undefined), + basePathService: httpServiceMock.createBasePath(), + eventLogger: eventLoggerMock.create(), + internalSavedObjectsRepository: savedObjectsRepositoryMock.create(), + ruleTypeRegistry, + kibanaBaseUrl: 'https://localhost:5601', + supportsEphemeralTasks: false, + maxEphemeralActionsPerAlert: 10, + cancelAlertsOnRuleTimeout: true, + }; + + const mockDate = new Date('2019-02-12T21:01:22.479Z'); + + const mockedRuleSavedObject: Alert = { + id: '1', + consumer: 'bar', + createdAt: mockDate, + updatedAt: mockDate, + throttle: null, + muteAll: false, + notifyWhen: 'onActiveAlert', + enabled: true, + alertTypeId: ruleType.id, + apiKey: '', + apiKeyOwner: 'elastic', + schedule: { interval: '10s' }, + name: 'rule-name', + tags: ['rule-', '-tags'], + createdBy: 'rule-creator', + updatedBy: 'rule-updater', + mutedInstanceIds: [], + params: { + bar: true, + }, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: 'action', + params: { + foo: true, + }, + }, + { + group: RecoveredActionGroup.id, + id: '2', + actionTypeId: 'action', + params: { + isResolved: true, + }, + }, + ], + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + }; + + beforeEach(() => { + jest.resetAllMocks(); + taskRunnerFactoryInitializerParams.getServices.mockReturnValue(services); + taskRunnerFactoryInitializerParams.getRulesClientWithRequest.mockReturnValue(rulesClient); + taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest.mockResolvedValue( + actionsClient + ); + taskRunnerFactoryInitializerParams.actionsPlugin.renderActionParameterTemplates.mockImplementation( + (actionTypeId, actionId, params) => params + ); + ruleTypeRegistry.get.mockReturnValue(ruleType); + taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation((ctx, fn) => + fn() + ); + rulesClient.get.mockResolvedValue(mockedRuleSavedObject); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + }); + + test('updates rule saved object execution status and writes to event log entry when task is cancelled mid-execution', async () => { + const taskRunner = new TaskRunner( + ruleType, + mockedTaskInstance, + taskRunnerFactoryInitializerParams + ); + + const promise = taskRunner.run(); + await Promise.resolve(); + await taskRunner.cancel(); + await promise; + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + // execute-start event, timeout event and then an execute event because rule executors are not cancelling anything yet + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { + action: 'execute-start', + category: ['alerts'], + kind: 'alert', + }, + kibana: { + saved_objects: [ + { + id: '1', + rel: 'primary', + type: 'alert', + type_id: 'test', + }, + ], + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, + }, + message: 'alert execution start: "1"', + rule: { + category: 'test', + id: '1', + license: 'basic', + ruleset: 'alerts', + }, + }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { + action: 'execute-timeout', + category: ['alerts'], + kind: 'alert', + }, + kibana: { + saved_objects: [ + { + id: '1', + rel: 'primary', + type: 'alert', + type_id: 'test', + }, + ], + }, + message: `rule: test:1: '' execution cancelled due to timeout - exceeded rule type timeout of 5m`, + rule: { + category: 'test', + id: '1', + license: 'basic', + ruleset: 'alerts', + }, + }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, { + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { + action: 'execute', + category: ['alerts'], + kind: 'alert', + outcome: 'success', + }, + kibana: { + alerting: { + status: 'ok', + }, + saved_objects: [ + { + id: '1', + rel: 'primary', + type: 'alert', + type_id: 'test', + }, + ], + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, + }, + message: `alert executed: test:1: 'rule-name'`, + rule: { + category: 'test', + id: '1', + license: 'basic', + name: 'rule-name', + ruleset: 'alerts', + }, + }); + + expect( + taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update + ).toHaveBeenCalledTimes(1); + expect( + taskRunnerFactoryInitializerParams.internalSavedObjectsRepository.update + ).toHaveBeenCalledWith( + 'alert', + '1', + { + executionStatus: { + error: { + message: `test:1: execution cancelled due to timeout - exceeded rule type timeout of 5m`, + reason: 'timeout', + }, + lastDuration: 0, + lastExecutionDate: '1970-01-01T00:00:00.000Z', + status: 'error', + }, + }, + { refresh: false, namespace: undefined } + ); + }); + + test('actionsPlugin.execute is called if rule execution is cancelled but cancelAlertsOnRuleTimeout from config is false', async () => { + ruleType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + } + ); + // setting cancelAlertsOnRuleTimeout to false here + const taskRunner = new TaskRunner(ruleType, mockedTaskInstance, { + ...taskRunnerFactoryInitializerParams, + cancelAlertsOnRuleTimeout: false, + }); + + const promise = taskRunner.run(); + await Promise.resolve(); + await taskRunner.cancel(); + await promise; + + testActionsExecute(); + }); + + test('actionsPlugin.execute is called if rule execution is cancelled but cancelAlertsOnRuleTimeout for ruleType is false', async () => { + ruleTypeRegistry.get.mockReturnValue({ + ...ruleType, + cancelAlertsOnRuleTimeout: false, + }); + ruleType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + } + ); + // setting cancelAlertsOnRuleTimeout for ruleType to false here + const taskRunner = new TaskRunner( + { + ...ruleType, + cancelAlertsOnRuleTimeout: false, + }, + mockedTaskInstance, + taskRunnerFactoryInitializerParams + ); + + const promise = taskRunner.run(); + await Promise.resolve(); + await taskRunner.cancel(); + await promise; + + testActionsExecute(); + }); + + test('actionsPlugin.execute is skipped if rule execution is cancelled and cancelAlertsOnRuleTimeout for both config and ruleType are true', async () => { + ruleType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + ruleType, + mockedTaskInstance, + taskRunnerFactoryInitializerParams + ); + + const promise = taskRunner.run(); + await Promise.resolve(); + await taskRunner.cancel(); + await promise; + + const logger = taskRunnerFactoryInitializerParams.logger; + expect(logger.debug).toHaveBeenCalledTimes(6); + expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); + expect(logger.debug).nthCalledWith( + 2, + `Cancelling rule type test with id 1 - execution exceeded rule type timeout of 5m` + ); + expect(logger.debug).nthCalledWith( + 3, + `Updating rule task for test rule with id 1 - execution error due to timeout` + ); + expect(logger.debug).nthCalledWith( + 4, + `alert test:1: 'rule-name' has 1 active alert instances: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` + ); + expect(logger.debug).nthCalledWith( + 5, + `no scheduling of actions for alert test:1: 'rule-name': alert execution has been cancelled.` + ); + expect(logger.debug).nthCalledWith( + 6, + 'alertExecutionStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + ); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { + action: 'execute-start', + category: ['alerts'], + kind: 'alert', + }, + kibana: { + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, + saved_objects: [ + { + id: '1', + rel: 'primary', + type: 'alert', + type_id: 'test', + }, + ], + }, + message: `alert execution start: \"1\"`, + rule: { + category: 'test', + id: '1', + license: 'basic', + ruleset: 'alerts', + }, + }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { + action: 'execute-timeout', + category: ['alerts'], + kind: 'alert', + }, + kibana: { + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + type_id: 'test', + }, + ], + }, + message: `rule: test:1: '' execution cancelled due to timeout - exceeded rule type timeout of 5m`, + rule: { + category: 'test', + id: '1', + license: 'basic', + ruleset: 'alerts', + }, + }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, { + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { + action: 'execute', + category: ['alerts'], + kind: 'alert', + outcome: 'success', + }, + kibana: { + alerting: { + status: 'active', + }, + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + type_id: 'test', + }, + ], + }, + message: "alert executed: test:1: 'rule-name'", + rule: { + category: 'test', + id: '1', + license: 'basic', + name: 'rule-name', + ruleset: 'alerts', + }, + }); + }); + + function testActionsExecute() { + const logger = taskRunnerFactoryInitializerParams.logger; + expect(logger.debug).toHaveBeenCalledTimes(5); + expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); + expect(logger.debug).nthCalledWith( + 2, + `Cancelling rule type test with id 1 - execution exceeded rule type timeout of 5m` + ); + expect(logger.debug).nthCalledWith( + 3, + `Updating rule task for test rule with id 1 - execution error due to timeout` + ); + expect(logger.debug).nthCalledWith( + 4, + `alert test:1: 'rule-name' has 1 active alert instances: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` + ); + expect(logger.debug).nthCalledWith( + 5, + 'alertExecutionStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + ); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(6); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { + action: 'execute-start', + category: ['alerts'], + kind: 'alert', + }, + kibana: { + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + type_id: 'test', + }, + ], + }, + message: `alert execution start: "1"`, + rule: { + category: 'test', + id: '1', + license: 'basic', + ruleset: 'alerts', + }, + }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { + action: 'execute-timeout', + category: ['alerts'], + kind: 'alert', + }, + kibana: { + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + type_id: 'test', + }, + ], + }, + message: `rule: test:1: '' execution cancelled due to timeout - exceeded rule type timeout of 5m`, + rule: { + category: 'test', + id: '1', + license: 'basic', + ruleset: 'alerts', + }, + }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, { + event: { + action: 'new-instance', + category: ['alerts'], + kind: 'alert', + duration: 0, + start: '1970-01-01T00:00:00.000Z', + }, + kibana: { + alerting: { + action_group_id: 'default', + instance_id: '1', + }, + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + type_id: 'test', + }, + ], + }, + message: "test:1: 'rule-name' created new instance: '1'", + rule: { + category: 'test', + id: '1', + license: 'basic', + name: 'rule-name', + namespace: undefined, + ruleset: 'alerts', + }, + }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(4, { + event: { + action: 'active-instance', + category: ['alerts'], + duration: 0, + kind: 'alert', + start: '1970-01-01T00:00:00.000Z', + }, + kibana: { + alerting: { + action_group_id: 'default', + instance_id: '1', + }, + saved_objects: [ + { id: '1', namespace: undefined, rel: 'primary', type: 'alert', type_id: 'test' }, + ], + }, + message: "test:1: 'rule-name' active instance: '1' in actionGroup: 'default'", + rule: { + category: 'test', + id: '1', + license: 'basic', + name: 'rule-name', + ruleset: 'alerts', + }, + }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(5, { + event: { + action: 'execute-action', + category: ['alerts'], + kind: 'alert', + }, + kibana: { + alerting: { + instance_id: '1', + action_group_id: 'default', + }, + saved_objects: [ + { + id: '1', + rel: 'primary', + type: 'alert', + type_id: 'test', + }, + { + id: '1', + type: 'action', + type_id: 'action', + }, + ], + }, + message: + "alert: test:1: 'rule-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", + rule: { + category: 'test', + id: '1', + license: 'basic', + name: 'rule-name', + ruleset: 'alerts', + }, + }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(6, { + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { action: 'execute', category: ['alerts'], kind: 'alert', outcome: 'success' }, + kibana: { + alerting: { + status: 'active', + }, + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + type_id: 'test', + }, + ], + }, + message: "alert executed: test:1: 'rule-name'", + rule: { + category: 'test', + id: '1', + license: 'basic', + name: 'rule-name', + ruleset: 'alerts', + }, + }); + } +}); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index d262607958347..b799dd2f4043d 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -83,7 +83,8 @@ describe('Task Runner Factory', () => { ruleTypeRegistry: ruleTypeRegistryMock.create(), kibanaBaseUrl: 'https://localhost:5601', supportsEphemeralTasks: true, - maxEphemeralActionsPerAlert: new Promise((resolve) => resolve(10)), + maxEphemeralActionsPerAlert: 10, + cancelAlertsOnRuleTimeout: true, executionContext, }; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts index 524b779a0d9ac..fc4b8eee89f5e 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts @@ -44,7 +44,8 @@ export interface TaskRunnerContext { ruleTypeRegistry: RuleTypeRegistry; kibanaBaseUrl: string | undefined; supportsEphemeralTasks: boolean; - maxEphemeralActionsPerAlert: Promise; + maxEphemeralActionsPerAlert: number; + cancelAlertsOnRuleTimeout: boolean; } export class TaskRunnerFactory { diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 82bb94b121840..c1645936c06e9 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -160,6 +160,7 @@ export interface AlertType< defaultScheduleInterval?: string; minimumScheduleInterval?: string; ruleTaskTimeout?: string; + cancelAlertsOnRuleTimeout?: boolean; } export type UntypedAlertType = AlertType< AlertTypeParams, diff --git a/x-pack/plugins/apm/common/utils/environment_query.ts b/x-pack/plugins/apm/common/utils/environment_query.ts index e2f9a722e3de2..bc02e4cd2518b 100644 --- a/x-pack/plugins/apm/common/utils/environment_query.ts +++ b/x-pack/plugins/apm/common/utils/environment_query.ts @@ -6,11 +6,15 @@ */ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { SERVICE_ENVIRONMENT } from '../elasticsearch_fieldnames'; +import { + SERVICE_ENVIRONMENT, + SERVICE_NODE_NAME, +} from '../elasticsearch_fieldnames'; import { ENVIRONMENT_ALL, ENVIRONMENT_NOT_DEFINED, } from '../environment_filter_values'; +import { SERVICE_NODE_NAME_MISSING } from '../service_nodes'; export function environmentQuery( environment: string @@ -25,3 +29,17 @@ export function environmentQuery( return [{ term: { [SERVICE_ENVIRONMENT]: environment } }]; } + +export function serviceNodeNameQuery( + serviceNodeName?: string +): QueryDslQueryContainer[] { + if (!serviceNodeName) { + return []; + } + + if (serviceNodeName === SERVICE_NODE_NAME_MISSING) { + return [{ bool: { must_not: [{ exists: { field: SERVICE_NODE_NAME } }] } }]; + } + + return [{ term: { [SERVICE_NODE_NAME]: serviceNodeName } }]; +} diff --git a/x-pack/plugins/apm/dev_docs/local_setup.md b/x-pack/plugins/apm/dev_docs/local_setup.md index 21d861fbb4e0b..4ed5d8c1b86dd 100644 --- a/x-pack/plugins/apm/dev_docs/local_setup.md +++ b/x-pack/plugins/apm/dev_docs/local_setup.md @@ -40,7 +40,7 @@ elasticsearch.password: changeme APM behaves differently depending on which the role and permissions a logged in user has. To create the users run: ```sh -node x-pack/plugins/apm/scripts/create-apm-users-and-roles.js --username admin --password changeme --kibana-url http://localhost:5601 --role-suffix +node x-pack/plugins/apm/scripts/create_apm_users_and_roles.js --username admin --password changeme --kibana-url http://localhost:5601 --role-suffix ``` This will create: diff --git a/x-pack/plugins/apm/dev_docs/query_debugging_in_development_and_production.md b/x-pack/plugins/apm/dev_docs/query_debugging_in_development_and_production.md index 0dcf20d3e2fed..5811306a05fda 100644 --- a/x-pack/plugins/apm/dev_docs/query_debugging_in_development_and_production.md +++ b/x-pack/plugins/apm/dev_docs/query_debugging_in_development_and_production.md @@ -14,4 +14,147 @@ There will be an `_inspect` key containing every Elasticsearch query made during ![image](https://user-images.githubusercontent.com/209966/140500012-b075adf0-8401-40fd-99f8-85b68711de17.png) +## Example +When "Inspect ES queries" are enabed all API calls to the APM API will be include the query param `_inspect=true`. For the environments API the request / response will be: + +``` +GET /internal/apm/environments?start=&end=&_inspect=true +``` + +```json +{ + "environments": [ + "production", + "testing", + "ENVIRONMENT_NOT_DEFINED" + ], + "_inspect": [ + { + "id": "get_environments (/internal/apm/environments)", + "json": { + "size": 0, + "query": { + "bool": { + "filter": [ + { + "range": { + "@timestamp": { + "gte": 1636918740000, + "lte": 1636919672329, + "format": "epoch_millis" + } + } + }, + { + "terms": { + "processor.event": [ + "transaction", + "metric", + "error" + ] + } + }, + { + "range": { + "observer.version_major": { + "gte": 7 + } + } + } + ] + } + }, + "aggs": { + "environments": { + "terms": { + "field": "service.environment", + "missing": "ENVIRONMENT_NOT_DEFINED", + "size": 100 + } + } + } + }, + "name": "get_environments (/internal/apm/environments)", + "response": { + "json": { + "took": 10, + "timed_out": false, + "_shards": { + "total": 17, + "successful": 17, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 10000, + "relation": "gte" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "environments": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "production", + "doc_count": 27643 + }, + { + "key": "testing", + "doc_count": 960 + }, + { + "key": "ENVIRONMENT_NOT_DEFINED", + "doc_count": 63 + } + ] + } + } + } + }, + "startTime": 1636919683285, + "stats": { + "kibanaApiQueryParameters": { + "label": "Kibana API query parameters", + "description": "The query parameters used in the Kibana API request that initiated the Elasticsearch request.", + "value": "{\n \"start\": \"2021-11-14T19:39:00.000Z\",\n \"end\": \"2021-11-14T19:54:32.329Z\",\n \"_inspect\": \"true\"\n}" + }, + "kibanaApiRoute": { + "label": "Kibana API route", + "description": "The route of the Kibana API request that initiated the Elasticsearch request.", + "value": "GET /internal/apm/environments" + }, + "indexPattern": { + "label": "Index pattern", + "value": [ + "traces-apm*,apm-*", + "metrics-apm*,apm-*", + "logs-apm*,apm-*" + ], + "description": "The index pattern that connected to the Elasticsearch indices." + }, + "hits": { + "label": "Hits", + "value": "0", + "description": "The number of documents returned by the query." + }, + "queryTime": { + "label": "Query time", + "value": "10ms", + "description": "The time it took to process the query. Does not include the time to send the request or parse it in the browser." + }, + "hitsTotal": { + "label": "Hits (total)", + "value": "> 10000", + "description": "The number of documents that match the query." + } + }, + "status": 1 + } + ] +} +``` diff --git a/x-pack/plugins/apm/dev_docs/telemetry.md b/x-pack/plugins/apm/dev_docs/telemetry.md index 5462fbcc3b36c..27b9e57447467 100644 --- a/x-pack/plugins/apm/dev_docs/telemetry.md +++ b/x-pack/plugins/apm/dev_docs/telemetry.md @@ -29,7 +29,7 @@ Once uploaded to the telemetry cluster, the data telemetry is stored in ### Generating sample data -The script in `scripts/upload-telemetry-data` can generate sample telemetry data and upload it to a cluster of your choosing. +The script in `scripts/upload_telemetry_data` can generate sample telemetry data and upload it to a cluster of your choosing. You'll need to set the `GITHUB_TOKEN` environment variable to a token that has `repo` scope so it can read from the [elastic/telemetry](https://github.com/elastic/telemetry) repository. (You probably have a token that works for this in diff --git a/x-pack/plugins/apm/dev_docs/typescript.md b/x-pack/plugins/apm/dev_docs/typescript.md index 6de61b665a1b1..110c4a2d1ec97 100644 --- a/x-pack/plugins/apm/dev_docs/typescript.md +++ b/x-pack/plugins/apm/dev_docs/typescript.md @@ -4,8 +4,8 @@ Kibana and X-Pack are very large TypeScript projects, and it comes at a cost. Ed To run the optimization: -`$ node x-pack/plugins/apm/scripts/optimize-tsconfig` +`$ node x-pack/plugins/apm/scripts/optimize_tsconfig` To undo the optimization: -`$ node x-pack/plugins/apm/scripts/unoptimize-tsconfig` +`$ node x-pack/plugins/apm/scripts/unoptimize_tsconfig` diff --git a/x-pack/plugins/apm/dev_docs/updating_functional_tests_archives.md b/x-pack/plugins/apm/dev_docs/updating_functional_tests_archives.md index 88e434d07d38f..3d1a5f0b00bb4 100644 --- a/x-pack/plugins/apm/dev_docs/updating_functional_tests_archives.md +++ b/x-pack/plugins/apm/dev_docs/updating_functional_tests_archives.md @@ -3,6 +3,6 @@ Some of our API tests use an archive generated by the [`esarchiver`](https://www.elastic.co/guide/en/kibana/current/development-tests.html#development-functional-tests) script. Updating the main archive (`apm_8.0.0`) is a scripted process, where a 30m snapshot is downloaded from a cluster running the [APM Integration Testing server](https://github.com/elastic/apm-integration-testing). The script will copy the generated archives into the `fixtures/es_archiver` folders of our test suites (currently `basic` and `trial`). It will also generate a file that contains metadata about the archive, that can be imported to get the time range of the snapshot. Usage: -`node x-pack/plugins/apm/scripts/create-functional-tests-archive --es-url=https://admin:changeme@localhost:9200 --kibana-url=https://localhost:5601` +`node x-pack/plugins/apm/scripts/create_functional_tests_archive --es-url=https://admin:changeme@localhost:9200 --kibana-url=https://localhost:5601` diff --git a/x-pack/plugins/apm/ftr_e2e/cypress_start.ts b/x-pack/plugins/apm/ftr_e2e/cypress_start.ts index c8ab216cbce5c..9e2b9daa5631f 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress_start.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress_start.ts @@ -11,7 +11,7 @@ import { argv } from 'yargs'; import Url from 'url'; import cypress from 'cypress'; import { FtrProviderContext } from './ftr_provider_context'; -import { createApmUsersAndRoles } from '../scripts/create-apm-users-and-roles/create_apm_users_and_roles'; +import { createApmUsersAndRoles } from '../scripts/create_apm_users_and_roles/create_apm_users_and_roles'; import { esArchiverLoad, esArchiverUnload } from './cypress/tasks/es_archiver'; export async function cypressStart( diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index 3d444c1cf7a3a..20af3db34e508 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -8,17 +8,11 @@ import { i18n } from '@kbn/i18n'; import { lazy } from 'react'; import { stringify } from 'querystring'; -import type { - ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, - ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, - ALERT_SEVERITY as ALERT_SEVERITY_TYPED, -} from '@kbn/rule-data-utils'; import { - ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, - ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, - ALERT_SEVERITY as ALERT_SEVERITY_NON_TYPED, - // @ts-expect-error -} from '@kbn/rule-data-utils/target_node/technical_field_names'; + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_SEVERITY, +} from '@kbn/rule-data-utils/technical_field_names'; import type { ObservabilityRuleTypeRegistry } from '../../../../observability/public'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { @@ -34,12 +28,6 @@ const SERVICE_ENVIRONMENT = 'service.environment'; const SERVICE_NAME = 'service.name'; const TRANSACTION_TYPE = 'transaction.type'; -const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = - ALERT_EVALUATION_THRESHOLD_NON_TYPED; -const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = - ALERT_EVALUATION_VALUE_NON_TYPED; -const ALERT_SEVERITY: typeof ALERT_SEVERITY_TYPED = ALERT_SEVERITY_NON_TYPED; - const format = ({ pathname, query, diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx index 8aa132bb85595..e0e17325fa815 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx @@ -16,9 +16,7 @@ import { } from '@elastic/charts'; import { EuiTitle } from '@elastic/eui'; import React, { Suspense, useState } from 'react'; -import type { ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED } from '@kbn/rule-data-utils'; -// @ts-expect-error -import { ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED } from '@kbn/rule-data-utils/target_node/technical_field_names'; +import { ALERT_RULE_TYPE_ID } from '@kbn/rule-data-utils/technical_field_names'; import { i18n } from '@kbn/i18n'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; @@ -32,9 +30,6 @@ import { LazyAlertsFlyout } from '../../../../../../observability/public'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { getTimeZone } from '../../../shared/charts/helper/timezone'; -const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = - ALERT_RULE_TYPE_ID_NON_TYPED; - type ErrorDistributionAPIResponse = APIReturnType<'GET /internal/apm/services/{serviceName}/errors/distribution'>; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/index.tsx index 4921dfe0606c3..457daef851bcf 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/index.tsx @@ -101,11 +101,11 @@ export function SpanFlyout({ const stackframes = span.span.stacktrace; const codeLanguage = parentTransaction?.service.language?.name; const spanDb = span.span.db; - const httpContext = span.span.http; const spanTypes = getSpanTypes(span); - const spanHttpStatusCode = httpContext?.response?.status_code; - const spanHttpUrl = httpContext?.url?.original; - const spanHttpMethod = httpContext?.method; + const spanHttpStatusCode = + span.http?.response?.status_code || span.span?.http?.response?.status_code; + const spanHttpUrl = span.url?.original || span.span?.http?.url?.original; + const spanHttpMethod = span.http?.request?.method || span.span?.http?.method; return ( diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx index a3ab10ea6514a..9f8240f4ed3eb 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx @@ -16,13 +16,13 @@ import { ALERT_SEVERITY, ALERT_START, ALERT_STATUS, - ALERT_STATUS_ACTIVE, ALERT_UUID, SPACE_IDS, ALERT_RULE_UUID, ALERT_RULE_NAME, ALERT_RULE_CATEGORY, -} from '@kbn/rule-data-utils'; +} from '@kbn/rule-data-utils/technical_field_names'; +import { ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils/alerts_as_data_status'; import { ValuesType } from 'utility-types'; import { EuiTheme } from '../../../../../../../../src/plugins/kibana_react/common'; import { ObservabilityRuleTypeRegistry } from '../../../../../../observability/public'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx index 038269d3cefdc..832d44f9ca887 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx @@ -12,23 +12,14 @@ import { } from '@elastic/charts'; import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { - ALERT_DURATION as ALERT_DURATION_TYPED, - ALERT_SEVERITY as ALERT_SEVERITY_TYPED, - ALERT_START as ALERT_START_TYPED, - ALERT_UUID as ALERT_UUID_TYPED, - ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED, - ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, -} from '@kbn/rule-data-utils'; import { - ALERT_DURATION as ALERT_DURATION_NON_TYPED, - ALERT_SEVERITY as ALERT_SEVERITY_NON_TYPED, - ALERT_START as ALERT_START_NON_TYPED, - ALERT_UUID as ALERT_UUID_NON_TYPED, - ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED, - ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, - // @ts-expect-error -} from '@kbn/rule-data-utils/target_node/technical_field_names'; + ALERT_DURATION, + ALERT_SEVERITY, + ALERT_START, + ALERT_UUID, + ALERT_RULE_TYPE_ID, + ALERT_RULE_NAME, +} from '@kbn/rule-data-utils/technical_field_names'; import React, { Dispatch, SetStateAction } from 'react'; import { EuiTheme } from 'src/plugins/kibana_react/common'; import { ValuesType } from 'utility-types'; @@ -37,14 +28,6 @@ import { parseTechnicalFields } from '../../../../../../rule_registry/common'; import { asDuration, asPercent } from '../../../../../common/utils/formatters'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; -const ALERT_SEVERITY: typeof ALERT_SEVERITY_TYPED = ALERT_SEVERITY_NON_TYPED; -const ALERT_START: typeof ALERT_START_TYPED = ALERT_START_NON_TYPED; -const ALERT_UUID: typeof ALERT_UUID_TYPED = ALERT_UUID_NON_TYPED; -const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = - ALERT_RULE_TYPE_ID_NON_TYPED; -const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; - type Alert = ValuesType< APIReturnType<'GET /internal/apm/services/{serviceName}/alerts'>['alerts'] >; diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx index e19f78006d9fe..2876febc21d1e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx @@ -9,9 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSelect, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import type { ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED } from '@kbn/rule-data-utils'; -// @ts-expect-error -import { ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED } from '@kbn/rule-data-utils/target_node/technical_field_names'; +import { ALERT_RULE_TYPE_ID } from '@kbn/rule-data-utils/technical_field_names'; import { AlertType } from '../../../../../common/alert_types'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; @@ -29,9 +27,6 @@ import { MLHeader } from '../../../shared/charts/transaction_charts/ml_header'; import * as urlHelpers from '../../../shared/Links/url_helpers'; import { getComparisonChartTheme } from '../../time_comparison/get_time_range_comparison'; -const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = - ALERT_RULE_TYPE_ID_NON_TYPED; - interface Props { height?: number; kuery: string; diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx index e1921aca8d9ef..5799a73b5822c 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx @@ -14,7 +14,6 @@ import { ALERT_SEVERITY, ALERT_START, ALERT_STATUS, - ALERT_STATUS_ACTIVE, ALERT_UUID, TIMESTAMP, ALERT_RULE_UUID, @@ -23,7 +22,8 @@ import { ALERT_RULE_CONSUMER, ALERT_RULE_PRODUCER, SPACE_IDS, -} from '@kbn/rule-data-utils'; +} from '@kbn/rule-data-utils/technical_field_names'; +import { ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils/alerts_as_data_status'; import { Meta, Story } from '@storybook/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx index 9f437a95e7dd9..1c2b7fdeb7714 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx @@ -7,6 +7,7 @@ import { EuiTitle } from '@elastic/eui'; import React from 'react'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asDecimal, asInteger, @@ -14,8 +15,6 @@ import { getDurationFormatter, getFixedByteFormatter, } from '../../../../../common/utils/formatters'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform_metrics_chart'; import { Maybe } from '../../../../../typings/common'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { TimeseriesChart } from '../timeseries_chart'; @@ -24,7 +23,11 @@ import { getResponseTimeTickFormatter, } from '../transaction_charts/helper'; -function getYTickFormatter(chart: GenericMetricsChart) { +type MetricChartApiResponse = + APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/charts'>; +type MetricChart = MetricChartApiResponse['charts'][0]; + +function getYTickFormatter(chart: MetricChart) { const max = getMaxY(chart.series); switch (chart.yUnit) { @@ -50,7 +53,7 @@ function getYTickFormatter(chart: GenericMetricsChart) { interface Props { start: Maybe; end: Maybe; - chart: GenericMetricsChart; + chart: MetricChart; fetchStatus: FETCH_STATUS; } diff --git a/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts index ef360698192e1..15037ecf6adb8 100644 --- a/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts @@ -5,14 +5,16 @@ * 2.0. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetricsChartsByAgentAPIResponse } from '../../server/lib/metrics/get_metrics_chart_data_by_agent'; +import type { APIReturnType } from '../services/rest/createCallApmApi'; import { useApmServiceContext } from '../context/apm_service/use_apm_service_context'; import { useFetcher } from './use_fetcher'; import { useTimeRange } from './use_time_range'; import { useApmParams } from './use_apm_params'; -const INITIAL_DATA: MetricsChartsByAgentAPIResponse = { +type MetricChartApiResponse = + APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/charts'>; + +const INITIAL_DATA: MetricChartApiResponse = { charts: [], }; diff --git a/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx index fddb187b7e322..2d224b0696d4d 100644 --- a/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx +++ b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx @@ -4,13 +4,17 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiButton } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiPanel } from '@elastic/eui'; -import { EuiCard } from '@elastic/eui'; -import { EuiImage } from '@elastic/eui'; -import { EuiLoadingSpinner } from '@elastic/eui'; +import { + EuiButton, + EuiFlexItem, + EuiFlexGroup, + EuiPanel, + EuiCard, + EuiImage, + EuiLoadingSpinner, + EuiText, + EuiSpacer, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { HttpStart } from 'kibana/public'; import React, { useEffect, useState } from 'react'; @@ -90,18 +94,32 @@ function TutorialFleetInstructions({ http, basePath, isDarkTheme }: Props) { } )} footer={ - - {i18n.translate( - 'xpack.apm.tutorial.apmServer.fleet.apmIntegration.button', - { - defaultMessage: 'APM integration', - } - )} - + <> + + {i18n.translate( + 'xpack.apm.tutorial.apmServer.fleet.apmIntegration.button', + { + defaultMessage: 'APM integration', + } + )} + + + +

+ {i18n.translate( + 'xpack.apm.tutorial.apmServer.fleet.apmIntegration.description', + { + defaultMessage: + 'Fleet allows you to centrally manage Elastic Agents running the APM integration. The default option is to install a Fleet Server on a dedicated host. For setups without a dedicated host, we recommend following the instructions to install the standalone APM Server for your operating system by selecting the respective tab above.', + } + )} +

+ + } /> diff --git a/x-pack/plugins/apm/scripts/aggregate-latency-metrics.js b/x-pack/plugins/apm/scripts/aggregate_latency_metrics.js similarity index 92% rename from x-pack/plugins/apm/scripts/aggregate-latency-metrics.js rename to x-pack/plugins/apm/scripts/aggregate_latency_metrics.js index 350c3e4256b0b..9cbfe73eeb6dc 100644 --- a/x-pack/plugins/apm/scripts/aggregate-latency-metrics.js +++ b/x-pack/plugins/apm/scripts/aggregate_latency_metrics.js @@ -10,7 +10,7 @@ require('@kbn/optimizer').registerNodeAutoTranspilation(); const { aggregateLatencyMetrics, -} = require('./aggregate-latency-metrics/index.ts'); +} = require('./aggregate_latency_metrics/index.ts'); aggregateLatencyMetrics().catch((err) => { if (err.meta && err.meta.body) { diff --git a/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts b/x-pack/plugins/apm/scripts/aggregate_latency_metrics/index.ts similarity index 99% rename from x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts rename to x-pack/plugins/apm/scripts/aggregate_latency_metrics/index.ts index d5cc9a63dbfcd..f9aa943b5eceb 100644 --- a/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts +++ b/x-pack/plugins/apm/scripts/aggregate_latency_metrics/index.ts @@ -24,7 +24,7 @@ import { TRANSACTION_RESULT, PROCESSOR_EVENT, } from '../../common/elasticsearch_fieldnames'; -import { createOrUpdateIndex } from '../shared/create-or-update-index'; +import { createOrUpdateIndex } from '../shared/create_or_update_index'; import { parseIndexUrl } from '../shared/parse_index_url'; import { ESClient, getEsClient } from '../shared/get_es_client'; diff --git a/x-pack/plugins/apm/scripts/create-apm-users-and-roles.js b/x-pack/plugins/apm/scripts/create_apm_users_and_roles.js similarity index 92% rename from x-pack/plugins/apm/scripts/create-apm-users-and-roles.js rename to x-pack/plugins/apm/scripts/create_apm_users_and_roles.js index d64364cb38928..8d8b0c2d3d813 100644 --- a/x-pack/plugins/apm/scripts/create-apm-users-and-roles.js +++ b/x-pack/plugins/apm/scripts/create_apm_users_and_roles.js @@ -20,4 +20,4 @@ // eslint-disable-next-line import/no-extraneous-dependencies require('@kbn/optimizer').registerNodeAutoTranspilation(); -require('./create-apm-users-and-roles/create_apm_users_and_roles_cli.ts'); +require('./create_apm_users_and_roles/create_apm_users_and_roles_cli.ts'); diff --git a/x-pack/plugins/apm/scripts/create-apm-users-and-roles/create_apm_users_and_roles.ts b/x-pack/plugins/apm/scripts/create_apm_users_and_roles/create_apm_users_and_roles.ts similarity index 100% rename from x-pack/plugins/apm/scripts/create-apm-users-and-roles/create_apm_users_and_roles.ts rename to x-pack/plugins/apm/scripts/create_apm_users_and_roles/create_apm_users_and_roles.ts diff --git a/x-pack/plugins/apm/scripts/create-apm-users-and-roles/create_apm_users_and_roles_cli.ts b/x-pack/plugins/apm/scripts/create_apm_users_and_roles/create_apm_users_and_roles_cli.ts similarity index 100% rename from x-pack/plugins/apm/scripts/create-apm-users-and-roles/create_apm_users_and_roles_cli.ts rename to x-pack/plugins/apm/scripts/create_apm_users_and_roles/create_apm_users_and_roles_cli.ts diff --git a/x-pack/plugins/apm/scripts/create-apm-users-and-roles/helpers/call_kibana.ts b/x-pack/plugins/apm/scripts/create_apm_users_and_roles/helpers/call_kibana.ts similarity index 100% rename from x-pack/plugins/apm/scripts/create-apm-users-and-roles/helpers/call_kibana.ts rename to x-pack/plugins/apm/scripts/create_apm_users_and_roles/helpers/call_kibana.ts diff --git a/x-pack/plugins/apm/scripts/create-apm-users-and-roles/helpers/create_or_update_user.ts b/x-pack/plugins/apm/scripts/create_apm_users_and_roles/helpers/create_or_update_user.ts similarity index 100% rename from x-pack/plugins/apm/scripts/create-apm-users-and-roles/helpers/create_or_update_user.ts rename to x-pack/plugins/apm/scripts/create_apm_users_and_roles/helpers/create_or_update_user.ts diff --git a/x-pack/plugins/apm/scripts/create-apm-users-and-roles/helpers/create_role.ts b/x-pack/plugins/apm/scripts/create_apm_users_and_roles/helpers/create_role.ts similarity index 100% rename from x-pack/plugins/apm/scripts/create-apm-users-and-roles/helpers/create_role.ts rename to x-pack/plugins/apm/scripts/create_apm_users_and_roles/helpers/create_role.ts diff --git a/x-pack/plugins/apm/scripts/create-apm-users-and-roles/helpers/get_version.ts b/x-pack/plugins/apm/scripts/create_apm_users_and_roles/helpers/get_version.ts similarity index 100% rename from x-pack/plugins/apm/scripts/create-apm-users-and-roles/helpers/get_version.ts rename to x-pack/plugins/apm/scripts/create_apm_users_and_roles/helpers/get_version.ts diff --git a/x-pack/plugins/apm/scripts/create-apm-users-and-roles/roles/power_user_role.ts b/x-pack/plugins/apm/scripts/create_apm_users_and_roles/roles/power_user_role.ts similarity index 100% rename from x-pack/plugins/apm/scripts/create-apm-users-and-roles/roles/power_user_role.ts rename to x-pack/plugins/apm/scripts/create_apm_users_and_roles/roles/power_user_role.ts diff --git a/x-pack/plugins/apm/scripts/create-apm-users-and-roles/roles/read_only_user_role.ts b/x-pack/plugins/apm/scripts/create_apm_users_and_roles/roles/read_only_user_role.ts similarity index 100% rename from x-pack/plugins/apm/scripts/create-apm-users-and-roles/roles/read_only_user_role.ts rename to x-pack/plugins/apm/scripts/create_apm_users_and_roles/roles/read_only_user_role.ts diff --git a/x-pack/plugins/apm/scripts/create-functional-tests-archive.js b/x-pack/plugins/apm/scripts/create_functional_tests_archive.js similarity index 88% rename from x-pack/plugins/apm/scripts/create-functional-tests-archive.js rename to x-pack/plugins/apm/scripts/create_functional_tests_archive.js index 6e979a2802a85..55290f00a6d8c 100644 --- a/x-pack/plugins/apm/scripts/create-functional-tests-archive.js +++ b/x-pack/plugins/apm/scripts/create_functional_tests_archive.js @@ -9,4 +9,4 @@ // eslint-disable-next-line import/no-extraneous-dependencies require('@kbn/optimizer').registerNodeAutoTranspilation(); -require('./create-functional-tests-archive/index.ts'); +require('./create_functional_tests_archive/index.ts'); 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 similarity index 100% rename from x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts rename to x-pack/plugins/apm/scripts/create_functional_tests_archive/index.ts diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig.js b/x-pack/plugins/apm/scripts/optimize_tsconfig.js similarity index 84% rename from x-pack/plugins/apm/scripts/optimize-tsconfig.js rename to x-pack/plugins/apm/scripts/optimize_tsconfig.js index 8e7268141a993..fa15f0f582e89 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig.js +++ b/x-pack/plugins/apm/scripts/optimize_tsconfig.js @@ -5,7 +5,7 @@ * 2.0. */ -const { optimizeTsConfig } = require('./optimize-tsconfig/optimize'); +const { optimizeTsConfig } = require('./optimize_tsconfig/optimize'); optimizeTsConfig().catch((err) => { console.error(err); diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js b/x-pack/plugins/apm/scripts/optimize_tsconfig/optimize.js similarity index 98% rename from x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js rename to x-pack/plugins/apm/scripts/optimize_tsconfig/optimize.js index 68cba9b397f2e..fa45bfa12f3b7 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js +++ b/x-pack/plugins/apm/scripts/optimize_tsconfig/optimize.js @@ -110,7 +110,7 @@ async function optimizeTsConfig() { await setIgnoreChanges(); // eslint-disable-next-line no-console console.log( - 'Created an optimized tsconfig.json for APM. To undo these changes, run `./scripts/unoptimize-tsconfig.js`' + 'Created an optimized tsconfig.json for APM. To undo these changes, run `./scripts/unoptimize_tsconfig.js`' ); } diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js b/x-pack/plugins/apm/scripts/optimize_tsconfig/paths.js similarity index 93% rename from x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js rename to x-pack/plugins/apm/scripts/optimize_tsconfig/paths.js index 3a21a89e30917..df430dabafc9c 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js +++ b/x-pack/plugins/apm/scripts/optimize_tsconfig/paths.js @@ -9,7 +9,7 @@ const path = require('path'); const kibanaRoot = path.resolve(__dirname, '../../../../..'); const tsconfigTpl = path.resolve(__dirname, './tsconfig.json'); -const tsconfigTplTest = path.resolve(__dirname, './test-tsconfig.json'); +const tsconfigTplTest = path.resolve(__dirname, './test_tsconfig.json'); const filesToIgnore = [ path.resolve(kibanaRoot, 'tsconfig.json'), diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json b/x-pack/plugins/apm/scripts/optimize_tsconfig/test_tsconfig.json similarity index 100% rename from x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json rename to x-pack/plugins/apm/scripts/optimize_tsconfig/test_tsconfig.json diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json b/x-pack/plugins/apm/scripts/optimize_tsconfig/tsconfig.json similarity index 100% rename from x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json rename to x-pack/plugins/apm/scripts/optimize_tsconfig/tsconfig.json diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/unoptimize.js b/x-pack/plugins/apm/scripts/optimize_tsconfig/unoptimize.js similarity index 100% rename from x-pack/plugins/apm/scripts/optimize-tsconfig/unoptimize.js rename to x-pack/plugins/apm/scripts/optimize_tsconfig/unoptimize.js diff --git a/x-pack/plugins/apm/scripts/precommit.js b/x-pack/plugins/apm/scripts/precommit.js index a259cf932c912..ab19094c4e610 100644 --- a/x-pack/plugins/apm/scripts/precommit.js +++ b/x-pack/plugins/apm/scripts/precommit.js @@ -40,8 +40,8 @@ const tasks = new Listr( resolve( __dirname, useOptimizedTsConfig - ? './optimize-tsconfig.js' - : './unoptimize-tsconfig.js' + ? './optimize_tsconfig.js' + : './unoptimize_tsconfig.js' ), ], execaOpts diff --git a/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts b/x-pack/plugins/apm/scripts/shared/create_or_update_index.ts similarity index 100% rename from x-pack/plugins/apm/scripts/shared/create-or-update-index.ts rename to x-pack/plugins/apm/scripts/shared/create_or_update_index.ts diff --git a/x-pack/plugins/apm/scripts/shared/download-telemetry-template.ts b/x-pack/plugins/apm/scripts/shared/download_telemetry_template.ts similarity index 100% rename from x-pack/plugins/apm/scripts/shared/download-telemetry-template.ts rename to x-pack/plugins/apm/scripts/shared/download_telemetry_template.ts diff --git a/x-pack/plugins/apm/scripts/shared/get-http-auth.ts b/x-pack/plugins/apm/scripts/shared/get_http_auth.ts similarity index 91% rename from x-pack/plugins/apm/scripts/shared/get-http-auth.ts rename to x-pack/plugins/apm/scripts/shared/get_http_auth.ts index 1adc5de10bed0..9a51000228d08 100644 --- a/x-pack/plugins/apm/scripts/shared/get-http-auth.ts +++ b/x-pack/plugins/apm/scripts/shared/get_http_auth.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { KibanaConfig } from './read-kibana-config'; +import { KibanaConfig } from './read_kibana_config'; export const getHttpAuth = (config: KibanaConfig) => { const httpAuth = diff --git a/x-pack/plugins/apm/scripts/shared/read-kibana-config.ts b/x-pack/plugins/apm/scripts/shared/read_kibana_config.ts similarity index 100% rename from x-pack/plugins/apm/scripts/shared/read-kibana-config.ts rename to x-pack/plugins/apm/scripts/shared/read_kibana_config.ts diff --git a/x-pack/plugins/apm/scripts/unoptimize-tsconfig.js b/x-pack/plugins/apm/scripts/unoptimize_tsconfig.js similarity index 84% rename from x-pack/plugins/apm/scripts/unoptimize-tsconfig.js rename to x-pack/plugins/apm/scripts/unoptimize_tsconfig.js index 7ca4017ccc9ff..330abd963393e 100644 --- a/x-pack/plugins/apm/scripts/unoptimize-tsconfig.js +++ b/x-pack/plugins/apm/scripts/unoptimize_tsconfig.js @@ -5,7 +5,7 @@ * 2.0. */ -const { unoptimizeTsConfig } = require('./optimize-tsconfig/unoptimize'); +const { unoptimizeTsConfig } = require('./optimize_tsconfig/unoptimize'); unoptimizeTsConfig().catch((err) => { console.error(err); diff --git a/x-pack/plugins/apm/scripts/upload-telemetry-data.js b/x-pack/plugins/apm/scripts/upload_telemetry_data.js similarity index 90% rename from x-pack/plugins/apm/scripts/upload-telemetry-data.js rename to x-pack/plugins/apm/scripts/upload_telemetry_data.js index d393ddcf608fa..f101fb73aba95 100644 --- a/x-pack/plugins/apm/scripts/upload-telemetry-data.js +++ b/x-pack/plugins/apm/scripts/upload_telemetry_data.js @@ -9,4 +9,4 @@ // eslint-disable-next-line import/no-extraneous-dependencies require('@kbn/optimizer').registerNodeAutoTranspilation(); -require('./upload-telemetry-data/index.ts'); +require('./upload_telemetry_data/index.ts'); diff --git a/x-pack/plugins/apm/scripts/upload-telemetry-data/generate-sample-documents.ts b/x-pack/plugins/apm/scripts/upload_telemetry_data/generate_sample_documents.ts similarity index 100% rename from x-pack/plugins/apm/scripts/upload-telemetry-data/generate-sample-documents.ts rename to x-pack/plugins/apm/scripts/upload_telemetry_data/generate_sample_documents.ts diff --git a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts b/x-pack/plugins/apm/scripts/upload_telemetry_data/index.ts similarity index 93% rename from x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts rename to x-pack/plugins/apm/scripts/upload_telemetry_data/index.ts index 990376ca3e6ba..ece8e33e6aacb 100644 --- a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts +++ b/x-pack/plugins/apm/scripts/upload_telemetry_data/index.ts @@ -17,12 +17,12 @@ import { argv } from 'yargs'; import { Logger } from 'kibana/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { CollectTelemetryParams } from '../../server/lib/apm_telemetry/collect_data_telemetry'; -import { downloadTelemetryTemplate } from '../shared/download-telemetry-template'; +import { downloadTelemetryTemplate } from '../shared/download_telemetry_template'; import { mergeApmTelemetryMapping } from '../../common/apm_telemetry'; -import { generateSampleDocuments } from './generate-sample-documents'; -import { readKibanaConfig } from '../shared/read-kibana-config'; -import { getHttpAuth } from '../shared/get-http-auth'; -import { createOrUpdateIndex } from '../shared/create-or-update-index'; +import { generateSampleDocuments } from './generate_sample_documents'; +import { readKibanaConfig } from '../shared/read_kibana_config'; +import { getHttpAuth } from '../shared/get_http_auth'; +import { createOrUpdateIndex } from '../shared/create_or_update_index'; import { getEsClient } from '../shared/get_es_client'; async function uploadData() { 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 17beacae4b14d..723f52cb1eeda 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 @@ -7,17 +7,11 @@ import { schema } from '@kbn/config-schema'; import { take } from 'rxjs/operators'; -import type { - ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, - ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, - ALERT_REASON as ALERT_REASON_TYPED, -} from '@kbn/rule-data-utils'; import { - ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, - ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, - ALERT_REASON as ALERT_REASON_NON_TYPED, - // @ts-expect-error -} from '@kbn/rule-data-utils/target_node/technical_field_names'; + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_REASON, +} from '@kbn/rule-data-utils/technical_field_names'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { ENVIRONMENT_NOT_DEFINED, @@ -43,12 +37,6 @@ import { alertingEsClient } from './alerting_es_client'; import { RegisterRuleDependencies } from './register_apm_alerts'; import { termQuery } from '../../../../observability/server'; -const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = - ALERT_EVALUATION_THRESHOLD_NON_TYPED; -const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = - ALERT_EVALUATION_VALUE_NON_TYPED; -const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; - const paramsSchema = schema.object({ windowSize: schema.number(), windowUnit: schema.string(), 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 3500dc8fee921..4e68ca6b52248 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 @@ -7,17 +7,11 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { schema } from '@kbn/config-schema'; -import type { - ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, - ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, - ALERT_REASON as ALERT_REASON_TYPED, -} from '@kbn/rule-data-utils'; import { - ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, - ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, - ALERT_REASON as ALERT_REASON_NON_TYPED, - // @ts-expect-error -} from '@kbn/rule-data-utils/target_node/technical_field_names'; + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_REASON, +} from '@kbn/rule-data-utils/technical_field_names'; import { take } from 'rxjs/operators'; import { asDuration } from '../../../../observability/common/utils/formatters'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; @@ -49,12 +43,6 @@ import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; import { RegisterRuleDependencies } from './register_apm_alerts'; -const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = - ALERT_EVALUATION_THRESHOLD_NON_TYPED; -const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = - ALERT_EVALUATION_VALUE_NON_TYPED; -const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; - const paramsSchema = schema.object({ serviceName: schema.string(), transactionType: schema.string(), diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index ec2fbb4028b74..dead149cd7761 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -9,19 +9,12 @@ import { schema } from '@kbn/config-schema'; import { compact } from 'lodash'; import { ESSearchResponse } from 'src/core/types/elasticsearch'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { - ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, - ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, - ALERT_SEVERITY as ALERT_SEVERITY_TYPED, - ALERT_REASON as ALERT_REASON_TYPED, -} from '@kbn/rule-data-utils'; import { - ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, - ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, - ALERT_SEVERITY as ALERT_SEVERITY_NON_TYPED, - ALERT_REASON as ALERT_REASON_NON_TYPED, - // @ts-expect-error -} from '@kbn/rule-data-utils/target_node/technical_field_names'; + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_SEVERITY, + ALERT_REASON, +} from '@kbn/rule-data-utils/technical_field_names'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { ProcessorEvent } from '../../../common/processor_event'; import { getSeverity } from '../../../common/anomaly_detection'; @@ -48,13 +41,6 @@ import { } from '../../../common/environment_filter_values'; import { termQuery } from '../../../../observability/server'; -const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = - ALERT_EVALUATION_THRESHOLD_NON_TYPED; -const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = - ALERT_EVALUATION_VALUE_NON_TYPED; -const ALERT_SEVERITY: typeof ALERT_SEVERITY_TYPED = ALERT_SEVERITY_NON_TYPED; -const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; - const paramsSchema = schema.object({ serviceName: schema.maybe(schema.string()), transactionType: schema.maybe(schema.string()), 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 43dfbaf156f6c..cf5b45d901228 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 @@ -7,17 +7,11 @@ import { schema } from '@kbn/config-schema'; import { take } from 'rxjs/operators'; -import type { - ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, - ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, - ALERT_REASON as ALERT_REASON_TYPED, -} from '@kbn/rule-data-utils'; import { - ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, - ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, - ALERT_REASON as ALERT_REASON_NON_TYPED, - // @ts-expect-error -} from '@kbn/rule-data-utils/target_node/technical_field_names'; + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_REASON, +} from '@kbn/rule-data-utils/technical_field_names'; import { ENVIRONMENT_NOT_DEFINED, getEnvironmentEsField, @@ -50,12 +44,6 @@ import { getDocumentTypeFilterForTransactions } from '../helpers/transactions'; import { asPercent } from '../../../../observability/common/utils/formatters'; import { termQuery } from '../../../../observability/server'; -const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = - ALERT_EVALUATION_THRESHOLD_NON_TYPED; -const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = - ALERT_EVALUATION_VALUE_NON_TYPED; -const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; - const paramsSchema = schema.object({ windowSize: schema.number(), windowUnit: schema.string(), diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.test.ts index eb771e1e1aaf4..9df412b65b8d3 100644 --- a/x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.test.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.test.ts @@ -10,35 +10,27 @@ import { getRequestBase } from './get_request_base'; describe('correlations', () => { describe('getRequestBase', () => { - it('returns the request base parameters', () => { + it('defaults to not setting `ignore_throttled`', () => { const requestBase = getRequestBase({ index: 'apm-*', - includeFrozen: true, environment: ENVIRONMENT_ALL.value, kuery: '', start: 1577836800000, end: 1609459200000, }); - expect(requestBase).toEqual({ - index: 'apm-*', - ignore_throttled: false, - ignore_unavailable: true, - }); + expect(requestBase.ignore_throttled).toEqual(undefined); }); - it('defaults ignore_throttled to true', () => { + it('adds `ignore_throttled=false` when `includeFrozen=true`', () => { const requestBase = getRequestBase({ index: 'apm-*', + includeFrozen: true, environment: ENVIRONMENT_ALL.value, kuery: '', start: 1577836800000, end: 1609459200000, }); - expect(requestBase).toEqual({ - index: 'apm-*', - ignore_throttled: true, - ignore_unavailable: true, - }); + expect(requestBase.ignore_throttled).toEqual(false); }); }); }); diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.ts b/x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.ts index 5ab4e3b26122d..02719ee3929ce 100644 --- a/x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.ts @@ -13,6 +13,6 @@ export const getRequestBase = ({ }: CorrelationsParams) => ({ index, // matches APM's event client settings - ignore_throttled: includeFrozen === undefined ? true : !includeFrozen, + ...(includeFrozen ? { ignore_throttled: false } : {}), ignore_unavailable: true, }); diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.test.ts index 02af6637e5bb3..957e2393558ed 100644 --- a/x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.test.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.test.ts @@ -88,7 +88,7 @@ describe('query_field_candidates', () => { size: 1000, }, index: params.index, - ignore_throttled: !params.includeFrozen, + ignore_throttled: params.includeFrozen ? false : undefined, ignore_unavailable: true, }); }); diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram.test.ts index 3c5726ee586da..c9ea70452bdb0 100644 --- a/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram.test.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram.test.ts @@ -63,7 +63,7 @@ describe('query_histogram', () => { size: 0, }, index: params.index, - ignore_throttled: !params.includeFrozen, + ignore_throttled: params.includeFrozen ? false : undefined, ignore_unavailable: true, }); }); diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram_range_steps.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram_range_steps.test.ts index 3a79b4375e4a5..526207beaabce 100644 --- a/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram_range_steps.test.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram_range_steps.test.ts @@ -66,7 +66,7 @@ describe('query_histogram_range_steps', () => { size: 0, }, index: params.index, - ignore_throttled: !params.includeFrozen, + ignore_throttled: params.includeFrozen ? false : undefined, ignore_unavailable: true, }); }); diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/query_percentiles.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_percentiles.test.ts index 67b2f580e3f4d..4e637d1ca6f4a 100644 --- a/x-pack/plugins/apm/server/lib/correlations/queries/query_percentiles.test.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_percentiles.test.ts @@ -65,7 +65,7 @@ describe('query_percentiles', () => { track_total_hits: true, }, index: params.index, - ignore_throttled: !params.includeFrozen, + ignore_throttled: params.includeFrozen ? false : undefined, ignore_unavailable: true, }); }); diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/query_ranges.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_ranges.test.ts index 3cafc17e2681b..5c43b771f69ee 100644 --- a/x-pack/plugins/apm/server/lib/correlations/queries/query_ranges.test.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_ranges.test.ts @@ -82,7 +82,7 @@ describe('query_ranges', () => { size: 0, }, index: params.index, - ignore_throttled: !params.includeFrozen, + ignore_throttled: params.includeFrozen ? false : undefined, ignore_unavailable: true, }); }); diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index 2c8d0f6998bf8..4d4c935d20e76 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -5,6 +5,10 @@ * 2.0. */ +import { AggregationsTermsAggregationOrder } from '@elastic/elasticsearch/lib/api/types'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { environmentQuery } from '../../../common/utils/environment_query'; +import { kqlQuery, rangeQuery } from '../../../../observability/server'; import { ERROR_CULPRIT, ERROR_EXC_HANDLED, @@ -12,9 +16,8 @@ import { ERROR_EXC_TYPE, ERROR_GROUP_ID, ERROR_LOG_MESSAGE, + SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { getErrorGroupsProjection } from '../../projections/errors'; -import { mergeProjection } from '../../projections/util/merge_projection'; import { getErrorName } from '../helpers/get_error_name'; import { Setup } from '../helpers/setup_request'; @@ -42,27 +45,31 @@ export async function getErrorGroups({ // sort buckets by last occurrence of error const sortByLatestOccurrence = sortField === 'latestOccurrenceAt'; - const projection = getErrorGroupsProjection({ - environment, - kuery, - serviceName, - start, - end, - }); - - const order = sortByLatestOccurrence - ? { - max_timestamp: sortDirection, - } + const maxTimestampAggKey = 'max_timestamp'; + const order: AggregationsTermsAggregationOrder = sortByLatestOccurrence + ? { [maxTimestampAggKey]: sortDirection } : { _count: sortDirection }; - const params = mergeProjection(projection, { + const params = { + apm: { + events: [ProcessorEvent.error as const], + }, body: { size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ], + }, + }, aggs: { error_groups: { terms: { - ...projection.body.aggs.error_groups.terms, + field: ERROR_GROUP_ID, size: 500, order, }, @@ -83,19 +90,13 @@ export async function getErrorGroups({ }, }, ...(sortByLatestOccurrence - ? { - max_timestamp: { - max: { - field: '@timestamp', - }, - }, - } + ? { [maxTimestampAggKey]: { max: { field: '@timestamp' } } } : {}), }, }, }, }, - }); + }; const resp = await apmEventClient.search('get_error_groups', params); 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 26b00b075a5c8..a45d314c33719 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 @@ -100,7 +100,7 @@ export function createApmEventClient({ const searchParams = { ...withPossibleLegacyDataFilter, - ignore_throttled: !includeFrozen, + ...(includeFrozen ? { ignore_throttled: false } : {}), ignore_unavailable: true, preference: 'any', }; 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 52e9e5a8ea74a..7b3201095106e 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 @@ -132,7 +132,6 @@ describe('setupRequest', () => { }, }, ignore_unavailable: true, - ignore_throttled: true, preference: 'any', }, { @@ -251,7 +250,7 @@ describe('without a bool filter', () => { }); describe('with includeFrozen=false', () => { - it('sets `ignore_throttled=true`', async () => { + it('should NOT send "ignore_throttled:true" in the request', async () => { const mockResources = getMockResources(); // mock includeFrozen to return false @@ -268,7 +267,7 @@ describe('with includeFrozen=false', () => { const params = mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock .calls[0][0]; - expect(params.ignore_throttled).toBe(true); + expect(params.ignore_throttled).toBe(undefined); }); }); diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/default.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/default.ts index 62e23d19b00bd..54e10bd8adde0 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/default.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/default.ts @@ -9,7 +9,7 @@ import { Setup } from '../../helpers/setup_request'; import { getCPUChartData } from './shared/cpu'; import { getMemoryChartData } from './shared/memory'; -export async function getDefaultMetricsCharts({ +export function getDefaultMetricsCharts({ environment, kuery, serviceName, @@ -24,10 +24,8 @@ export async function getDefaultMetricsCharts({ start: number; end: number; }) { - const charts = await Promise.all([ + return Promise.all([ getCPUChartData({ environment, kuery, setup, serviceName, start, end }), getMemoryChartData({ environment, kuery, setup, serviceName, start, end }), ]); - - return { charts }; } diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index 117b372d445d2..2d4cf2f70ab5f 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -11,17 +11,26 @@ import { isFiniteNumber } from '../../../../../../common/utils/is_finite_number' import { Setup } from '../../../../helpers/setup_request'; import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; import { ChartBase } from '../../../types'; -import { getMetricsProjection } from '../../../../../projections/metrics'; -import { mergeProjection } from '../../../../../projections/util/merge_projection'; + import { AGENT_NAME, LABEL_NAME, METRIC_JAVA_GC_COUNT, METRIC_JAVA_GC_TIME, + SERVICE_NAME, } from '../../../../../../common/elasticsearch_fieldnames'; import { getBucketSize } from '../../../../helpers/get_bucket_size'; import { getVizColorForIndex } from '../../../../../../common/viz_colors'; import { JAVA_AGENT_NAMES } from '../../../../../../common/agent_name'; +import { + environmentQuery, + serviceNodeNameQuery, +} from '../../../../../../common/utils/environment_query'; +import { + kqlQuery, + rangeQuery, +} from '../../../../../../../observability/server'; +import { ProcessorEvent } from '../../../../../../common/processor_event'; export async function fetchAndTransformGcMetrics({ environment, @@ -50,26 +59,24 @@ export async function fetchAndTransformGcMetrics({ const { bucketSize } = getBucketSize({ start, end }); - const projection = getMetricsProjection({ - environment, - kuery, - serviceName, - serviceNodeName, - start, - end, - }); - // GC rate and time are reported by the agents as monotonically // increasing counters, which means that we have to calculate // the delta in an es query. In the future agent might start // reporting deltas. - const params = mergeProjection(projection, { + const params = { + apm: { + events: [ProcessorEvent.metric], + }, body: { size: 0, query: { bool: { filter: [ - ...projection.body.query.bool.filter, + { term: { [SERVICE_NAME]: serviceName } }, + ...serviceNodeNameQuery(serviceNodeName), + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), { exists: { field: fieldName } }, { terms: { [AGENT_NAME]: JAVA_AGENT_NAMES } }, ], @@ -114,7 +121,7 @@ export async function fetchAndTransformGcMetrics({ }, }, }, - }); + }; const response = await apmEventClient.search(operationName, params); diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/index.ts index 164777539c9d5..9039bb19ebb78 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/index.ts @@ -32,7 +32,7 @@ export function getJavaMetricsCharts({ start: number; end: number; }) { - return withApmSpan('get_java_system_metric_charts', async () => { + return withApmSpan('get_java_system_metric_charts', () => { const options = { environment, kuery, @@ -43,7 +43,7 @@ export function getJavaMetricsCharts({ end, }; - const charts = await Promise.all([ + return Promise.all([ getCPUChartData(options), getMemoryChartData(options), getHeapMemoryChart(options), @@ -52,7 +52,5 @@ export function getJavaMetricsCharts({ getGcRateChart(options), getGcTimeChart(options), ]); - - return { charts }; }); } diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index 581a0782e4d72..0a24179ed9fc6 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -5,15 +5,23 @@ * 2.0. */ -import { Overwrite, Unionize } from 'utility-types'; +import { Unionize } from 'utility-types'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { getVizColorForIndex } from '../../../common/viz_colors'; import { AggregationOptionsByType } from '../../../../../../src/core/types/elasticsearch'; -import { getMetricsProjection } from '../../projections/metrics'; -import { mergeProjection } from '../../projections/util/merge_projection'; -import { APMEventESSearchRequest } from '../helpers/create_es_client/create_apm_event_client'; import { getMetricsDateHistogramParams } from '../helpers/metrics'; import { Setup } from '../helpers/setup_request'; -import { transformDataToMetricsChart } from './transform_metrics_chart'; import { ChartBase } from './types'; +import { + environmentQuery, + serviceNodeNameQuery, +} from '../../../common/utils/environment_query'; +import { kqlQuery, rangeQuery } from '../../../../observability/server'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; +import { APMEventESSearchRequest } from '../helpers/create_es_client/create_apm_event_client'; +import { PromiseReturnType } from '../../../../observability/typings/common'; type MetricsAggregationMap = Unionize<{ min: AggregationOptionsByType['min']; @@ -24,31 +32,20 @@ type MetricsAggregationMap = Unionize<{ type MetricAggs = Record; -export type GenericMetricsRequest = Overwrite< - APMEventESSearchRequest, - { - body: { - aggs: { - timeseriesData: { - date_histogram: AggregationOptionsByType['date_histogram']; - aggs: MetricAggs; - }; - } & MetricAggs; - }; - } ->; - -interface Filter { - exists?: { - field: string; - }; - term?: { - [key: string]: string; - }; - terms?: { - [key: string]: string[]; +export type GenericMetricsRequest = APMEventESSearchRequest & { + body: { + aggs: { + timeseriesData: { + date_histogram: AggregationOptionsByType['date_histogram']; + aggs: MetricAggs; + }; + } & MetricAggs; }; -} +}; + +export type GenericMetricsChart = PromiseReturnType< + typeof fetchAndTransformMetrics +>; export async function fetchAndTransformMetrics({ environment, @@ -72,26 +69,27 @@ export async function fetchAndTransformMetrics({ end: number; chartBase: ChartBase; aggs: T; - additionalFilters?: Filter[]; + additionalFilters?: QueryDslQueryContainer[]; operationName: string; }) { const { apmEventClient, config } = setup; - const projection = getMetricsProjection({ - environment, - kuery, - serviceName, - serviceNodeName, - start, - end, - }); - - const params: GenericMetricsRequest = mergeProjection(projection, { + const params: GenericMetricsRequest = { + apm: { + events: [ProcessorEvent.metric], + }, body: { size: 0, query: { bool: { - filter: [...projection.body.query.bool.filter, ...additionalFilters], + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + ...serviceNodeNameQuery(serviceNodeName), + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ...additionalFilters, + ], }, }, aggs: { @@ -106,9 +104,43 @@ export async function fetchAndTransformMetrics({ ...aggs, }, }, - }); + }; + + const { hits, aggregations } = await apmEventClient.search( + operationName, + params + ); + const timeseriesData = aggregations?.timeseriesData; - const response = await apmEventClient.search(operationName, params); + return { + title: chartBase.title, + key: chartBase.key, + yUnit: chartBase.yUnit, + series: + hits.total.value === 0 + ? [] + : Object.keys(chartBase.series).map((seriesKey, i) => { + // @ts-ignore + const overallValue = aggregations?.[seriesKey]?.value as number; - return transformDataToMetricsChart(response, chartBase); + return { + title: chartBase.series[seriesKey].title, + key: seriesKey, + type: chartBase.type, + color: + chartBase.series[seriesKey].color || + getVizColorForIndex(i, theme), + overallValue, + data: + timeseriesData?.buckets.map((bucket) => { + const { value } = bucket[seriesKey]; + const y = value === null || isNaN(value) ? null : value; + return { + x: bucket.key, + y, + }; + }) || [], + }; + }), + }; } diff --git a/x-pack/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts b/x-pack/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts index 29991034f7c5e..611bb8196032c 100644 --- a/x-pack/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts +++ b/x-pack/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts @@ -8,12 +8,8 @@ import { Setup } from '../helpers/setup_request'; import { getJavaMetricsCharts } from './by_agent/java'; import { getDefaultMetricsCharts } from './by_agent/default'; -import { GenericMetricsChart } from './transform_metrics_chart'; import { isJavaAgentName } from '../../../common/agent_name'; - -export interface MetricsChartsByAgentAPIResponse { - charts: GenericMetricsChart[]; -} +import { GenericMetricsChart } from './fetch_and_transform_metrics'; export async function getMetricsChartDataByAgent({ environment, @@ -33,7 +29,7 @@ export async function getMetricsChartDataByAgent({ agentName: string; start: number; end: number; -}): Promise { +}): Promise { if (isJavaAgentName(agentName)) { return getJavaMetricsCharts({ environment, diff --git a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.test.ts b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.test.ts deleted file mode 100644 index 107e2d5774816..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { transformDataToMetricsChart } from './transform_metrics_chart'; -import { ChartType, YUnit } from '../../../typings/timeseries'; - -test('transformDataToMetricsChart should transform an ES result into a chart object', () => { - const response = { - hits: { total: { value: 5000 } }, - aggregations: { - a: { value: 1000 }, - b: { value: 1000 }, - c: { value: 1000 }, - timeseriesData: { - buckets: [ - { - a: { value: 10 }, - b: { value: 10 }, - c: { value: 10 }, - key: 1, - doc_count: 0, - }, - { - a: { value: 20 }, - b: { value: 20 }, - c: { value: 20 }, - key: 2, - doc_count: 0, - }, - { - a: { value: 30 }, - b: { value: 30 }, - c: { value: 30 }, - key: 3, - doc_count: 0, - }, - ], - }, - }, - } as any; - - const chartBase = { - title: 'Test Chart Title', - type: 'linemark' as ChartType, - key: 'test_chart_key', - yUnit: 'number' as YUnit, - series: { - a: { title: 'Series A', color: 'red' }, - b: { title: 'Series B', color: 'blue' }, - c: { title: 'Series C', color: 'green' }, - }, - }; - - const chart = transformDataToMetricsChart(response, chartBase); - - expect(chart).toMatchInlineSnapshot(` -Object { - "key": "test_chart_key", - "series": Array [ - Object { - "color": "red", - "data": Array [ - Object { - "x": 1, - "y": 10, - }, - Object { - "x": 2, - "y": 20, - }, - Object { - "x": 3, - "y": 30, - }, - ], - "key": "a", - "overallValue": 1000, - "title": "Series A", - "type": "linemark", - }, - Object { - "color": "blue", - "data": Array [ - Object { - "x": 1, - "y": 10, - }, - Object { - "x": 2, - "y": 20, - }, - Object { - "x": 3, - "y": 30, - }, - ], - "key": "b", - "overallValue": 1000, - "title": "Series B", - "type": "linemark", - }, - Object { - "color": "green", - "data": Array [ - Object { - "x": 1, - "y": 10, - }, - Object { - "x": 2, - "y": 20, - }, - Object { - "x": 3, - "y": 30, - }, - ], - "key": "c", - "overallValue": 1000, - "title": "Series C", - "type": "linemark", - }, - ], - "title": "Test Chart Title", - "yUnit": "number", -} -`); -}); diff --git a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts deleted file mode 100644 index fea853af93b84..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; -import { ESSearchResponse } from '../../../../../../src/core/types/elasticsearch'; -import { getVizColorForIndex } from '../../../common/viz_colors'; -import { GenericMetricsRequest } from './fetch_and_transform_metrics'; -import { ChartBase } from './types'; - -export type GenericMetricsChart = ReturnType< - typeof transformDataToMetricsChart ->; - -export function transformDataToMetricsChart( - result: ESSearchResponse, - chartBase: ChartBase -) { - const { aggregations } = result; - const timeseriesData = aggregations?.timeseriesData; - - return { - title: chartBase.title, - key: chartBase.key, - yUnit: chartBase.yUnit, - series: - result.hits.total.value > 0 - ? Object.keys(chartBase.series).map((seriesKey, i) => { - const overallValue = aggregations?.[seriesKey]?.value; - - return { - title: chartBase.series[seriesKey].title, - key: seriesKey, - type: chartBase.type, - color: - chartBase.series[seriesKey].color || - getVizColorForIndex(i, theme), - overallValue, - data: - timeseriesData?.buckets.map((bucket) => { - const { value } = bucket[seriesKey]; - const y = value === null || isNaN(value) ? null : value; - return { - x: bucket.key, - y, - }; - }) || [], - }; - }) - : [], - }; -} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index aaf55413d9774..9f94bdd9275c0 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -156,7 +156,6 @@ async function getServicesData(options: IEnvOptions) { export type ConnectionsResponse = PromiseReturnType; export type ServicesResponse = PromiseReturnType; -export type ServiceMapAPIResponse = PromiseReturnType; export function getServiceMap(options: IEnvOptions) { return withApmSpan('get_service_map', async () => { diff --git a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap index 1b72fc8867570..e0591a90b1c19 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap @@ -35,11 +35,6 @@ Object { "service.name": "foo", }, }, - Object { - "term": Object { - "service.node.name": "bar", - }, - }, Object { "range": Object { "@timestamp": Object { @@ -49,6 +44,11 @@ Object { }, }, }, + Object { + "term": Object { + "service.node.name": "bar", + }, + }, ], }, }, @@ -92,6 +92,15 @@ Object { "service.name": "foo", }, }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 0, + "lte": 50000, + }, + }, + }, Object { "bool": Object { "must_not": Array [ @@ -103,15 +112,6 @@ Object { ], }, }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 0, - "lte": 50000, - }, - }, - }, ], }, }, @@ -191,6 +191,7 @@ Object { ], }, }, + "size": 0, }, } `; diff --git a/x-pack/plugins/apm/server/lib/service_nodes/index.ts b/x-pack/plugins/apm/server/lib/service_nodes/index.ts index 0ae7274c3b33f..541fbb79c9e50 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/index.ts +++ b/x-pack/plugins/apm/server/lib/service_nodes/index.ts @@ -14,8 +14,13 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; -import { getServiceNodesProjection } from '../../projections/service_nodes'; -import { mergeProjection } from '../../projections/util/merge_projection'; +import { + SERVICE_NAME, + SERVICE_NODE_NAME, +} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { kqlQuery, rangeQuery } from '../../../../observability/server'; +import { environmentQuery } from '../../../common/utils/environment_query'; import { Setup } from '../helpers/setup_request'; const getServiceNodes = async ({ @@ -35,20 +40,26 @@ const getServiceNodes = async ({ }) => { const { apmEventClient } = setup; - const projection = getServiceNodesProjection({ - kuery, - serviceName, - environment, - start, - end, - }); - - const params = mergeProjection(projection, { + const params = { + apm: { + events: [ProcessorEvent.metric], + }, body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ], + }, + }, aggs: { nodes: { terms: { - ...projection.body.aggs.nodes.terms, + field: SERVICE_NODE_NAME, size: 10000, missing: SERVICE_NODE_NAME_MISSING, }, @@ -57,7 +68,7 @@ const getServiceNodes = async ({ top_metrics: { metrics: asMutableArray([{ field: HOST_NAME }] as const), sort: { - '@timestamp': 'desc', + '@timestamp': 'desc' as const, }, }, }, @@ -85,7 +96,7 @@ const getServiceNodes = async ({ }, }, }, - }); + }; const response = await apmEventClient.search('get_service_nodes', params); diff --git a/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts b/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts index 3b2354a54d96f..aac04fee397d4 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts @@ -5,9 +5,7 @@ * 2.0. */ -import type { EVENT_KIND as EVENT_KIND_TYPED } from '@kbn/rule-data-utils'; -// @ts-expect-error -import { EVENT_KIND as EVENT_KIND_NON_TYPED } from '@kbn/rule-data-utils/target_node/technical_field_names'; +import { EVENT_KIND } from '@kbn/rule-data-utils/technical_field_names'; import { IRuleDataClient } from '../../../../rule_registry/server'; import { SERVICE_NAME, @@ -16,8 +14,6 @@ import { import { rangeQuery } from '../../../../observability/server'; import { environmentQuery } from '../../../common/utils/environment_query'; -const EVENT_KIND: typeof EVENT_KIND_TYPED = EVENT_KIND_NON_TYPED; - export async function getServiceAlerts({ ruleDataClient, start, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts index ef52c4b0f4927..ab0fa91529917 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts @@ -11,8 +11,16 @@ import { CONTAINER_ID, } from '../../../common/elasticsearch_fieldnames'; import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; -import { mergeProjection } from '../../projections/util/merge_projection'; -import { getServiceNodesProjection } from '../../projections/service_nodes'; +import { + SERVICE_NAME, + SERVICE_NODE_NAME, +} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { kqlQuery, rangeQuery } from '../../../../observability/server'; +import { + environmentQuery, + serviceNodeNameQuery, +} from '../../../common/utils/environment_query'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; export async function getServiceNodeMetadata({ @@ -32,39 +40,48 @@ export async function getServiceNodeMetadata({ }) { const { apmEventClient } = setup; - const query = mergeProjection( - getServiceNodesProjection({ - kuery, - serviceName, - serviceNodeName, - environment: ENVIRONMENT_ALL.value, - start, - end, - }), - { - body: { - size: 0, - aggs: { - host: { - terms: { - field: HOST_NAME, - size: 1, - }, + const params = { + apm: { + events: [ProcessorEvent.metric], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ...environmentQuery(ENVIRONMENT_ALL.value), + ...kqlQuery(kuery), + ...serviceNodeNameQuery(serviceNodeName), + ], + }, + }, + aggs: { + nodes: { + terms: { + field: SERVICE_NODE_NAME, }, - containerId: { - terms: { - field: CONTAINER_ID, - size: 1, - }, + }, + host: { + terms: { + field: HOST_NAME, + size: 1, + }, + }, + containerId: { + terms: { + field: CONTAINER_ID, + size: 1, }, }, }, - } - ); + }, + }; const response = await apmEventClient.search( 'get_service_node_metadata', - query + params ); return { diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts index dadb29d156e0b..a22c1d35dc663 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts @@ -8,14 +8,9 @@ import { withApmSpan } from '../../../../utils/with_apm_span'; import { getAllEnvironments } from '../../../environments/get_all_environments'; import { Setup } from '../../../helpers/setup_request'; -import { PromiseReturnType } from '../../../../../../observability/typings/common'; import { getExistingEnvironmentsForService } from './get_existing_environments_for_service'; import { ALL_OPTION_VALUE } from '../../../../../common/agent_configuration/all_option'; -export type AgentConfigurationEnvironmentsAPIResponse = PromiseReturnType< - typeof getEnvironments ->; - export async function getEnvironments({ serviceName, setup, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts index 06bd900872a20..fc5167159b98d 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts @@ -7,15 +7,10 @@ import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup } from '../../helpers/setup_request'; -import { PromiseReturnType } from '../../../../../observability/typings/common'; import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { ALL_OPTION_VALUE } from '../../../../common/agent_configuration/all_option'; import { getProcessorEventForTransactions } from '../../helpers/transactions'; -export type AgentConfigurationServicesAPIResponse = PromiseReturnType< - typeof getServiceNames ->; - export async function getServiceNames({ setup, searchAggregatedTransactions, diff --git a/x-pack/plugins/apm/server/projections/errors.ts b/x-pack/plugins/apm/server/projections/errors.ts deleted file mode 100644 index b256428143400..0000000000000 --- a/x-pack/plugins/apm/server/projections/errors.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - SERVICE_NAME, - ERROR_GROUP_ID, -} from '../../common/elasticsearch_fieldnames'; -import { rangeQuery, kqlQuery } from '../../../observability/server'; -import { environmentQuery } from '../../common/utils/environment_query'; -import { ProcessorEvent } from '../../common/processor_event'; - -export function getErrorGroupsProjection({ - environment, - kuery, - serviceName, - start, - end, -}: { - environment: string; - kuery: string; - serviceName: string; - start: number; - end: number; -}) { - return { - apm: { - events: [ProcessorEvent.error as const], - }, - body: { - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ], - }, - }, - aggs: { - error_groups: { - terms: { - field: ERROR_GROUP_ID, - }, - }, - }, - }, - }; -} diff --git a/x-pack/plugins/apm/server/projections/metrics.ts b/x-pack/plugins/apm/server/projections/metrics.ts deleted file mode 100644 index 417281f2de487..0000000000000 --- a/x-pack/plugins/apm/server/projections/metrics.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { - SERVICE_NAME, - SERVICE_NODE_NAME, -} from '../../common/elasticsearch_fieldnames'; -import { rangeQuery, kqlQuery } from '../../../observability/server'; -import { environmentQuery } from '../../common/utils/environment_query'; -import { SERVICE_NODE_NAME_MISSING } from '../../common/service_nodes'; -import { ProcessorEvent } from '../../common/processor_event'; - -function getServiceNodeNameFilters(serviceNodeName?: string) { - if (!serviceNodeName) { - return []; - } - - if (serviceNodeName === SERVICE_NODE_NAME_MISSING) { - return [{ bool: { must_not: [{ exists: { field: SERVICE_NODE_NAME } }] } }]; - } - - return [{ term: { [SERVICE_NODE_NAME]: serviceNodeName } }]; -} - -export function getMetricsProjection({ - environment, - kuery, - serviceName, - serviceNodeName, - start, - end, -}: { - environment: string; - kuery: string; - serviceName: string; - serviceNodeName?: string; - start: number; - end: number; -}) { - const filter = [ - { term: { [SERVICE_NAME]: serviceName } }, - ...getServiceNodeNameFilters(serviceNodeName), - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ] as QueryDslQueryContainer[]; - - return { - apm: { - events: [ProcessorEvent.metric], - }, - body: { - query: { - bool: { - filter, - }, - }, - }, - }; -} diff --git a/x-pack/plugins/apm/server/projections/service_nodes.ts b/x-pack/plugins/apm/server/projections/service_nodes.ts deleted file mode 100644 index 5d97af0ad9860..0000000000000 --- a/x-pack/plugins/apm/server/projections/service_nodes.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SERVICE_NODE_NAME } from '../../common/elasticsearch_fieldnames'; -import { mergeProjection } from './util/merge_projection'; -import { getMetricsProjection } from './metrics'; - -export function getServiceNodesProjection({ - serviceName, - serviceNodeName, - environment, - kuery, - start, - end, -}: { - serviceName: string; - serviceNodeName?: string; - environment: string; - kuery: string; - start: number; - end: number; -}) { - return mergeProjection( - getMetricsProjection({ - serviceName, - serviceNodeName, - environment, - kuery, - start, - end, - }), - { - body: { - aggs: { - nodes: { - terms: { - field: SERVICE_NODE_NAME, - }, - }, - }, - }, - } - ); -} diff --git a/x-pack/plugins/apm/server/routes/metrics.ts b/x-pack/plugins/apm/server/routes/metrics.ts index 1817c3e1546bd..36a504859797a 100644 --- a/x-pack/plugins/apm/server/routes/metrics.ts +++ b/x-pack/plugins/apm/server/routes/metrics.ts @@ -37,7 +37,8 @@ const metricsChartsRoute = createApmServerRoute({ const { serviceName } = params.path; const { agentName, environment, kuery, serviceNodeName, start, end } = params.query; - return await getMetricsChartDataByAgent({ + + const charts = await getMetricsChartDataByAgent({ environment, kuery, setup, @@ -47,6 +48,8 @@ const metricsChartsRoute = createApmServerRoute({ start, end, }); + + return { charts }; }, }); diff --git a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts index ba99b0624c441..8051ef2a72b6a 100644 --- a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts +++ b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts @@ -64,30 +64,6 @@ export function onPremInstructions({ iconType: 'alert', }, instructionVariants: [ - // hides fleet section when plugin is disabled - ...(isFleetPluginEnabled - ? [ - { - id: INSTRUCTION_VARIANT.FLEET, - instructions: [ - { - title: i18n.translate('xpack.apm.tutorial.fleet.title', { - defaultMessage: 'Fleet', - }), - customComponentName: 'TutorialFleetInstructions', - }, - ], - }, - ] - : []), - { - id: INSTRUCTION_VARIANT.OSX, - instructions: [ - createDownloadServerOsx(), - EDIT_CONFIG, - START_SERVER_UNIX, - ], - }, { id: INSTRUCTION_VARIANT.DEB, instructions: [ @@ -104,10 +80,34 @@ export function onPremInstructions({ START_SERVER_UNIX_SYSV, ], }, + { + id: INSTRUCTION_VARIANT.OSX, + instructions: [ + createDownloadServerOsx(), + EDIT_CONFIG, + START_SERVER_UNIX, + ], + }, { id: INSTRUCTION_VARIANT.WINDOWS, instructions: createWindowsServerInstructions(), }, + // hides fleet section when plugin is disabled + ...(isFleetPluginEnabled + ? [ + { + id: INSTRUCTION_VARIANT.FLEET, + instructions: [ + { + title: i18n.translate('xpack.apm.tutorial.fleet.title', { + defaultMessage: 'Fleet', + }), + customComponentName: 'TutorialFleetInstructions', + }, + ], + }, + ] + : []), ], statusCheck: { title: i18n.translate( diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/url.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/url.ts index e9d25904aa0b4..001d6370e5f06 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/fields/url.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/url.ts @@ -8,4 +8,5 @@ export interface Url { domain?: string; full: string; + original?: string; } diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts index 01bc0ed52ecae..d191cb6d4e84c 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts @@ -7,8 +7,10 @@ import { APMBaseDoc } from './apm_base_doc'; import { EventOutcome } from './fields/event_outcome'; +import { Http } from './fields/http'; import { Stackframe } from './fields/stackframe'; import { TimestampUs } from './fields/timestamp_us'; +import { Url } from './fields/url'; interface Processor { name: 'transaction'; @@ -67,4 +69,6 @@ export interface SpanRaw extends APMBaseDoc { id: string; }; child?: { id: string[] }; + http?: Http; + url?: Url; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_tables.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_tables.ts index 18aa70534b0ba..22c4d16d9810a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_tables.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__fixtures__/test_tables.ts @@ -21,6 +21,11 @@ const testTable: Datatable = { name: 'name', meta: { type: 'string' }, }, + { + id: 'title_id', + name: 'title', + meta: { type: 'string' }, + }, { id: 'time', name: 'time', @@ -49,6 +54,7 @@ const testTable: Datatable = { price: 605, quantity: 100, in_stock: true, + title_id: 'title1', }, { name: 'product1', @@ -56,6 +62,7 @@ const testTable: Datatable = { price: 583, quantity: 200, in_stock: true, + title_id: 'title2', }, { name: 'product1', @@ -63,6 +70,7 @@ const testTable: Datatable = { price: 420, quantity: 300, in_stock: true, + title_id: 'title3', }, { name: 'product2', @@ -70,6 +78,7 @@ const testTable: Datatable = { price: 216, quantity: 350, in_stock: false, + title_id: 'title4', }, { name: 'product2', @@ -77,6 +86,7 @@ const testTable: Datatable = { price: 200, quantity: 256, in_stock: false, + title_id: 'title5', }, { name: 'product2', @@ -84,6 +94,7 @@ const testTable: Datatable = { price: 190, quantity: 231, in_stock: false, + title_id: 'title6', }, { name: 'product3', @@ -91,6 +102,7 @@ const testTable: Datatable = { price: 67, quantity: 240, in_stock: true, + title_id: 'title7', }, { name: 'product4', @@ -98,6 +110,7 @@ const testTable: Datatable = { price: 311, quantity: 447, in_stock: false, + title_id: 'title8', }, { name: 'product5', @@ -105,6 +118,7 @@ const testTable: Datatable = { price: 288, quantity: 384, in_stock: true, + title_id: 'title9', }, ], }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/columns.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/columns.test.js index d7f28559ee0ef..0184920285a6d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/columns.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/columns.test.js @@ -20,15 +20,20 @@ describe('columns', () => { it('returns a datatable with included columns and without excluded columns', () => { const arbitraryRowIndex = 7; const result = fn(testTable, { - include: 'name, price, quantity, foo, bar', + include: 'name, title_id, price, quantity, foo, bar', exclude: 'price, quantity, fizz, buzz', }); expect(result.columns[0]).toHaveProperty('name', 'name'); + expect(result.columns[1]).toHaveProperty('id', 'title_id'); expect(result.rows[arbitraryRowIndex]).toHaveProperty( 'name', testTable.rows[arbitraryRowIndex].name ); + expect(result.rows[arbitraryRowIndex]).toHaveProperty( + 'title_id', + testTable.rows[arbitraryRowIndex].title_id + ); expect(result.rows[arbitraryRowIndex]).not.toHaveProperty('price'); expect(result.rows[arbitraryRowIndex]).not.toHaveProperty('quantity'); expect(result.rows[arbitraryRowIndex]).not.toHaveProperty('foo'); @@ -46,8 +51,8 @@ describe('columns', () => { expect( fn(testTable, { - include: 'price, quantity, in_stock', - exclude: 'price, quantity, in_stock', + include: 'price, quantity, in_stock, title_id', + exclude: 'price, quantity, in_stock, title_id', }) ).toEqual(emptyTable); }); @@ -56,15 +61,17 @@ describe('columns', () => { it('returns a datatable with included columns only', () => { const arbitraryRowIndex = 3; const result = fn(testTable, { - include: 'name, time, in_stock', + include: 'name, time, in_stock, title_id', }); - expect(result.columns).toHaveLength(3); - expect(Object.keys(result.rows[0])).toHaveLength(3); + expect(result.columns).toHaveLength(4); + expect(Object.keys(result.rows[0])).toHaveLength(4); expect(result.columns[0]).toHaveProperty('name', 'name'); expect(result.columns[1]).toHaveProperty('name', 'time'); expect(result.columns[2]).toHaveProperty('name', 'in_stock'); + expect(result.columns[3]).toHaveProperty('id', 'title_id'); + expect(result.rows[arbitraryRowIndex]).toHaveProperty( 'name', testTable.rows[arbitraryRowIndex].name @@ -77,6 +84,10 @@ describe('columns', () => { 'in_stock', testTable.rows[arbitraryRowIndex].in_stock ); + expect(result.rows[arbitraryRowIndex]).toHaveProperty( + 'title_id', + testTable.rows[arbitraryRowIndex].title_id + ); }); it('ignores invalid columns', () => { @@ -102,14 +113,15 @@ describe('columns', () => { describe('exclude', () => { it('returns a datatable without excluded columns', () => { const arbitraryRowIndex = 5; - const result = fn(testTable, { exclude: 'price, quantity, foo, bar' }); + const result = fn(testTable, { exclude: 'price, quantity, foo, bar, title_id' }); - expect(result.columns.length).toEqual(testTable.columns.length - 2); - expect(Object.keys(result.rows[0])).toHaveLength(testTable.columns.length - 2); + expect(result.columns.length).toEqual(testTable.columns.length - 3); + expect(Object.keys(result.rows[0])).toHaveLength(testTable.columns.length - 3); expect(result.rows[arbitraryRowIndex]).not.toHaveProperty('price'); expect(result.rows[arbitraryRowIndex]).not.toHaveProperty('quantity'); expect(result.rows[arbitraryRowIndex]).not.toHaveProperty('foo'); expect(result.rows[arbitraryRowIndex]).not.toHaveProperty('bar'); + expect(result.rows[arbitraryRowIndex]).not.toHaveProperty('title_id'); }); it('ignores invalid columns', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/columns.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/columns.ts index a81368e23a477..e627086d70d9d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/columns.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/columns.ts @@ -14,6 +14,26 @@ interface Arguments { exclude: string; } +const prepareFields = (fields: string) => fields.split(',').map((field) => field.trim()); + +const getFieldsIds = (cols: DatatableColumn[]) => cols.map((col) => col.id ?? col.name); + +const splitColumnsByFields = ( + cols: DatatableColumn[], + fields: string[], + saveOther: boolean = false +) => + cols.reduce<{ matched: DatatableColumn[]; other: DatatableColumn[] }>( + (splitColumns, col) => { + if (fields.includes(col.id) || fields.includes(col.name)) { + return { ...splitColumns, matched: [...splitColumns.matched, col] }; + } + + return saveOther ? { ...splitColumns, other: [...splitColumns.other, col] } : splitColumns; + }, + { matched: [], other: [] } + ); + export function columns(): ExpressionFunctionDefinition< 'columns', Datatable, @@ -44,27 +64,25 @@ export function columns(): ExpressionFunctionDefinition< let result = { ...input }; if (exclude) { - const fields = exclude.split(',').map((field) => field.trim()); - const cols = contextColumns.filter((col) => !fields.includes(col.name)); - const rows = cols.length > 0 ? contextRows.map((row) => omit(row, fields)) : []; - - result = { rows, columns: cols, ...rest }; + const fields = prepareFields(exclude); + const { matched: excluded, other } = splitColumnsByFields(result.columns, fields, true); + const fieldsIds = getFieldsIds(excluded); + const rows = excluded.length ? result.rows.map((row) => omit(row, fieldsIds)) : result.rows; + result = { rows, columns: other, ...rest }; } if (include) { - const fields = include.split(',').map((field) => field.trim()); - // const columns = result.columns.filter(col => fields.includes(col.name)); + const fields = prepareFields(include); + const { matched: included } = splitColumnsByFields(result.columns, fields); + const fieldsIds = getFieldsIds(included); // Include columns in the order the user specified - const cols: DatatableColumn[] = []; + const cols = fields.reduce((includedCols, field) => { + const column = find(included, (col) => col.id === field || col.name === field); + return column ? [...includedCols, column] : includedCols; + }, []); - fields.forEach((field) => { - const column = find(result.columns, { name: field }); - if (column) { - cols.push(column); - } - }); - const rows = cols.length > 0 ? result.rows.map((row) => pick(row, fields)) : []; + const rows = cols.length ? result.rows.map((row) => pick(row, fieldsIds)) : []; result = { rows, columns: cols, ...rest }; } diff --git a/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.test.ts b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.test.ts new file mode 100644 index 0000000000000..bcc7ce888d8d4 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { WorkpadSchema } from './workpad_schema'; + +const pageOneId = 'page-1'; +const pageTwoId = 'page-2'; +const elementOneId = 'element-1'; +const elementTwoId = 'element-2'; +const elementThreeId = 'element-3'; + +const position = { + angle: 0, + height: 0, + left: 0, + parent: null, + top: 0, + width: 0, +}; +const baseWorkpad = { + colors: [], + css: '', + variables: [], + height: 0, + id: 'workpad-id', + name: 'workpad', + page: 1, + pages: [ + { + elements: [ + { + expression: 'expression', + id: elementOneId, + position, + }, + { + expression: 'expression', + id: elementTwoId, + position, + }, + ], + id: pageOneId, + style: {}, + }, + { + elements: [ + { + expression: 'expression', + id: elementThreeId, + position, + }, + ], + id: pageTwoId, + style: {}, + }, + ], + width: 0, +}; + +it('validates there are no duplicate page ids', () => { + const dupePage = { + ...baseWorkpad, + pages: [{ ...baseWorkpad.pages[0] }, { ...baseWorkpad.pages[1], id: pageOneId }], + }; + + expect(() => WorkpadSchema.validate(dupePage)).toThrowError('Page Ids are not unique'); +}); + +it('validates there are no duplicate element ids on the same page', () => { + const dupeElement = { + ...baseWorkpad, + pages: [ + { + ...baseWorkpad.pages[0], + elements: [ + { ...baseWorkpad.pages[0].elements[0] }, + { ...baseWorkpad.pages[0].elements[1], id: elementOneId }, + ], + }, + { ...baseWorkpad.pages[1] }, + ], + }; + + expect(() => WorkpadSchema.validate(dupeElement)).toThrowError('Element Ids are not unique'); +}); + +it('validates there are no duplicate element ids in the workpad', () => { + const dupeElement = { + ...baseWorkpad, + pages: [ + { + ...baseWorkpad.pages[0], + elements: [ + { ...baseWorkpad.pages[0].elements[0] }, + { ...baseWorkpad.pages[0].elements[1], id: elementOneId }, + ], + }, + { + ...baseWorkpad.pages[1], + elements: [{ ...baseWorkpad.pages[1].elements[0], id: elementOneId }], + }, + ], + }; + + expect(() => WorkpadSchema.validate(dupeElement)).toThrowError('Element Ids are not unique'); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts index 57e02367fcd3a..9bde26298185b 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts @@ -58,18 +58,41 @@ export const WorkpadVariable = schema.object({ type: schema.string(), }); -export const WorkpadSchema = schema.object({ - '@created': schema.maybe(schema.string()), - '@timestamp': schema.maybe(schema.string()), - assets: schema.maybe(schema.recordOf(schema.string(), WorkpadAssetSchema)), - colors: schema.arrayOf(schema.string()), - css: schema.string(), - variables: schema.arrayOf(WorkpadVariable), - height: schema.number(), - id: schema.string(), - isWriteable: schema.maybe(schema.boolean()), - name: schema.string(), - page: schema.number(), - pages: schema.arrayOf(WorkpadPageSchema), - width: schema.number(), -}); +export const WorkpadSchema = schema.object( + { + '@created': schema.maybe(schema.string()), + '@timestamp': schema.maybe(schema.string()), + assets: schema.maybe(schema.recordOf(schema.string(), WorkpadAssetSchema)), + colors: schema.arrayOf(schema.string()), + css: schema.string(), + variables: schema.arrayOf(WorkpadVariable), + height: schema.number(), + id: schema.string(), + isWriteable: schema.maybe(schema.boolean()), + name: schema.string(), + page: schema.number(), + pages: schema.arrayOf(WorkpadPageSchema), + width: schema.number(), + }, + { + validate: (workpad) => { + // Validate unique page ids + const pageIdsArray = workpad.pages.map((page) => page.id); + const pageIdsSet = new Set(pageIdsArray); + + if (pageIdsArray.length !== pageIdsSet.size) { + return 'Page Ids are not unique'; + } + + // Validate unique element ids + const elementIdsArray = workpad.pages + .map((page) => page.elements.map((element) => element.id)) + .flat(); + const elementIdsSet = new Set(elementIdsArray); + + if (elementIdsArray.length !== elementIdsSet.size) { + return 'Element Ids are not unique'; + } + }, + } +); diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index b699b4ff7c5cf..0eb2917c3495e 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -19,7 +19,7 @@ export const CASE_COMMENT_SAVED_OBJECT = 'cases-comments'; export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure'; /** - * If more values are added here please also add them here: x-pack/test/case_api_integration/common/fixtures/plugins + * If more values are added here please also add them here: x-pack/test/cases_api_integration/common/fixtures/plugins */ export const SAVED_OBJECT_TYPES = [ CASE_SAVED_OBJECT, diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx index 990d44584cf05..8843bed9a2b43 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx @@ -39,6 +39,8 @@ const useConnectorsMock = useConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; const useGetUrlSearchMock = jest.fn(); const useActionTypesMock = useActionTypes as jest.Mock; +const getAddConnectorFlyoutMock = jest.fn(); +const getEditConnectorFlyoutMock = jest.fn(); describe('ConfigureCases', () => { beforeAll(() => { @@ -46,6 +48,12 @@ describe('ConfigureCases', () => { actionTypeTitle: '.servicenow', iconClass: 'logoSecurity', }); + + useKibanaMock().services.triggersActionsUi.getAddConnectorFlyout = + getAddConnectorFlyoutMock.mockReturnValue(
); + + useKibanaMock().services.triggersActionsUi.getEditConnectorFlyout = + getEditConnectorFlyoutMock.mockReturnValue(
); }); beforeEach(() => { @@ -72,12 +80,12 @@ describe('ConfigureCases', () => { expect(wrapper.find('[data-test-subj="closure-options-radio-group"]').exists()).toBeTruthy(); }); - test('it does NOT render the ConnectorAddFlyout', () => { - expect(wrapper.find('ConnectorAddFlyout').exists()).toBeFalsy(); + test('it does NOT render the add connector flyout', () => { + expect(wrapper.find('[data-test-subj="add-connector-flyout"]').exists()).toBeFalsy(); }); - test('it does NOT render the ConnectorEditFlyout', () => { - expect(wrapper.find('ConnectorEditFlyout').exists()).toBeFalsy(); + test('it does NOT render the edit connector flyout"]', () => { + expect(wrapper.find('[data-test-subj="edit-connector-flyout"]').exists()).toBeFalsy(); }); test('it does NOT render the EuiCallOut', () => { @@ -176,8 +184,8 @@ describe('ConfigureCases', () => { expect(wrapper.find(ClosureOptions).prop('closureTypeSelected')).toBe('close-by-user'); // Flyouts - expect(wrapper.find('ConnectorAddFlyout').exists()).toBe(false); - expect(wrapper.find('ConnectorEditFlyout').exists()).toBe(false); + expect(wrapper.find('[data-test-subj="add-connector-flyout"]').exists()).toBe(false); + expect(wrapper.find('[data-test-subj="edit-connector-flyout"]').exists()).toBe(false); }); test('it disables correctly when the user cannot crud', () => { @@ -541,21 +549,25 @@ describe('ConfigureCases', () => { await waitFor(() => { wrapper.update(); - expect(wrapper.find('ConnectorAddFlyout').exists()).toBe(true); - expect(wrapper.find('ConnectorAddFlyout').prop('actionTypes')).toEqual([ - expect.objectContaining({ - id: '.servicenow', - }), - expect.objectContaining({ - id: '.jira', - }), - expect.objectContaining({ - id: '.resilient', - }), + expect(wrapper.find('[data-test-subj="add-connector-flyout"]').exists()).toBe(true); + expect(getAddConnectorFlyoutMock).toHaveBeenCalledWith( expect.objectContaining({ - id: '.servicenow-sir', - }), - ]); + actionTypes: [ + expect.objectContaining({ + id: '.servicenow', + }), + expect.objectContaining({ + id: '.jira', + }), + expect.objectContaining({ + id: '.resilient', + }), + expect.objectContaining({ + id: '.servicenow-sir', + }), + ], + }) + ); }); }); @@ -588,8 +600,10 @@ describe('ConfigureCases', () => { await waitFor(() => { wrapper.update(); - expect(wrapper.find('ConnectorEditFlyout').exists()).toBe(true); - expect(wrapper.find('ConnectorEditFlyout').prop('initialConnector')).toEqual(connectors[1]); + expect(wrapper.find('[data-test-subj="edit-connector-flyout"]').exists()).toBe(true); + expect(getEditConnectorFlyoutMock).toHaveBeenCalledWith( + expect.objectContaining({ initialConnector: connectors[1] }) + ); }); expect( diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx index 95c4f76eae0a2..c1ba83e324ed2 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx @@ -12,7 +12,7 @@ import { EuiCommentList, EuiCommentProps, } from '@elastic/eui'; -import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; +import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils/technical_field_names'; import classNames from 'classnames'; import { get, isEmpty } from 'lodash'; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/_index.scss b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/_index.scss index b99f34f95cd74..7ac8aa4365732 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/_index.scss +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/_index.scss @@ -3,6 +3,7 @@ @import 'components/field_data_row/index'; $panelWidthS: #{'max(20%, 225px)'}; +$panelWidthM: #{'max(30%, 300px)'}; $panelWidthL: #{'max(40%, 450px)'}; .dvExpandedRow { @@ -52,9 +53,6 @@ $panelWidthL: #{'max(40%, 450px)'}; } .dvSummaryTable { - .euiTableRow > .euiTableRowCell { - border-bottom: 0; - } .euiTableHeaderCell { display: none; } @@ -63,6 +61,11 @@ $panelWidthL: #{'max(40%, 450px)'}; .dvSummaryTable__wrapper { min-width: $panelWidthS; max-width: $panelWidthS; + + &.dvPanel__dateSummary { + min-width: $panelWidthM; + max-width: $panelWidthM; + } } .dvTopValues__wrapper { diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/boolean_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/boolean_content.tsx index b56e6ae815645..f4060bc16a39c 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/boolean_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/boolean_content.tsx @@ -6,7 +6,13 @@ */ import React, { FC, ReactNode, useMemo } from 'react'; -import { EuiBasicTable, EuiSpacer, RIGHT_ALIGNMENT, HorizontalAlignment } from '@elastic/eui'; +import { + EuiBasicTable, + EuiSpacer, + RIGHT_ALIGNMENT, + LEFT_ALIGNMENT, + HorizontalAlignment, +} from '@elastic/eui'; import { Axis, BarSeries, Chart, Settings, ScaleType } from '@elastic/charts'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -73,12 +79,13 @@ export const BooleanContent: FC = ({ config }) => { name: '', render: (_: string, summaryItem: { display: ReactNode }) => summaryItem.display, width: '25px', - align: RIGHT_ALIGNMENT as HorizontalAlignment, + align: LEFT_ALIGNMENT as HorizontalAlignment, }, { field: 'value', name: '', render: (v: string) => {v}, + align: RIGHT_ALIGNMENT as HorizontalAlignment, }, ]; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/date_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/date_content.tsx index 8d5704fc16fd5..352782b4bb301 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/date_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/date_content.tsx @@ -6,13 +6,12 @@ */ import React, { FC, ReactNode } from 'react'; -import { EuiBasicTable, HorizontalAlignment } from '@elastic/eui'; +import { EuiBasicTable, HorizontalAlignment, LEFT_ALIGNMENT, RIGHT_ALIGNMENT } from '@elastic/eui'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { RIGHT_ALIGNMENT } from '@elastic/eui'; import type { FieldDataRowProps } from '../../types/field_data_row'; import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; import { DocumentStatsTable } from './document_stats'; @@ -65,19 +64,20 @@ export const DateContent: FC = ({ config }) => { field: 'function', render: (func: string, summaryItem: { display: ReactNode }) => summaryItem.display, width: '70px', - align: RIGHT_ALIGNMENT as HorizontalAlignment, + align: LEFT_ALIGNMENT as HorizontalAlignment, }, { field: 'value', name: '', render: (v: string) => {v}, + align: RIGHT_ALIGNMENT as HorizontalAlignment, }, ]; return ( - + {summaryTableTitle} className={'dvSummaryTable'} diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/document_stats.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/document_stats.tsx index 5995b81555f9b..9805514365903 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/document_stats.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/document_stats.tsx @@ -8,7 +8,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC, ReactNode } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiBasicTable, HorizontalAlignment, RIGHT_ALIGNMENT } from '@elastic/eui'; +import { EuiBasicTable, HorizontalAlignment, LEFT_ALIGNMENT, RIGHT_ALIGNMENT } from '@elastic/eui'; import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; import { FieldDataRowProps } from '../../types'; import { roundToDecimalPlace } from '../../../utils'; @@ -20,12 +20,13 @@ const metaTableColumns = [ name: '', render: (_: string, metaItem: { display: ReactNode }) => metaItem.display, width: '25px', - align: RIGHT_ALIGNMENT as HorizontalAlignment, + align: LEFT_ALIGNMENT as HorizontalAlignment, }, { field: 'value', name: '', render: (v: string) => {v}, + align: RIGHT_ALIGNMENT as HorizontalAlignment, }, ]; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_panel.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_panel.tsx index b738dbdf67178..4246a53464f74 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_panel.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/expanded_row_panel.tsx @@ -20,7 +20,7 @@ export const ExpandedRowPanel: FC = ({ children, dataTestSubj, grow, clas = ({ config, onAddFilter }) => name: '', render: (summaryItem: { display: ReactNode }) => summaryItem.display, width: '25px', - align: RIGHT_ALIGNMENT as HorizontalAlignment, + align: LEFT_ALIGNMENT as HorizontalAlignment, }, { field: 'value', name: '', render: (v: string) => {v}, + align: RIGHT_ALIGNMENT as HorizontalAlignment, }, ]; diff --git a/x-pack/plugins/fleet/README.md b/x-pack/plugins/fleet/README.md index 80d13b28c8265..29436440fac8b 100644 --- a/x-pack/plugins/fleet/README.md +++ b/x-pack/plugins/fleet/README.md @@ -120,3 +120,12 @@ You need to have `docker` to run ingest manager api integration tests FLEET_PACKAGE_REGISTRY_DOCKER_IMAGE='docker.elastic.co/package-registry/distribution:production' FLEET_PACKAGE_REGISTRY_PORT=12345 yarn test:ftr:runner ``` +### Storybook + +Fleet contains [Storybook](https://storybook.js.org/) stories for developing UI components in isolation. To start the Storybook environment for Fleet, run the following from your `kibana` project root: + +```sh +$ yarn storybook fleet +``` + +Write stories by creating `.stories.tsx` files colocated with the components you're working on. Consult the [Storybook docs](https://storybook.js.org/docs/react/get-started/introduction) for more information. diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.stories.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.stories.tsx new file mode 100644 index 0000000000000..6736b5a30d23e --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.stories.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { addParameters } from '@storybook/react'; +import React from 'react'; + +import { FleetServerHostsFlyout as Component } from '.'; + +addParameters({ + docs: { + inlineStories: false, + }, +}); +export default { + component: Component, + title: 'Sections/Fleet/Settings', +}; + +interface Args { + width: number; +} + +const args: Args = { + width: 1200, +}; + +export const FleetServerHostsFlyout = ({ width }: Args) => { + return ( +
+ {}} + fleetServerHosts={['https://host1.fr:8220', 'https://host2-with-a-longer-name.fr:8220']} + /> +
+ ); +}; + +FleetServerHostsFlyout.args = args; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.tsx new file mode 100644 index 0000000000000..07593ffe3e6c5 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiText, + EuiLink, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, +} from '@elastic/eui'; + +import { HostsInput } from '../hosts_input'; +import { useStartServices } from '../../../../hooks'; + +import { useFleetServerHostsForm } from './use_fleet_server_host_form'; + +const FLYOUT_MAX_WIDTH = 800; + +export interface FleetServerHostsFlyoutProps { + onClose: () => void; + fleetServerHosts: string[]; +} + +export const FleetServerHostsFlyout: React.FunctionComponent = ({ + onClose, + fleetServerHosts, +}) => { + const { docLinks } = useStartServices(); + + const form = useFleetServerHostsForm(fleetServerHosts, onClose); + + return ( + + + +

+ +

+
+
+ + + + + + ), + }} + /> + + + + + + + onClose()} flush="left"> + + + + + + + + + + +
+ ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx new file mode 100644 index 0000000000000..f4dda2b059542 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { sendPutSettings, useComboInput, useStartServices } from '../../../../hooks'; +import { isDiffPathProtocol } from '../../../../../../../common'; +import { useConfirmModal } from '../../hooks/use_confirm_modal'; + +const URL_REGEX = /^(https?):\/\/[^\s$.?#].[^\s]*$/gm; + +function validateFleetServerHosts(value: string[]) { + if (value.length === 0) { + return [ + { + message: i18n.translate('xpack.fleet.settings.fleetServerHostsEmptyError', { + defaultMessage: 'At least one URL is required', + }), + }, + ]; + } + + const res: Array<{ message: string; index: number }> = []; + const hostIndexes: { [key: string]: number[] } = {}; + value.forEach((val, idx) => { + if (!val.match(URL_REGEX)) { + res.push({ + message: i18n.translate('xpack.fleet.settings.fleetServerHostsError', { + defaultMessage: 'Invalid URL', + }), + index: idx, + }); + } + const curIndexes = hostIndexes[val] || []; + hostIndexes[val] = [...curIndexes, idx]; + }); + + Object.values(hostIndexes) + .filter(({ length }) => length > 1) + .forEach((indexes) => { + indexes.forEach((index) => + res.push({ + message: i18n.translate('xpack.fleet.settings.fleetServerHostsDuplicateError', { + defaultMessage: 'Duplicate URL', + }), + index, + }) + ); + }); + + if (res.length) { + return res; + } + + if (value.length && isDiffPathProtocol(value)) { + return [ + { + message: i18n.translate( + 'xpack.fleet.settings.fleetServerHostsDifferentPathOrProtocolError', + { + defaultMessage: 'Protocol and path must be the same for each URL', + } + ), + }, + ]; + } +} + +export function useFleetServerHostsForm( + fleetServerHostsDefaultValue: string[], + onSuccess: () => void +) { + const [isLoading, setIsLoading] = React.useState(false); + const { notifications } = useStartServices(); + const { confirm } = useConfirmModal(); + + const fleetServerHostsInput = useComboInput( + 'fleetServerHostsInput', + fleetServerHostsDefaultValue, + validateFleetServerHosts + ); + + const fleetServerHostsInputValidate = fleetServerHostsInput.validate; + const validate = useCallback( + () => fleetServerHostsInputValidate(), + [fleetServerHostsInputValidate] + ); + + const submit = useCallback(async () => { + try { + if (!validate) { + return; + } + if ( + !(await confirm( + i18n.translate('xpack.fleet.settings.fleetServerHostsFlyout.confirmModalTitle', { + defaultMessage: 'Save and deploy changes?', + }), + i18n.translate('xpack.fleet.settings.fleetServerHostsFlyout.confirmModalDescription', { + defaultMessage: + 'This action will update all of your agent policies and all of your agents. Are you sure you wish to continue?', + }) + )) + ) { + return; + } + setIsLoading(true); + const settingsResponse = await sendPutSettings({ + fleet_server_hosts: fleetServerHostsInput.value, + }); + if (settingsResponse.error) { + throw settingsResponse.error; + } + notifications.toasts.addSuccess( + i18n.translate('xpack.fleet.settings.fleetServerHostsFlyout.successToastTitle', { + defaultMessage: 'Settings saved', + }) + ); + setIsLoading(false); + onSuccess(); + } catch (error) { + setIsLoading(false); + notifications.toasts.addError(error, { + title: i18n.translate('xpack.fleet.settings.fleetServerHostsFlyout.errorToastTitle', { + defaultMessage: 'An error happened while saving settings', + }), + }); + } + }, [fleetServerHostsInput.value, validate, notifications, confirm, onSuccess]); + + return { + isLoading, + submit, + fleetServerHostsInput, + }; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.stories.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.stories.tsx new file mode 100644 index 0000000000000..64ac34c52d112 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.stories.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState } from '@storybook/addons'; +import { addParameters } from '@storybook/react'; +import React from 'react'; + +import { HostsInput as Component } from '.'; + +addParameters({ + options: { + enableShortcuts: false, + }, +}); + +export default { + component: Component, + title: 'Sections/Fleet/Settings', +}; + +interface Args { + width: number; + label: string; + helpText: string; +} + +const args: Args = { + width: 250, + label: 'Demo label', + helpText: 'Demo helpText', +}; + +export const HostsInput = ({ width, label, helpText }: Args) => { + const [value, setValue] = useState([]); + return ( +
+ +
+ ); +}; + +HostsInput.args = args; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.test.tsx similarity index 99% rename from x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.test.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.test.tsx index aca3399c4af46..4d556cd2749c6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.test.tsx @@ -10,7 +10,7 @@ import { fireEvent, act } from '@testing-library/react'; import { createFleetTestRendererMock } from '../../../../../../mock'; -import { HostsInput } from './hosts_input'; +import { HostsInput } from '.'; function renderInput( value = ['http://host1.com'], diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.tsx similarity index 93% rename from x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.tsx index 30ef969aceec7..6b169a207ea73 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.tsx @@ -29,12 +29,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import type { EuiTheme } from '../../../../../../../../../../src/plugins/kibana_react/common'; -interface Props { +export interface HostInputProps { id: string; value: string[]; onChange: (newValue: string[]) => void; - label: string; - helpText: ReactNode; + label?: string; + helpText?: ReactNode; errors?: Array<{ message: string; index?: number }>; isInvalid?: boolean; } @@ -105,11 +105,13 @@ const SortableTextField: FunctionComponent = React.memo( {displayErrors(errors)} @@ -130,7 +132,7 @@ const SortableTextField: FunctionComponent = React.memo( } ); -export const HostsInput: FunctionComponent = ({ +export const HostsInput: FunctionComponent = ({ id, value: valueFromProps, onChange, @@ -231,10 +233,12 @@ export const HostsInput: FunctionComponent = ({ <> {displayErrors(indexedErrors[idx])} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx index 6caca7209e0d2..6c52475400bdc 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx @@ -18,7 +18,6 @@ import { EuiForm, EuiFormRow, EuiCode, - EuiLink, EuiPanel, EuiTextColor, } from '@elastic/eui'; @@ -31,16 +30,15 @@ import { useStartServices, useGetSettings, useInput, - sendPutSettings, useDefaultOutput, sendPutOutput, } from '../../../../../../hooks'; -import { isDiffPathProtocol, normalizeHostsForAgents } from '../../../../../../../common'; +import { normalizeHostsForAgents } from '../../../../../../../common'; import { CodeEditor } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { HostsInput } from '../hosts_input'; import { SettingsConfirmModal } from './confirm_modal'; import type { SettingsConfirmModalProps } from './confirm_modal'; -import { HostsInput } from './hosts_input'; const CodeEditorContainer = styled.div` min-height: 0; @@ -84,63 +82,6 @@ function useSettingsForm(outputId: string | undefined) { const [isLoading, setIsloading] = React.useState(false); const { notifications } = useStartServices(); - const fleetServerHostsInput = useComboInput('fleetServerHostsComboBox', [], (value) => { - if (value.length === 0) { - return [ - { - message: i18n.translate('xpack.fleet.settings.fleetServerHostsEmptyError', { - defaultMessage: 'At least one URL is required', - }), - }, - ]; - } - - const res: Array<{ message: string; index: number }> = []; - const hostIndexes: { [key: string]: number[] } = {}; - value.forEach((val, idx) => { - if (!val.match(URL_REGEX)) { - res.push({ - message: i18n.translate('xpack.fleet.settings.fleetServerHostsError', { - defaultMessage: 'Invalid URL', - }), - index: idx, - }); - } - const curIndexes = hostIndexes[val] || []; - hostIndexes[val] = [...curIndexes, idx]; - }); - - Object.values(hostIndexes) - .filter(({ length }) => length > 1) - .forEach((indexes) => { - indexes.forEach((index) => - res.push({ - message: i18n.translate('xpack.fleet.settings.fleetServerHostsDuplicateError', { - defaultMessage: 'Duplicate URL', - }), - index, - }) - ); - }); - - if (res.length) { - return res; - } - - if (value.length && isDiffPathProtocol(value)) { - return [ - { - message: i18n.translate( - 'xpack.fleet.settings.fleetServerHostsDifferentPathOrProtocolError', - { - defaultMessage: 'Protocol and path must be the same for each URL', - } - ), - }, - ]; - } - }); - const elasticsearchUrlInput = useComboInput('esHostsComboxBox', [], (value) => { const res: Array<{ message: string; index: number }> = []; const urlIndexes: { [key: string]: number[] } = {}; @@ -190,16 +131,15 @@ function useSettingsForm(outputId: string | undefined) { }); const validate = useCallback(() => { - const fleetServerHostsValid = fleetServerHostsInput.validate(); const elasticsearchUrlsValid = elasticsearchUrlInput.validate(); const additionalYamlConfigValid = additionalYamlConfigInput.validate(); - if (!fleetServerHostsValid || !elasticsearchUrlsValid || !additionalYamlConfigValid) { + if (!elasticsearchUrlsValid || !additionalYamlConfigValid) { return false; } return true; - }, [fleetServerHostsInput, elasticsearchUrlInput, additionalYamlConfigInput]); + }, [elasticsearchUrlInput, additionalYamlConfigInput]); return { isLoading, @@ -217,15 +157,10 @@ function useSettingsForm(outputId: string | undefined) { if (outputResponse.error) { throw outputResponse.error; } - const settingsResponse = await sendPutSettings({ - fleet_server_hosts: fleetServerHostsInput.value, - }); - if (settingsResponse.error) { - throw settingsResponse.error; - } + notifications.toasts.addSuccess( i18n.translate('xpack.fleet.settings.success.message', { - defaultMessage: 'Settings saved', + defaultMessage: 'Output saved', }) ); setIsloading(false); @@ -237,7 +172,6 @@ function useSettingsForm(outputId: string | undefined) { } }, inputs: { - fleetServerHosts: fleetServerHostsInput, elasticsearchUrl: elasticsearchUrlInput, additionalYamlConfig: additionalYamlConfigInput, }, @@ -245,8 +179,6 @@ function useSettingsForm(outputId: string | undefined) { } export const LegacySettingsForm: React.FunctionComponent = () => { - const { docLinks } = useStartServices(); - const settingsRequest = useGetSettings(); const settings = settingsRequest?.data?.item; const { output } = useDefaultOutput(); @@ -277,22 +209,11 @@ export const LegacySettingsForm: React.FunctionComponent = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [output]); - useEffect(() => { - if (settings) { - inputs.fleetServerHosts.setValue([...settings.fleet_server_hosts]); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [settings]); - const isUpdated = React.useMemo(() => { if (!settings || !output) { return false; } return ( - !isSameArrayValueWithNormalizedHosts( - settings.fleet_server_hosts, - inputs.fleetServerHosts.value - ) || !isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value) || (output.config_yaml || '') !== inputs.additionalYamlConfig.value ); @@ -319,26 +240,6 @@ export const LegacySettingsForm: React.FunctionComponent = () => { ); } - if ( - !isSameArrayValueWithNormalizedHosts( - settings.fleet_server_hosts, - inputs.fleetServerHosts.value - ) - ) { - tmpChanges.push( - { - type: 'fleet_server', - direction: 'removed', - urls: normalizeHosts(settings.fleet_server_hosts || []), - }, - { - type: 'fleet_server', - direction: 'added', - urls: normalizeHosts(inputs.fleetServerHosts.value), - } - ); - } - return tmpChanges; }, [settings, inputs, output, isConfirmModalVisible]); @@ -354,35 +255,6 @@ export const LegacySettingsForm: React.FunctionComponent = () => { /> - - - - - ), - }} - /> - } - /> - - { )} <> <> - +

diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx new file mode 100644 index 0000000000000..66a95a7952c35 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; + +import type { Settings } from '../../../../types'; +import { LegacySettingsForm } from '../legacy_settings_form'; + +import { SettingsSection } from './settings_section'; + +export interface SettingsPageProps { + settings: Settings; +} + +export const SettingsPage: React.FunctionComponent = ({ settings }) => { + return ( + <> + + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/settings_section.stories.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/settings_section.stories.tsx new file mode 100644 index 0000000000000..8133d5959c126 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/settings_section.stories.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { SettingsSection as Component } from './settings_section'; + +export default { + component: Component, + title: 'Sections/Fleet/Settings', +}; + +interface Args { + width: number; + fleetServerHosts: string[]; +} + +const args: Args = { + width: 1200, + fleetServerHosts: [ + 'https://myfleetserver:8220', + 'https://alongerfleetserverwithaverylongname:8220', + ], +}; + +export const SettingsSection = ({ width, fleetServerHosts }: Args) => { + return ( +
+ +
+ ); +}; + +SettingsSection.args = args; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/settings_section.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/settings_section.tsx new file mode 100644 index 0000000000000..9aef2bb3f6380 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/settings_section.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiTitle, EuiLink, EuiText, EuiSpacer, EuiBasicTable, EuiButtonEmpty } from '@elastic/eui'; +import type { EuiBasicTableColumn } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { useLink, useStartServices } from '../../../../hooks'; + +export interface SettingsSectionProps { + fleetServerHosts: string[]; +} + +export const SettingsSection: React.FunctionComponent = ({ + fleetServerHosts, +}) => { + const { docLinks } = useStartServices(); + const { getHref } = useLink(); + + const columns = useMemo((): Array> => { + return [ + { + render: (host: string) => host, + name: i18n.translate('xpack.fleet.settings.fleetServerHostUrlColumnTitle', { + defaultMessage: 'Host URL', + }), + }, + ]; + }, []); + + return ( + <> + +

+ +

+
+ + + + + + ), + }} + /> + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/hooks/use_confirm_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/hooks/use_confirm_modal.tsx new file mode 100644 index 0000000000000..b8663f8cb2977 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/hooks/use_confirm_modal.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiConfirmModal, EuiPortal } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useContext, useState } from 'react'; + +interface ModalState { + title?: React.ReactNode; + description?: React.ReactNode; + onConfirm: () => void; + onCancel: () => void; +} + +const ModalContext = React.createContext void; +}>(null); + +export function useConfirmModal() { + const context = useContext(ModalContext); + + const confirm = useCallback( + async (title: React.ReactNode, description: React.ReactNode) => { + if (context === null) { + throw new Error('Context need to be provided to use useConfirmModal'); + } + return new Promise((resolve) => { + context.showModal({ + title, + description, + onConfirm: () => resolve(true), + onCancel: () => resolve(false), + }); + }); + }, + [context] + ); + + return { + confirm, + }; +} + +export const ConfirmModalProvider: React.FunctionComponent = ({ children }) => { + const [isVisible, setIsVisible] = useState(false); + const [modal, setModal] = useState({ + onCancel: () => {}, + onConfirm: () => {}, + }); + + const showModal = useCallback(({ title, description, onConfirm, onCancel }) => { + setIsVisible(true); + setModal({ + title, + description, + onConfirm: () => { + setIsVisible(false); + onConfirm(); + }, + onCancel: () => { + setIsVisible(false); + onCancel(); + }, + }); + }, []); + + return ( + + {isVisible && ( + + + {modal.description} + + + )} + {children} + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx index 6117d3249b189..212b2d9191e24 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx @@ -5,19 +5,57 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; +import { EuiPortal } from '@elastic/eui'; +import { Router, Route, Switch, useHistory } from 'react-router-dom'; -import { useBreadcrumbs } from '../../hooks'; +import { useBreadcrumbs, useGetSettings } from '../../hooks'; +import { FLEET_ROUTING_PATHS, pagePathGetters } from '../../constants'; import { DefaultLayout } from '../../layouts'; +import { Loading } from '../../components'; -import { LegacySettingsForm } from './components/legacy_settings_form'; +import { SettingsPage } from './components/settings_page'; +import { ConfirmModalProvider } from './hooks/use_confirm_modal'; +import { FleetServerHostsFlyout } from './components/fleet_server_hosts_flyout'; export const SettingsApp = () => { useBreadcrumbs('settings'); + const history = useHistory(); + + const settings = useGetSettings(); + + const resendSettingsRequest = settings.resendRequest; + + const onCloseCallback = useCallback(() => { + resendSettingsRequest(); + history.replace(pagePathGetters.settings()[1]); + }, [history, resendSettingsRequest]); + + if (settings.isLoading || !settings.data?.item) { + return ( + + + + ); + } return ( - + + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/fleet/public/constants/page_paths.ts b/x-pack/plugins/fleet/public/constants/page_paths.ts index 821c115cb1cac..39b6e4c6da075 100644 --- a/x-pack/plugins/fleet/public/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/constants/page_paths.ts @@ -15,7 +15,8 @@ export type StaticPage = | 'policies_list' | 'enrollment_tokens' | 'data_streams' - | 'settings'; + | 'settings' + | 'settings_edit_fleet_server_hosts'; export type DynamicPage = | 'integrations_all' @@ -59,6 +60,7 @@ export const FLEET_ROUTING_PATHS = { enrollment_tokens: '/enrollment-tokens', data_streams: '/data-streams', settings: '/settings', + settings_edit_fleet_server_hosts: '/settings/edit-fleet-server-hosts', // TODO: Move this to the integrations app add_integration_to_policy: '/integrations/:pkgkey/add-integration/:integration?', @@ -147,5 +149,9 @@ export const pagePathGetters: { agent_details_logs: ({ agentId }) => [FLEET_BASE_PATH, `/agents/${agentId}/logs`], enrollment_tokens: () => [FLEET_BASE_PATH, '/enrollment-tokens'], data_streams: () => [FLEET_BASE_PATH, '/data-streams'], - settings: () => [FLEET_BASE_PATH, '/settings'], + settings: () => [FLEET_BASE_PATH, FLEET_ROUTING_PATHS.settings], + settings_edit_fleet_server_hosts: () => [ + FLEET_BASE_PATH, + FLEET_ROUTING_PATHS.settings_edit_fleet_server_hosts, + ], }; diff --git a/x-pack/plugins/fleet/public/hooks/use_input.ts b/x-pack/plugins/fleet/public/hooks/use_input.ts index e4a517dbae9c8..908c3f4f717ca 100644 --- a/x-pack/plugins/fleet/public/hooks/use_input.ts +++ b/x-pack/plugins/fleet/public/hooks/use_input.ts @@ -52,7 +52,7 @@ export function useInput(defaultValue = '', validate?: (value: string) => string export function useComboInput( id: string, - defaultValue = [], + defaultValue: string[] = [], validate?: (value: string[]) => Array<{ message: string; index?: number }> | undefined ) { const [value, setValue] = useState(defaultValue); diff --git a/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts b/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts index a08ed450b5b30..9a1aca9486c60 100644 --- a/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts +++ b/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts @@ -43,7 +43,7 @@ export const getFleetServerUsage = async ( .getSettings(soClient) .then((settings) => settings.fleet_server_hosts?.length ?? 0) .catch((err) => { - if (isBoom(error) && error.output.statusCode === 404) { + if (isBoom(err) && err.output.statusCode === 404) { return 0; } diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index c3dd408925cf0..17425050a4e10 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -17,7 +17,6 @@ import { import { FleetPlugin } from './plugin'; -export { default as apm } from 'elastic-apm-node'; export type { AgentService, ESIndexPatternService, diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index d0c73a0fe42a7..f7593e32c25c9 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -20,7 +20,7 @@ import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import type { TelemetryPluginSetup, TelemetryPluginStart } from 'src/plugins/telemetry/server'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; +import { DEFAULT_APP_CATEGORIES, SavedObjectsClient } from '../../../../src/core/server'; import type { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; import type { LicensingPluginSetup, ILicense } from '../../licensing/server'; import type { @@ -83,6 +83,7 @@ import { RouterWrappers } from './routes/security'; import { FleetArtifactsClient } from './services/artifacts'; import type { FleetRouter } from './types/request_context'; import { TelemetryEventsSender } from './telemetry/sender'; +import { setupFleet } from './services/setup'; export interface FleetSetupDeps { licensing: LicensingPluginSetup; @@ -332,8 +333,22 @@ export class FleetPlugin this.telemetryEventsSender.start(plugins.telemetry, core); + const logger = appContextService.getLogger(); + + const fleetSetupPromise = (async () => { + try { + await setupFleet( + new SavedObjectsClient(core.savedObjects.createInternalRepository()), + core.elasticsearch.client.asInternalUser + ); + } catch (error) { + logger.warn('Fleet setup failed'); + logger.warn(error); + } + })(); + return { - fleetSetupCompleted: () => Promise.resolve(), + fleetSetupCompleted: () => fleetSetupPromise, esIndexPatternService: new ESIndexPatternSavedObjectService(), packageService: { getInstallation, diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index 5f9ff51dadf65..250bfd13a84c1 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -37,6 +37,7 @@ import type { GetFullAgentConfigMapResponse, } from '../../../common'; import { defaultIngestErrorHandler } from '../../errors'; +import { incrementPackageName } from '../../services/package_policy'; export const getAgentPoliciesHandler: RequestHandler< undefined, @@ -108,6 +109,7 @@ export const createAgentPolicyHandler: RequestHandler< const esClient = context.core.elasticsearch.client.asCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; const withSysMonitoring = request.query.sys_monitoring ?? false; + try { // eslint-disable-next-line prefer-const let [agentPolicy, newSysPackagePolicy] = await Promise.all< @@ -131,6 +133,8 @@ export const createAgentPolicyHandler: RequestHandler< if (withSysMonitoring && newSysPackagePolicy !== undefined && agentPolicy !== undefined) { newSysPackagePolicy.policy_id = agentPolicy.id; newSysPackagePolicy.namespace = agentPolicy.namespace; + newSysPackagePolicy.name = await incrementPackageName(soClient, FLEET_SYSTEM_PACKAGE); + await packagePolicyService.create(soClient, esClient, newSysPackagePolicy, { user, bumpRevision: false, diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts index b39c6e7686110..ffdec9509b05c 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -18,6 +18,7 @@ import { fleetSetupHandler } from './handlers'; jest.mock('../../services/setup', () => { return { + ...jest.requireActual('../../services/setup'), setupFleet: jest.fn(), }; }); diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index fad5d93c3f5d5..60094c532b913 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -7,7 +7,7 @@ import { appContextService } from '../../services'; import type { GetFleetStatusResponse, PostFleetSetupResponse } from '../../../common'; -import { setupFleet } from '../../services/setup'; +import { formatNonFatalErrors, setupFleet } from '../../services/setup'; import { hasFleetServers } from '../../services/fleet_server'; import { defaultIngestErrorHandler } from '../../errors'; import type { FleetRequestHandler } from '../../types'; @@ -50,24 +50,8 @@ export const fleetSetupHandler: FleetRequestHandler = async (context, request, r const setupStatus = await setupFleet(soClient, esClient); const body: PostFleetSetupResponse = { ...setupStatus, - nonFatalErrors: setupStatus.nonFatalErrors.flatMap((e) => { - // JSONify the error object so it can be displayed properly in the UI - if ('error' in e) { - return { - name: e.error.name, - message: e.error.message, - }; - } else { - return e.errors.map((upgradePackagePolicyError: any) => { - return { - name: upgradePackagePolicyError.key, - message: upgradePackagePolicyError.message, - }; - }); - } - }), + nonFatalErrors: formatNonFatalErrors(setupStatus.nonFatalErrors), }; - return response.ok({ body }); } catch (error) { return defaultIngestErrorHandler({ error, response }); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 20434e8290457..856bf077b33d3 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -1195,3 +1195,30 @@ function deepMergeVars(original: any, override: any): any { return result; } + +export async function incrementPackageName( + soClient: SavedObjectsClientContract, + packageName: string +) { + // Fetch all packagePolicies having the package name + const packagePolicyData = await packagePolicyService.list(soClient, { + perPage: 1, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "${packageName}"`, + }); + + // Retrieve highest number appended to package policy name and increment it by one + const pkgPoliciesNamePattern = new RegExp(`${packageName}-(\\d+)`); + + const pkgPoliciesWithMatchingNames = packagePolicyData?.items + ? packagePolicyData.items + .filter((ds) => Boolean(ds.name.match(pkgPoliciesNamePattern))) + .map((ds) => parseInt(ds.name.match(pkgPoliciesNamePattern)![1], 10)) + .sort() + : []; + + return `${packageName}-${ + pkgPoliciesWithMatchingNames.length + ? pkgPoliciesWithMatchingNames[pkgPoliciesWithMatchingNames.length - 1] + 1 + : 1 + }`; +} diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 6cdb3abf24908..b16eae266d28c 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -73,6 +73,8 @@ export async function ensurePreconfiguredOutputs( esClient: ElasticsearchClient, outputs: PreconfiguredOutput[] ) { + const logger = appContextService.getLogger(); + if (outputs.length === 0) { return; } @@ -106,8 +108,10 @@ export async function ensurePreconfiguredOutputs( existingOutput && isPreconfiguredOutputDifferentFromCurrent(existingOutput, data); if (isCreate) { + logger.debug(`Creating output ${output.id}`); await outputService.create(soClient, data, { id, fromPreconfiguration: true }); } else if (isUpdateWithNewData) { + logger.debug(`Updating output ${output.id}`); await outputService.update(soClient, id, data, { fromPreconfiguration: true }); // Bump revision of all policies using that output if (outputData.is_default || outputData.is_default_monitoring) { @@ -335,7 +339,7 @@ export async function ensurePreconfiguredPackagesAndPolicies( await soClient .delete(AGENT_POLICY_SAVED_OBJECT_TYPE, policy!.id) // swallow error - .catch((deleteErr) => appContextService.getLogger().error(deleteErr)); + .catch((deleteErr) => logger.error(deleteErr)); throw err; } diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 7cde9c4c052d6..1c84073552e57 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -51,6 +51,9 @@ async function createSetupSideEffects( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient ): Promise { + const logger = appContextService.getLogger(); + logger.info('Beginning fleet setup'); + const { agentPolicies: policiesOrUndefined, packages: packagesOrUndefined, @@ -60,6 +63,7 @@ async function createSetupSideEffects( const policies = policiesOrUndefined ?? []; let packages = packagesOrUndefined ?? []; + logger.debug('Setting up Fleet outputs'); await Promise.all([ ensurePreconfiguredOutputs(soClient, esClient, outputsOrUndefined ?? []), settingsService.settingsSetup(soClient), @@ -68,6 +72,7 @@ async function createSetupSideEffects( const defaultOutput = await outputService.ensureDefaultOutput(soClient); if (appContextService.getConfig()?.agentIdVerificationEnabled) { + logger.debug('Setting up Fleet Elasticsearch assets'); await ensureFleetGlobalEsAssets(soClient, esClient); } @@ -91,6 +96,8 @@ async function createSetupSideEffects( ...autoUpdateablePackages.filter((pkg) => !preconfiguredPackageNames.has(pkg.name)), ]; + logger.debug('Setting up initial Fleet packages'); + const { nonFatalErrors } = await ensurePreconfiguredPackagesAndPolicies( soClient, esClient, @@ -99,11 +106,22 @@ async function createSetupSideEffects( defaultOutput ); + logger.debug('Cleaning up Fleet outputs'); await cleanPreconfiguredOutputs(soClient, outputsOrUndefined ?? []); + logger.debug('Setting up Fleet enrollment keys'); await ensureDefaultEnrollmentAPIKeysExists(soClient, esClient); + + logger.debug('Setting up Fleet Server agent policies'); await ensureFleetServerAgentPoliciesExists(soClient, esClient); + if (nonFatalErrors.length > 0) { + logger.info('Encountered non fatal errors during Fleet setup'); + formatNonFatalErrors(nonFatalErrors).forEach((error) => logger.info(JSON.stringify(error))); + } + + logger.info('Fleet setup completed'); + return { isInitialized: true, nonFatalErrors, @@ -119,6 +137,7 @@ export async function ensureFleetGlobalEsAssets( ) { const logger = appContextService.getLogger(); // Ensure Global Fleet ES assets are installed + logger.debug('Creating Fleet component template and ingest pipeline'); const globalAssetsRes = await Promise.all([ ensureDefaultComponentTemplate(esClient), ensureFleetFinalPipelineIsInstalled(esClient), @@ -141,7 +160,7 @@ export async function ensureFleetGlobalEsAssets( savedObjectsClient: soClient, pkgkey: pkgToPkgKey({ name: installation.name, version: installation.version }), esClient, - // Force install the pacakge will update the index template and the datastream write indices + // Force install the package will update the index template and the datastream write indices force: true, }).catch((err) => { logger.error( @@ -187,3 +206,27 @@ export async function ensureDefaultEnrollmentAPIKeysExists( }) ); } + +/** + * Maps the `nonFatalErrors` object returned by the setup process to a more readable + * and predictable format suitable for logging output or UI presentation. + */ +export function formatNonFatalErrors( + nonFatalErrors: SetupStatus['nonFatalErrors'] +): Array<{ name: string; message: string }> { + return nonFatalErrors.flatMap((e) => { + if ('error' in e) { + return { + name: e.error.name, + message: e.error.message, + }; + } else { + return e.errors.map((upgradePackagePolicyError: any) => { + return { + name: upgradePackagePolicyError.key, + message: upgradePackagePolicyError.message, + }; + }); + } + }); +} diff --git a/x-pack/plugins/fleet/storybook/context/index.tsx b/x-pack/plugins/fleet/storybook/context/index.tsx index e6c0726e755c2..76425540c2fbc 100644 --- a/x-pack/plugins/fleet/storybook/context/index.tsx +++ b/x-pack/plugins/fleet/storybook/context/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useMemo, useCallback } from 'react'; import type { StoryContext } from '@storybook/react'; import { createBrowserHistory } from 'history'; @@ -45,37 +45,43 @@ export const StorybookContext: React.FC<{ storyContext?: StoryContext }> = ({ const browserHistory = createBrowserHistory(); const history = new ScopedHistory(browserHistory, basepath); - const startServices: FleetStartServices = { - ...stubbedStartServices, - application: getApplication(), - chrome: getChrome(), - cloud: getCloud({ isCloudEnabled: storyContext?.args.isCloudEnabled }), - customIntegrations: { - ContextProvider: getStorybookContextProvider(), - }, - docLinks: getDocLinks(), - http: getHttp(), - i18n: { - Context: function I18nContext({ children }) { - return {children}; - }, - }, - injectedMetadata: { - getInjectedVar: () => null, - }, - notifications: getNotifications(), - share: getShare(), - uiSettings: getUiSettings(), - }; + const isCloudEnabled = storyContext?.args.isCloudEnabled; - setHttpClient(startServices.http); - setCustomIntegrations({ - getAppendCustomIntegrations: async () => [], - getReplacementCustomIntegrations: async () => { - const { integrations } = await import('./fixtures/replacement_integrations'); - return integrations; - }, - }); + const startServices: FleetStartServices = useMemo( + () => ({ + ...stubbedStartServices, + application: getApplication(), + chrome: getChrome(), + cloud: getCloud({ isCloudEnabled }), + customIntegrations: { + ContextProvider: getStorybookContextProvider(), + }, + docLinks: getDocLinks(), + http: getHttp(), + i18n: { + Context: function I18nContext({ children }) { + return {children}; + }, + }, + injectedMetadata: { + getInjectedVar: () => null, + }, + notifications: getNotifications(), + share: getShare(), + uiSettings: getUiSettings(), + }), + [isCloudEnabled] + ); + useEffect(() => { + setHttpClient(startServices.http); + setCustomIntegrations({ + getAppendCustomIntegrations: async () => [], + getReplacementCustomIntegrations: async () => { + const { integrations } = await import('./fixtures/replacement_integrations'); + return integrations; + }, + }); + }, [startServices]); const config = { enabled: true, @@ -87,7 +93,7 @@ export const StorybookContext: React.FC<{ storyContext?: StoryContext }> = ({ const extensions = {}; const kibanaVersion = '1.2.3'; - const setHeaderActionMenu = () => {}; + const setHeaderActionMenu = useCallback(() => {}, []); return ( { diff --git a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx index 9ed4047e45bd3..bb9e7ced40c53 100644 --- a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx @@ -75,8 +75,12 @@ export function SavedViewsToolbarControls(props: Props) { setModalOpen(true); }, [find, hideSavedViewMenu]); const showSavedViewMenu = useCallback(() => { + if (isSavedViewMenuOpen) { + setIsSavedViewMenuOpen(false); + return; + } setIsSavedViewMenuOpen(true); - }, [setIsSavedViewMenuOpen]); + }, [setIsSavedViewMenuOpen, isSavedViewMenuOpen]); const save = useCallback( (name: string, hasTime: boolean = false) => { const currentState = { diff --git a/x-pack/plugins/infra/public/pages/logs/settings/index_names_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/index_names_configuration_panel.tsx index d91c70f09cd3d..fa19c7e12a8a5 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/index_names_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/index_names_configuration_panel.tsx @@ -52,12 +52,12 @@ export const IndexNamesConfigurationPanel: React.FC<{ diff --git a/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_configuration_panel.tsx index 7e466ee2a6ae9..b5720f61e5e93 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_configuration_panel.tsx @@ -47,27 +47,27 @@ export const IndexPatternConfigurationPanel: React.FC<{

- + } description={ } > @@ -75,8 +75,8 @@ export const IndexPatternConfigurationPanel: React.FC<{ fullWidth label={ } {...useMemo( @@ -96,22 +96,22 @@ export const IndexPatternConfigurationPanel: React.FC<{ ); }; -const IndexPatternInlineHelpMessage = React.memo(() => { - const indexPatternManagementLinkProps = useLinkProps({ +const DataViewsInlineHelpMessage = React.memo(() => { + const dataViewsManagementLinkProps = useLinkProps({ app: 'management', - pathname: '/kibana/indexPatterns', + pathname: '/kibana/dataViews', }); return ( + dataViewsManagementLink: ( + ), diff --git a/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx b/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx index b91119b7d5625..cbc9bc477829d 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_selector.tsx @@ -40,8 +40,8 @@ export const IndexPatternSelector: React.FC<{ : [ { key: indexPatternId, - label: i18n.translate('xpack.infra.logSourceConfiguration.missingIndexPatternLabel', { - defaultMessage: `Missing index pattern {indexPatternId}`, + label: i18n.translate('xpack.infra.logSourceConfiguration.missingDataViewsLabel', { + defaultMessage: `Missing data view {indexPatternId}`, values: { indexPatternId, }, @@ -83,6 +83,6 @@ export const IndexPatternSelector: React.FC<{ }; const indexPatternSelectorPlaceholder = i18n.translate( - 'xpack.infra.logSourceConfiguration.indexPatternSelectorPlaceholder', - { defaultMessage: 'Choose an index pattern' } + 'xpack.infra.logSourceConfiguration.dataViewSelectorPlaceholder', + { defaultMessage: 'Choose a data view' } ); diff --git a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts index 31f4b96c92379..1136524cf6c8d 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts +++ b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts @@ -33,12 +33,12 @@ export const useLogIndicesFormElement = (initialValue: LogIndicesFormState) => { validate: useMemo( () => async (logIndices) => { if (logIndices == null) { - return validateStringNotEmpty('log index pattern', ''); + return validateStringNotEmpty('log data view', ''); } else if (logIndexNameReferenceRT.is(logIndices)) { return validateStringNotEmpty('log indices', logIndices.indexName); } else { const emptyStringErrors = validateStringNotEmpty( - 'log index pattern', + 'log data view', logIndices.indexPatternId ); diff --git a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx index 37262e05db5a0..740a446d74f47 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_errors.tsx @@ -58,14 +58,14 @@ export const LogSourceConfigurationFormError: React.FC<{ error: FormValidationEr return ( ); } else if (error.type === 'missing_message_field') { return ( message, }} @@ -85,16 +85,16 @@ export const LogSourceConfigurationFormError: React.FC<{ error: FormValidationEr return ( ); } else if (error.type === 'missing_index_pattern') { return ( {error.indexPatternId}, + dataViewId: {error.indexPatternId}, }} /> ); diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts index 67adb15f7cd85..e5c0e0a5e3673 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts @@ -52,11 +52,23 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setParseCsvResponse = (response?: object, error?: any) => { + const status = error ? error.status || 400 : 200; + const body = error ? JSON.stringify(error.body) : JSON.stringify(response); + + server.respondWith('POST', `${API_BASE_PATH}/parse_csv`, [ + status, + { 'Content-Type': 'application/json' }, + body, + ]); + }; + return { setLoadPipelinesResponse, setLoadPipelineResponse, setDeletePipelineResponse, setCreatePipelineResponse, + setParseCsvResponse, }; }; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/index.ts index 8b3d91dac0f51..ed88156b740bf 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/index.ts @@ -9,6 +9,7 @@ import { setup as pipelinesListSetup } from './pipelines_list.helpers'; import { setup as pipelinesCreateSetup } from './pipelines_create.helpers'; import { setup as pipelinesCloneSetup } from './pipelines_clone.helpers'; import { setup as pipelinesEditSetup } from './pipelines_edit.helpers'; +import { setup as pipelinesCreateFromCsvSetup } from './pipelines_create_from_csv.helpers'; export { nextTick, getRandomString, findTestSubject } from '@kbn/test/jest'; @@ -19,4 +20,5 @@ export const pageHelpers = { pipelinesCreate: { setup: pipelinesCreateSetup }, pipelinesClone: { setup: pipelinesCloneSetup }, pipelinesEdit: { setup: pipelinesEditSetup }, + pipelinesCreateFromCsv: { setup: pipelinesCreateFromCsvSetup }, }; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create_from_csv.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create_from_csv.helpers.ts new file mode 100644 index 0000000000000..e7de57de0e948 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create_from_csv.helpers.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; + +import { registerTestBed, TestBed, AsyncTestBedConfig } from '@kbn/test/jest'; +import { PipelinesCreateFromCsv } from '../../../public/application/sections/pipelines_create_from_csv'; +import { WithAppDependencies } from './setup_environment'; +import { getCreateFromCsvPath, ROUTES } from '../../../public/application/services/navigation'; + +const testBedConfig: AsyncTestBedConfig = { + memoryRouter: { + initialEntries: [getCreateFromCsvPath()], + componentRoutePath: ROUTES.createFromCsv, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed(WithAppDependencies(PipelinesCreateFromCsv), testBedConfig); + +export type PipelineCreateFromCsvTestBed = TestBed & { + actions: ReturnType; +}; +const createFromCsvActions = (testBed: TestBed) => { + // User Actions + + const selectCsvForUpload = (file?: File) => { + const { find } = testBed; + const csv = [file ? file : 'foo'] as any; + + act(() => { + find('csvFilePicker').simulate('change', { files: csv }); + }); + }; + + const clickProcessCsv = async () => { + const { component, find } = testBed; + + await act(async () => { + find('processFileButton').simulate('click'); + }); + + component.update(); + }; + + const uploadFile = async (file?: File) => { + selectCsvForUpload(file); + await clickProcessCsv(); + }; + + return { + selectCsvForUpload, + clickProcessCsv, + uploadFile, + }; +}; + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: createFromCsvActions(testBed), + }; +}; + +export type PipelineCreateFromCsvTestSubjects = + | 'pageTitle' + | 'documentationLink' + | 'processFileButton' + | 'csvFilePicker' + | 'errorCallout' + | 'errorDetailsMessage' + | 'pipelineMappingsJSONEditor' + | 'continueToCreate' + | 'copyToClipboard' + | 'downloadJson'; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts index 3cd768104203a..e2d9f1f8bf5f9 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts @@ -96,7 +96,7 @@ export const setup = async (): Promise => { export type PipelineListTestSubjects = | 'appTitle' | 'documentationLink' - | 'createPipelineButton' + | 'createPipelineDropdown' | 'pipelinesTable' | 'pipelineDetails' | 'pipelineDetails.title' diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx index 7ba5e44cddf61..a2c36e204cbea 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/setup_environment.tsx @@ -16,6 +16,7 @@ import { notificationServiceMock, docLinksServiceMock, scopedHistoryMock, + uiSettingsServiceMock, } from '../../../../../../src/core/public/mocks'; import { usageCollectionPluginMock } from '../../../../../../src/plugins/usage_collection/public/mocks'; @@ -41,13 +42,21 @@ const appServices = { metric: uiMetricService, documentation: documentationService, api: apiService, + fileReader: { + readFile: jest.fn((file) => file.text()), + }, notifications: notificationServiceMock.createSetupContract(), history, + uiSettings: uiSettingsServiceMock.createSetupContract(), urlGenerators: { getUrlGenerator: jest.fn().mockReturnValue({ createUrl: jest.fn(), }), }, + fileUpload: { + getMaxBytes: jest.fn().mockReturnValue(100), + getMaxBytesFormatted: jest.fn().mockReturnValue('100'), + }, }; export const setupEnvironment = () => { diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create_from_csv.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create_from_csv.test.tsx new file mode 100644 index 0000000000000..5f1230f004684 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create_from_csv.test.tsx @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act } from 'react-dom/test-utils'; + +import { API_BASE_PATH } from '../../common/constants'; + +import { setupEnvironment, pageHelpers } from './helpers'; +import { PipelineCreateFromCsvTestBed } from './helpers/pipelines_create_from_csv.helpers'; + +const { setup } = pageHelpers.pipelinesCreateFromCsv; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + EuiFilePicker: (props: any) => ( + { + props.onChange(syntheticEvent.files); + }} + /> + ), + }; +}); + +jest.mock('../../../../../src/plugins/kibana_react/public', () => { + const original = jest.requireActual('../../../../../src/plugins/kibana_react/public'); + + return { + ...original, + CodeEditorField: (props: any) => ( +

{props.value}

+ ), + }; +}); + +describe('', () => { + const { server, httpRequestsMockHelpers } = setupEnvironment(); + let testBed: PipelineCreateFromCsvTestBed; + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + }); + + describe('on component mount', () => { + test('should render the correct page header and documentation link', () => { + const { exists, find } = testBed; + + expect(exists('pageTitle')).toBe(true); + expect(find('pageTitle').text()).toEqual('Create pipeline from CSV'); + + expect(exists('documentationLink')).toBe(true); + expect(find('documentationLink').text()).toBe('Create pipeline docs'); + }); + + describe('form validation', () => { + test('should prevent form submission if file for upload is missing', async () => { + const { component, find, actions } = testBed; + + expect(find('processFileButton').props().disabled).toEqual(true); + + actions.selectCsvForUpload(); + component.update(); + + expect(find('processFileButton').props().disabled).toEqual(false); + }); + }); + + describe('form submission', () => { + const fileContent = 'Mock file content'; + + const mockFile = { + name: 'foo.csv', + text: () => Promise.resolve(fileContent), + size: fileContent.length, + } as File; + + const parsedCsv = { + processors: [ + { + set: { + field: 'foo', + if: 'ctx.bar != null', + value: '{{bar}}', + }, + }, + ], + }; + + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + + testBed.actions.selectCsvForUpload(mockFile); + + testBed.component.update(); + + httpRequestsMockHelpers.setParseCsvResponse(parsedCsv, undefined); + }); + + test('should parse csv from file upload', async () => { + const { actions, find } = testBed; + const totalRequests = server.requests.length; + + await actions.clickProcessCsv(); + + expect(server.requests.length).toBe(totalRequests + 1); + + const lastRequest = server.requests[server.requests.length - 1]; + expect(lastRequest.url).toBe(`${API_BASE_PATH}/parse_csv`); + expect(JSON.parse(JSON.parse(lastRequest.requestBody).body)).toEqual({ + copyAction: 'copy', + file: fileContent, + }); + + expect(JSON.parse(find('pipelineMappingsJSONEditor').text())).toEqual(parsedCsv); + }); + + test('should render an error message if error mapping pipeline', async () => { + const { actions, find, exists } = testBed; + + const errorTitle = 'title'; + const errorDetails = 'helpful description'; + + const error = { + status: 400, + error: 'Bad Request', + message: `${errorTitle}:${errorDetails}`, + }; + + httpRequestsMockHelpers.setParseCsvResponse(undefined, { body: error }); + + actions.selectCsvForUpload(mockFile); + await actions.clickProcessCsv(); + + expect(exists('errorCallout')).toBe(true); + expect(find('errorCallout').text()).toContain(errorTitle); + expect(find('errorCallout').text()).toContain(errorDetails); + }); + + describe('results', () => { + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + }); + + test('result buttons', async () => { + const { exists, find } = testBed; + + await testBed.actions.uploadFile(mockFile); + + expect(exists('pipelineMappingsJSONEditor')).toBe(true); + + expect(exists('continueToCreate')).toBe(true); + expect(find('continueToCreate').text()).toContain('Continue to create pipeline'); + + expect(exists('copyToClipboard')).toBe(true); + expect(find('copyToClipboard').text()).toContain('Copy JSON'); + + expect(exists('downloadJson')).toBe(true); + expect(find('downloadJson').text()).toContain('Download JSON'); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts index 19a2abb5a5a52..3f6a0f57bac34 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts @@ -58,8 +58,8 @@ describe('', () => { expect(exists('documentationLink')).toBe(true); expect(find('documentationLink').text()).toBe('Ingest Pipelines docs'); - // Verify create button exists - expect(exists('createPipelineButton')).toBe(true); + // Verify create dropdown exists + expect(exists('createPipelineDropdown')).toBe(true); // Verify table content const { tableCellsValues } = table.getMetaData('pipelinesTable'); diff --git a/x-pack/plugins/ingest_pipelines/common/types.ts b/x-pack/plugins/ingest_pipelines/common/types.ts index 303db8423d401..2053d3e22dc74 100644 --- a/x-pack/plugins/ingest_pipelines/common/types.ts +++ b/x-pack/plugins/ingest_pipelines/common/types.ts @@ -33,3 +33,8 @@ export interface PipelinesByName { on_failure?: Processor[]; }; } + +export enum FieldCopyAction { + Copy = 'copy', + Rename = 'rename', +} diff --git a/x-pack/plugins/ingest_pipelines/kibana.json b/x-pack/plugins/ingest_pipelines/kibana.json index 889559826f1f1..958aa729ccf0d 100644 --- a/x-pack/plugins/ingest_pipelines/kibana.json +++ b/x-pack/plugins/ingest_pipelines/kibana.json @@ -7,7 +7,7 @@ "name": "Stack Management", "githubTeam": "kibana-stack-management" }, - "requiredPlugins": ["management", "features", "share"], + "requiredPlugins": ["management", "features", "share", "fileUpload"], "optionalPlugins": ["security", "usageCollection"], "configPath": ["xpack", "ingest_pipelines"], "requiredBundles": ["esUiShared", "kibanaReact"] diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx index da8f74e1efae5..971a52d0b25b7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx @@ -21,7 +21,13 @@ import { SectionLoading, } from '../shared_imports'; -import { PipelinesList, PipelinesCreate, PipelinesEdit, PipelinesClone } from './sections'; +import { + PipelinesList, + PipelinesCreate, + PipelinesEdit, + PipelinesClone, + PipelinesCreateFromCsv, +} from './sections'; import { ROUTES } from './services/navigation'; export const AppWithoutRouter = () => ( @@ -30,6 +36,7 @@ export const AppWithoutRouter = () => ( + {/* Catch all */} diff --git a/x-pack/plugins/ingest_pipelines/public/application/index.tsx b/x-pack/plugins/ingest_pipelines/public/application/index.tsx index 18da9bbdc5d06..fb185537b1aca 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/index.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/index.tsx @@ -11,6 +11,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { NotificationsSetup, IUiSettingsClient } from 'kibana/public'; import { ManagementAppMountParams } from 'src/plugins/management/public'; import { SharePluginStart } from 'src/plugins/share/public'; +import type { FileUploadPluginStart } from '../../../file_upload/public'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { API_BASE_PATH } from '../../common/constants'; @@ -18,17 +19,25 @@ import { API_BASE_PATH } from '../../common/constants'; import { AuthorizationProvider } from '../shared_imports'; import { App } from './app'; -import { DocumentationService, UiMetricService, ApiService, BreadcrumbService } from './services'; +import { + DocumentationService, + UiMetricService, + ApiService, + BreadcrumbService, + FileReaderService, +} from './services'; export interface AppServices { breadcrumbs: BreadcrumbService; metric: UiMetricService; documentation: DocumentationService; api: ApiService; + fileReader: FileReaderService; notifications: NotificationsSetup; history: ManagementAppMountParams['history']; uiSettings: IUiSettingsClient; urlGenerators: SharePluginStart['urlGenerators']; + fileUpload: FileUploadPluginStart; } export interface CoreServices { diff --git a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts index b662f0a99de91..025a661477a24 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts @@ -9,7 +9,13 @@ import { CoreSetup } from 'src/core/public'; import { ManagementAppMountParams } from 'src/plugins/management/public'; import { StartDependencies } from '../types'; -import { documentationService, uiMetricService, apiService, breadcrumbService } from './services'; +import { + documentationService, + uiMetricService, + apiService, + breadcrumbService, + fileReaderService, +} from './services'; import { renderApp } from '.'; export async function mountManagementSection( @@ -31,10 +37,12 @@ export async function mountManagementSection( metric: uiMetricService, documentation: documentationService, api: apiService, + fileReader: fileReaderService, notifications, history, uiSettings: coreStart.uiSettings, urlGenerators: depsStart.share.urlGenerators, + fileUpload: depsStart.fileUpload, }; return renderApp(element, I18nContext, services, { http }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts index 215fbd032932c..bd3ab41936b29 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts @@ -12,3 +12,5 @@ export { PipelinesCreate } from './pipelines_create'; export { PipelinesEdit } from './pipelines_edit'; export { PipelinesClone } from './pipelines_clone'; + +export { PipelinesCreateFromCsv } from './pipelines_create_from_csv'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/index.ts index b44fdfa77c06a..1962ce4f30738 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/index.ts @@ -6,3 +6,4 @@ */ export { PipelinesCreate } from './pipelines_create'; +export { PipelinesCreateFromCsv } from '../pipelines_create_from_csv'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx index 5aa9205e1e1e5..a8068a6521406 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageHeader, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; @@ -55,6 +55,16 @@ export const PipelinesCreate: React.FunctionComponent { + if (sourcePipeline) { + return sourcePipeline; + } + + if (history.location.state?.sourcePipeline) { + return history.location.state.sourcePipeline as Pipeline; + } + }, [sourcePipeline, history]); + return ( <> = ({ error }) => { + return ( + +

+ +

+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create_from_csv/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create_from_csv/index.ts new file mode 100644 index 0000000000000..8549d39e85c07 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create_from_csv/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { PipelinesCreateFromCsv } from './main'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create_from_csv/instructions.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create_from_csv/instructions.tsx new file mode 100644 index 0000000000000..42c6c3d348e3f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create_from_csv/instructions.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FC } from 'react'; +import { EuiCode, EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; + +export const Instructions: FC = () => { + return ( +
+ + + +

+ + sample mappings + + ), + source: source_field, + destination: destination_field, + }} + /> +

+
+ + +

+ +

+
+ +
+
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create_from_csv/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create_from_csv/main.tsx new file mode 100644 index 0000000000000..0c2f3316661a5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create_from_csv/main.tsx @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { RouteComponentProps } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty, EuiPageHeader, EuiSpacer } from '@elastic/eui'; +import fileSaver from 'file-saver'; + +import { FieldCopyAction, Processor } from '../../../../common/types'; +import { useKibana } from '../../../shared_imports'; +import { PipelinesCsvUploader } from './pipelines_csv_uploader'; +import { PipelinesPreview } from './pipelines_preview'; +import { Error } from './error_display'; +import { Instructions } from './instructions'; + +export const PipelinesCreateFromCsv: React.FunctionComponent = ({ + history, +}) => { + const [isLoading, setIsLoading] = useState(false); + const [isUploaded, setIsUploaded] = useState(false); + const [pipelineProcessors, setPipelineProcessors] = useState([]); + const [errorInfo, setErrorInfo] = useState<{ title: string; message: string } | null>(null); + const [file, setFile] = useState(null); + + const hasError = errorInfo !== null; + + const { services } = useKibana(); + + useEffect(() => { + services.breadcrumbs.setBreadcrumbs('create'); + }, [services]); + + const onFilePickerChange = (files: FileList) => { + setErrorInfo(null); + setFile(files); + }; + + const onFileUpload = async (action: FieldCopyAction) => { + if (file != null && file.length > 0) { + await processFile(file[0], action); + } + }; + + const onUpdateProcessors = (updatedProcessors: Processor[]) => { + setPipelineProcessors(updatedProcessors); + }; + + const onDownload = () => { + const jsonBlob = new Blob([JSON.stringify(pipelineProcessors)], { type: 'application/json' }); + fileSaver.saveAs(jsonBlob, `my-mappings.json`); + }; + + const onClickToCreatePipeline = () => { + history.push({ pathname: '/create', state: { sourcePipeline: pipelineProcessors } }); + }; + + const processFile = async (csv: File, action: FieldCopyAction) => { + const maxBytes = services.fileUpload.getMaxBytes(); + + if (csv.size === 0) { + setErrorInfo({ + title: i18n.translate( + 'xpack.ingestPipelines.createFromCsv.processFile.emptyFileErrorTitle', + { + defaultMessage: 'File is empty', + } + ), + message: i18n.translate('xpack.ingestPipelines.createFromCsv.processFile.emptyFileError', { + defaultMessage: 'The file provided is empty.', + }), + }); + } else if (csv.size > maxBytes) { + setErrorInfo({ + title: i18n.translate( + 'xpack.ingestPipelines.createFromCsv.processFile.fileTooLargeErrorTitle', + { + defaultMessage: 'File too large', + } + ), + message: i18n.translate( + 'xpack.ingestPipelines.createFromCsv.processFile.fileTooLargeError', + { + defaultMessage: 'File is greater than allowed size of {maxBytes} bytes.', + values: { maxBytes }, + } + ), + }); + } else { + try { + setIsLoading(true); + setIsUploaded(false); + + const fileContents = await services.fileReader.readFile(csv, maxBytes); + const success = await fetchPipelineFromMapping(fileContents, action); + + setIsLoading(false); + if (success) { + setIsUploaded(true); + } + } catch (e) { + setIsLoading(false); + setErrorInfo({ + title: i18n.translate( + 'xpack.ingestPipelines.createFromCsv.processFile.unexpectedErrorTitle', + { + defaultMessage: 'Error reading file', + } + ), + message: i18n.translate( + 'xpack.ingestPipelines.createFromCsv.processFile.unexpectedError', + { + defaultMessage: 'The file provided could not be read.', + } + ), + }); + } + } + }; + + const fetchPipelineFromMapping = async (fileContents: string, action: FieldCopyAction) => { + const { error, data: processors } = await services.api.parseCsv({ + file: fileContents, + copyAction: action, + }); + setPipelineProcessors(processors); + + if (!!error) { + try { + const errorParts = error.message.split(':'); + setErrorInfo({ title: errorParts[0], message: errorParts[1] }); + } catch (e) { + setErrorInfo({ + title: i18n.translate( + 'xpack.ingestPipelines.createFromCsv.fetchPipeline.unexpectedErrorTitle', + { + defaultMessage: 'Something went wrong', + } + ), + message: i18n.translate( + 'xpack.ingestPipelines.createFromCsv.fetchPipeline.unexpectedErrorDetails', + { + defaultMessage: 'Unexpected error', + } + ), + }); + } + } + + return error === null; + }; + + return ( + <> + + + + } + rightSideItems={[ + + + , + ]} + /> + + + + + + {hasError && } + + + + {(!isUploaded || hasError) && ( + 0} + /> + )} + + {isUploaded && ( + + )} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create_from_csv/pipelines_csv_uploader.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create_from_csv/pipelines_csv_uploader.tsx new file mode 100644 index 0000000000000..75d249ff8c423 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create_from_csv/pipelines_csv_uploader.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FC, useState } from 'react'; +import { + EuiSpacer, + EuiFilePicker, + EuiButton, + EuiFormRow, + EuiIconTip, + EuiRadioGroup, +} from '@elastic/eui'; + +import { useKibana } from '../../../shared_imports'; +import { FieldCopyAction } from '../../../../common/types'; + +interface Props { + actionOptions: FieldCopyAction[]; + onFilePickerChange(files: FileList | null): void; + onFileUpload(action: string | null): void; + isLoading: boolean; + isUploaded: boolean; + hasError: boolean; + hasFile: boolean; +} + +function getOptions(actions: FieldCopyAction[]) { + return actions.map((action) => ({ + id: action, + label: action === FieldCopyAction.Copy ? 'Copy field name' : 'Rename field', + })); +} + +export const PipelinesCsvUploader: FC = ({ + actionOptions, + onFilePickerChange, + onFileUpload, + isLoading, + isUploaded, + hasError, + hasFile, +}) => { + const [action, setAction] = useState(FieldCopyAction.Copy); + const { services } = useKibana(); + + const maxFileSize = services.fileUpload.getMaxBytesFormatted(); + + const options = getOptions(actionOptions); + + return ( + <> + + } + > + + + + + + + Default action + + } + /> +

+ } + > + setAction(id as FieldCopyAction)} + /> +
+ + + +
+ onFileUpload(action)} + isLoading={isLoading} + isDisabled={!hasFile || isUploaded || hasError} + data-test-subj="processFileButton" + fill + > + + +
+ + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create_from_csv/pipelines_preview.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create_from_csv/pipelines_preview.tsx new file mode 100644 index 0000000000000..dda616bdec4d0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create_from_csv/pipelines_preview.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, Fragment, useState, useEffect } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiCopy, + EuiCallOut, + EuiText, + EuiButton, + EuiButtonEmpty, +} from '@elastic/eui'; +import { XJsonLang } from '@kbn/monaco'; +import { i18n } from '@kbn/i18n'; + +import { CodeEditorField } from '../../../../../../../src/plugins/kibana_react/public'; + +interface Props { + processors: object[]; + onDownload(): void; + onClickToCreatePipeline(): void; + onUpdateProcessors(processors: object[]): void; + hasError: boolean; +} + +export const PipelinesPreview: FC = ({ + processors, + onDownload, + onClickToCreatePipeline, + onUpdateProcessors, + hasError, +}) => { + const [isValidJson, setIsValidJson] = useState(true); + const [processorsJson, setProcessorsJson] = useState(''); + + useEffect(() => { + const jsonString = JSON.stringify(processors, null, 2); + setProcessorsJson(jsonString); + }, [processors]); + + const onUpdate = (updated: string) => { + setProcessorsJson(updated); + + try { + setIsValidJson(true); + const parsedJson = JSON.parse(updated); + onUpdateProcessors(parsedJson); + } catch (e) { + setIsValidJson(false); + } + }; + + return ( + + + {!hasError && ( + + +

+ +

+
+
+ )} + + + + + + + + + + + + + + + + + + + + + {(copy: () => void) => ( + + + + )} + + + + + + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx index 9f401bca5431f..65950870e7d4a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx @@ -5,19 +5,47 @@ * 2.0. */ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiEmptyPrompt, EuiLink, EuiPageContent, EuiButton } from '@elastic/eui'; +import { + EuiEmptyPrompt, + EuiLink, + EuiPageContent, + EuiButton, + EuiPopover, + EuiContextMenu, +} from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { ScopedHistory } from 'kibana/public'; import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; import { useKibana } from '../../../shared_imports'; -import { getCreatePath } from '../../services/navigation'; +import { getCreateFromCsvPath, getCreatePath } from '../../services/navigation'; export const EmptyList: FunctionComponent = () => { const { services } = useKibana(); const history = useHistory() as ScopedHistory; + const [showPopover, setShowPopover] = useState(false); + + const createMenuItems = [ + { + name: i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', { + defaultMessage: 'New pipeline', + }), + ...reactRouterNavigate(history, getCreatePath()), + 'data-test-subj': `emptyStateCreatePipelineButton`, + }, + { + name: i18n.translate( + 'xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel.createPipelineFromCsvButtonLabel', + { + defaultMessage: 'New pipeline from CSV', + } + ), + ...reactRouterNavigate(history, getCreateFromCsvPath()), + 'data-test-subj': `emptyStatecreatePipelineFromCsvButton`, + }, + ]; return ( @@ -35,7 +63,7 @@ export const EmptyList: FunctionComponent = () => {

{' '} {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptDocumentionLink', { @@ -45,16 +73,40 @@ export const EmptyList: FunctionComponent = () => {

} actions={ - setShowPopover(false)} + button={ + setShowPopover((previousBool) => !previousBool)} + > + {i18n.translate( + 'xpack.ingestPipelines.list.table.emptyCreatePipelineDropdownLabel', + { + defaultMessage: 'Create pipeline', + } + )} + + } + panelPaddingSize="none" + repositionOnScroll > - {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', { - defaultMessage: 'Create a pipeline', - })} - + + } />
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx index 95621601011f9..835ebc23666e2 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -160,7 +160,7 @@ export const PipelinesList: React.FunctionComponent = ({ description={ } rightSideItems={[ diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx index 02568efd98e46..a8b434e4fed22 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx @@ -14,6 +14,8 @@ import { EuiButton, EuiInMemoryTableProps, EuiTableFieldDataColumnType, + EuiPopover, + EuiContextMenu, } from '@elastic/eui'; import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; @@ -37,6 +39,30 @@ export const PipelineTable: FunctionComponent = ({ }) => { const { history } = useKibana().services; const [selection, setSelection] = useState([]); + const [showPopover, setShowPopover] = useState(false); + + const createMenuItems = [ + /** + * Create pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.table.createPipelineButtonLabel', { + defaultMessage: 'New pipeline', + }), + ...reactRouterNavigate(history, '/create'), + 'data-test-subj': `createNewPipeline`, + }, + /** + * Create pipeline from CSV + */ + { + name: i18n.translate('xpack.ingestPipelines.list.table.createPipelineFromCsvButtonLabel', { + defaultMessage: 'New pipeline from CSV', + }), + ...reactRouterNavigate(history, '/csv_create'), + 'data-test-subj': `createPipelineFromCsv`, + }, + ]; const tableProps: EuiInMemoryTableProps = { itemId: 'name', @@ -83,17 +109,37 @@ export const PipelineTable: FunctionComponent = ({ defaultMessage: 'Reload', })} , - setShowPopover(false)} + button={ + setShowPopover((previousBool) => !previousBool)} + > + {i18n.translate('xpack.ingestPipelines.list.table.createPipelineDropdownLabel', { + defaultMessage: 'Create pipeline', + })} + + } + panelPaddingSize="none" + repositionOnScroll > - {i18n.translate('xpack.ingestPipelines.list.table.createPipelineButtonLabel', { - defaultMessage: 'Create pipeline', - })} - , + + , ], box: { incremental: true, diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts index 6db30dd283048..e1a9bdbd4e026 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts @@ -7,7 +7,7 @@ import { HttpSetup } from 'src/core/public'; -import { Pipeline } from '../../../common/types'; +import { FieldCopyAction, Pipeline } from '../../../common/types'; import { API_BASE_PATH } from '../../../common/constants'; import { UseRequestConfig, @@ -131,6 +131,15 @@ export class ApiService { return result; } + + public async parseCsv(reqBody: { file: string; copyAction: FieldCopyAction }) { + const result = await this.sendRequest({ + path: `${API_BASE_PATH}/parse_csv`, + method: 'post', + body: JSON.stringify(reqBody), + }); + return result; + } } export const apiService = new ApiService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/file_reader.ts b/x-pack/plugins/ingest_pipelines/public/application/services/file_reader.ts new file mode 100644 index 0000000000000..cebb0658f31ec --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/file_reader.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export class FileReaderService { + public readFile(file: File, maxFileSizeBytes: any): Promise { + return new Promise((resolve, reject) => { + if (file && file.size) { + const reader = new FileReader(); + reader.readAsArrayBuffer(file); + + reader.onload = (() => { + return () => { + const decoder = new TextDecoder(); + const data = reader.result; + if (data === null || typeof data === 'string') { + return reject(); + } + const fileContents = decoder.decode(data.slice(0, maxFileSizeBytes)); + + if (fileContents === '') { + reject(); + } else { + resolve(fileContents); + } + }; + })(); + } else { + reject(); + } + }); + } +} + +export const fileReaderService = new FileReaderService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/index.ts b/x-pack/plugins/ingest_pipelines/public/application/services/index.ts index 3672af5d181fd..1f1bc29677375 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/index.ts @@ -12,3 +12,5 @@ export { uiMetricService, UiMetricService } from './ui_metric'; export { apiService, ApiService } from './api'; export { breadcrumbService, BreadcrumbService } from './breadcrumbs'; + +export { fileReaderService, FileReaderService } from './file_reader'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/navigation.ts b/x-pack/plugins/ingest_pipelines/public/application/services/navigation.ts index a0f2667a43c71..7d3e11fea3d89 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/navigation.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/navigation.ts @@ -11,6 +11,8 @@ const EDIT_PATH = 'edit'; const CREATE_PATH = 'create'; +const CREATE_FROM_CSV_PATH = 'csv_create'; + const _getEditPath = (name: string, encode = true): string => { return `${BASE_PATH}${EDIT_PATH}/${encode ? encodeURIComponent(name) : name}`; }; @@ -22,15 +24,21 @@ const _getCreatePath = (): string => { const _getClonePath = (name: string, encode = true): string => { return `${BASE_PATH}${CREATE_PATH}/${encode ? encodeURIComponent(name) : name}`; }; + const _getListPath = (name?: string): string => { return `${BASE_PATH}${name ? `?pipeline=${encodeURIComponent(name)}` : ''}`; }; +const _getCreateFromCsvPath = (): string => { + return `${BASE_PATH}${CREATE_FROM_CSV_PATH}`; +}; + export const ROUTES = { list: _getListPath(), edit: _getEditPath(':name', false), create: _getCreatePath(), clone: _getClonePath(':sourceName', false), + createFromCsv: _getCreateFromCsvPath(), }; export const getListPath = ({ @@ -43,3 +51,4 @@ export const getEditPath = ({ pipelineName }: { pipelineName: string }): string export const getCreatePath = (): string => _getCreatePath(); export const getClonePath = ({ clonedPipelineName }: { clonedPipelineName: string }): string => _getClonePath(clonedPipelineName, true); +export const getCreateFromCsvPath = (): string => _getCreateFromCsvPath(); diff --git a/x-pack/plugins/ingest_pipelines/public/types.ts b/x-pack/plugins/ingest_pipelines/public/types.ts index 784eccd462af4..e135f8fed58a7 100644 --- a/x-pack/plugins/ingest_pipelines/public/types.ts +++ b/x-pack/plugins/ingest_pipelines/public/types.ts @@ -8,6 +8,7 @@ import { ManagementSetup } from 'src/plugins/management/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { SharePluginStart, SharePluginSetup } from 'src/plugins/share/public'; +import type { FileUploadPluginStart } from '../../file_upload/public'; export interface SetupDependencies { management: ManagementSetup; @@ -17,4 +18,5 @@ export interface SetupDependencies { export interface StartDependencies { share: SharePluginStart; + fileUpload: FileUploadPluginStart; } diff --git a/x-pack/plugins/ingest_pipelines/server/lib/index.ts b/x-pack/plugins/ingest_pipelines/server/lib/index.ts new file mode 100644 index 0000000000000..7aa564f5e9645 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/lib/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { csvToIngestPipeline } from './mapper'; diff --git a/x-pack/plugins/ingest_pipelines/server/lib/mapper.test.ts b/x-pack/plugins/ingest_pipelines/server/lib/mapper.test.ts new file mode 100644 index 0000000000000..0c2f52d3c3bdf --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/lib/mapper.test.ts @@ -0,0 +1,251 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { csvToIngestPipeline } from './mapper'; +import { FieldCopyAction } from '../../common/types'; + +describe('mapper', () => { + describe('csvToIngestPipeline()', () => { + it('empty file returns empty mapping', () => { + expect(() => { + csvToIngestPipeline('', FieldCopyAction.Copy); + }).toThrow(new Error('Error reading file: The file provided is empty.')); + }); + + it('file parsing error for invalid csv', () => { + const invalidCsv = `name,number + one|1 + two.2 + fourty two,42`; + + expect(() => { + csvToIngestPipeline(invalidCsv, FieldCopyAction.Copy); + }).toThrow( + new Error( + 'Error reading file: An unexpected issue has occured during the processing of the file.' + ) + ); + }); + + it('missing the required headers errors', () => { + const noHeadersCsv = 'srcip,,,,source.address,Copying srcip to source.address'; + + expect(() => { + csvToIngestPipeline(noHeadersCsv, FieldCopyAction.Copy); + }).toThrow( + new Error( + 'Missing required headers: Include [source_field, destination_field] header(s) in the CSV file.' + ) + ); + }); + + it('unacceptable format action errors', () => { + const badFormatCsv = + 'source_field,copy_action,format_action,timestamp_format,destination_field,Notes\nsrcport,,invalid_action,,source.port,\n'; + + expect(() => { + csvToIngestPipeline(badFormatCsv, FieldCopyAction.Copy); + }).toThrow( + new Error( + 'Invalid format action [invalid_action]. The valid actions are uppercase, lowercase, to_boolean, to_integer, to_float, to_array, to_string, parse_timestamp' + ) + ); + }); + + describe('successful mapping', () => { + it('duplicate row handled', () => { + const duplciateRow = + 'source_field,copy_action,format_action,timestamp_format,destination_field,Notes\nsrcip,,,,source.address,Copying srcip to source.address\nsrcip,,,,source.address,Copying srcip to source.address\n'; + const expectedJson = { + processors: [ + { + set: { + field: 'source.address', + if: 'ctx.srcip != null', + value: '{{srcip}}', + }, + }, + ], + }; + expect(csvToIngestPipeline(duplciateRow, FieldCopyAction.Copy)).toEqual(expectedJson); + }); + + describe('timestamp formatting', () => { + it('default handling', () => { + const defaultTimestamp = + 'source_field,copy_action,format_action,timestamp_format,destination_field,Notes\nsome_timestamp,,,,@timestamp,\n'; + + const expectedJson = { + processors: [ + { + date: { + field: 'some_timestamp', + formats: ['UNIX_MS'], + timezone: 'UTC', + target_field: '@timestamp', + ignore_failure: true, + }, + }, + ], + }; + expect(csvToIngestPipeline(defaultTimestamp, FieldCopyAction.Copy)).toEqual(expectedJson); + }); + + it('specified handling', () => { + const timestampSpecifics = + 'source_field,copy_action,format_action,timestamp_format,destination_field,Notes\nsome_timestamp,,parse_timestamp,UNIX,destination_timestamp,\n'; + + const expectedJson = { + processors: [ + { + date: { + field: 'some_timestamp', + formats: ['UNIX'], + timezone: 'UTC', + target_field: 'destination_timestamp', + ignore_failure: true, + }, + }, + ], + }; + expect(csvToIngestPipeline(timestampSpecifics, FieldCopyAction.Copy)).toEqual( + expectedJson + ); + }); + }); + + describe('field copy action', () => { + it('copy', () => { + const copyFile = + 'source_field,copy_action,format_action,timestamp_format,destination_field,Notes\nts,copy,,,timestamp,\n'; + + const expectedJson = { + processors: [ + { + set: { + field: 'timestamp', + value: '{{ts}}', + if: 'ctx.ts != null', + }, + }, + ], + }; + expect(csvToIngestPipeline(copyFile, FieldCopyAction.Rename)).toEqual(expectedJson); + }); + it('rename', () => { + const renameFile = + 'source_field,copy_action,format_action,timestamp_format,destination_field,Notes\nhostip,,to_array,,host.ip,\n'; + + const expectedJson = { + processors: [ + { + rename: { + field: 'hostip', + target_field: 'host.ip', + ignore_missing: true, + }, + }, + { + append: { + field: 'host.ip', + value: [], + ignore_failure: true, + if: 'ctx.host?.ip != null', + }, + }, + ], + }; + expect(csvToIngestPipeline(renameFile, FieldCopyAction.Rename)).toEqual(expectedJson); + }); + }); + + it('successful mapping example file', () => { + const validCsv = + 'source_field,copy_action,format_action,timestamp_format,destination_field,Notes\n' + + 'srcip,,,,source.address,Copying srcip to source.address\n' + + 'new_event.srcip,,,,source.ip,\n' + + 'some_timestamp_field,,parse_timestamp,,@timestamp,\n' + + 'srcport,rename,to_integer,,source.port,\n' + + 'log_level,rename,uppercase,,log.level,\n' + + 'hostip,,to_array,,host.ip,\n'; + + const expectedJson = { + processors: [ + { + set: { + field: 'source.address', + if: 'ctx.srcip != null', + value: '{{srcip}}', + }, + }, + { + set: { + field: 'source.ip', + value: '{{new_event.srcip}}', + if: 'ctx.new_event?.srcip != null', + }, + }, + { + date: { + field: 'some_timestamp_field', + target_field: '@timestamp', + formats: ['UNIX_MS'], + timezone: 'UTC', + ignore_failure: true, + }, + }, + { + rename: { + field: 'srcport', + target_field: 'source.port', + ignore_missing: true, + }, + }, + { + convert: { + field: 'source.port', + type: 'long', + ignore_missing: true, + ignore_failure: true, + }, + }, + { + rename: { + field: 'log_level', + target_field: 'log.level', + ignore_missing: true, + }, + }, + { + uppercase: { + field: 'log.level', + ignore_missing: true, + ignore_failure: true, + }, + }, + { + set: { + field: 'host.ip', + value: '{{hostip}}', + if: 'ctx.hostip != null', + }, + }, + { + append: { + field: 'host.ip', + value: [], + ignore_failure: true, + if: 'ctx.host?.ip != null', + }, + }, + ], + }; + expect(csvToIngestPipeline(validCsv, FieldCopyAction.Copy)).toEqual(expectedJson); + }); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/server/lib/mapper.ts b/x-pack/plugins/ingest_pipelines/server/lib/mapper.ts new file mode 100644 index 0000000000000..86a2a886156d8 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/lib/mapper.ts @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import Papa from 'papaparse'; + +import { FieldCopyAction, Pipeline, Processor } from '../../common/types'; + +const REQUIRED_CSV_HEADERS = ['source_field', 'destination_field']; + +const FORMAT_ACTIONS = [ + 'uppercase', + 'lowercase', + 'to_boolean', + 'to_integer', + 'to_float', + 'to_array', + 'to_string', + 'parse_timestamp', +] as const; + +type FormatAction = typeof FORMAT_ACTIONS[number]; +type TimeStampFormat = 'UNIX' | 'UNIX_MS' | 'ISO8601' | 'TAI64N'; + +interface Mapping { + source_field: string; + destination_field?: string; + copy_action?: FieldCopyAction; + format_action?: FormatAction; + timestamp_format?: TimeStampFormat; +} + +interface Row extends Mapping { + notes?: string; + [key: string]: unknown; // allow unknown columns +} + +export function csvToIngestPipeline(file: string, copyAction: FieldCopyAction) { + if (file.trim().length === 0) { + throw new Error( + i18n.translate('xpack.ingestPipelines.csvToIngestPipeline.error.emptyFileErrors', { + defaultMessage: 'Error reading file: The file provided is empty.', + }) + ); + } + + const fileData = parseAndValidate(file); + const mapping = convertCsvToMapping(fileData, copyAction); + return generatePipeline(mapping); +} + +function parseAndValidate(file: string) { + const config: Papa.ParseConfig = { + header: true, + skipEmptyLines: true, + }; + + const { data, errors, meta } = Papa.parse(file, config); + if (errors.length > 0) { + throw new Error( + i18n.translate('xpack.ingestPipelines.mapToIngestPipeline.error.parseErrors', { + defaultMessage: + 'Error reading file: An unexpected issue has occured during the processing of the file.', + }) + ); + } + + const missingHeaders = REQUIRED_CSV_HEADERS.reduce((acc, header) => { + if (meta.fields.includes(header)) { + return acc; + } + return [...acc, header]; + }, []); + + if (missingHeaders.length > 0) { + throw new Error( + i18n.translate('xpack.ingestPipelines.mapToIngestPipeline.error.missingHeaders', { + defaultMessage: 'Missing required headers: Include [{missing}] header(s) in the CSV file.', + values: { missing: missingHeaders.join(', ') }, + }) + ); + } + + return data; +} + +function convertCsvToMapping(rows: Row[], copyFieldAction: FieldCopyAction) { + const mapping = new Map(); + + if (rows.length < 1) { + return mapping; + } + + for (const row of rows) { + if (!row.source_field || !row.source_field.trim()) { + // Skip rows that don't have a source field + continue; + } + if ( + (!row.destination_field || !row.destination_field.trim()) && + (!row.format_action || !row.format_action.trim()) + ) { + // Skip if no destination field and no format field provided since it's possible to reformat a source field by itself + continue; + } + + const source = row.source_field.trim(); + let destination = row.destination_field && row.destination_field.trim(); + const copyAction = (row.copy_action && row.copy_action.trim()) || copyFieldAction; + let formatAction = row.format_action && (row.format_action.trim() as FormatAction); + let timestampFormat = row.timestamp_format && (row.timestamp_format.trim() as TimeStampFormat); + + if (destination === '@timestamp' && !Boolean(timestampFormat)) { + // If @timestamp is the destination and the user does not specify how to format the conversion, convert it to UNIX_MS + formatAction = 'parse_timestamp'; + timestampFormat = 'UNIX_MS'; + } else if (!destination && formatAction) { + // If the destination field is empty but a format action is provided, then assume we're formating the source field. + destination = source; + } + + if (formatAction && !FORMAT_ACTIONS.includes(formatAction)) { + const formatActions = FORMAT_ACTIONS.join(', '); + throw new Error( + i18n.translate('xpack.ingestPipelines.mapToIngestPipeline.error.invalidFormatAction', { + defaultMessage: + 'Invalid format action [{ formatAction }]. The valid actions are {formatActions}', + values: { formatAction, formatActions }, + }) + ); + } + + mapping.set(`${source}+${destination}`, { + source_field: source, + destination_field: destination, + copy_action: copyAction as FieldCopyAction, + format_action: formatAction as FormatAction, + timestamp_format: timestampFormat as TimeStampFormat, + }); + } + + return mapping; +} + +function generatePipeline(mapping: Map) { + const processors: Processor[] = []; + for (const [, row] of mapping) { + if (hasSameName(row) && !row.format_action) continue; + + const source = row.source_field; + const dest = row.destination_field; + + // Copy/Rename + if (dest && `parse_timestamp` !== row.format_action) { + let processor = {}; + if ('copy' === row.copy_action) { + processor = { + set: { + field: dest, + value: `{{${source}}}`, + if: fieldPresencePredicate(source), + }, + }; + } else { + processor = { + rename: { + field: source, + target_field: dest, + ignore_missing: true, + }, + }; + } + processors.push(processor); + } + + if (row.format_action) { + // Modify the source_field if there's no destination_field (no rename, just a type change) + const affectedField = dest || source; + + let type = ''; + if ('to_boolean' === row.format_action) type = 'boolean'; + else if ('to_integer' === row.format_action) type = 'long'; + else if ('to_string' === row.format_action) type = 'string'; + else if ('to_float' === row.format_action) type = 'float'; + + let processor: Processor | undefined; + + if (type) { + processor = { + convert: { + field: affectedField, + type, + ignore_missing: true, + ignore_failure: true, + }, + }; + } else if ('uppercase' === row.format_action || 'lowercase' === row.format_action) { + processor = { + [row.format_action]: { + field: affectedField, + ignore_missing: true, + ignore_failure: true, + }, + }; + } else if ('to_array' === row.format_action) { + processor = { + append: { + field: affectedField, + value: [], + ignore_failure: true, + if: fieldPresencePredicate(affectedField), + }, + }; + } else if ('parse_timestamp' === row.format_action) { + processor = { + date: { + field: source, + target_field: dest, + formats: [row.timestamp_format], + timezone: 'UTC', + ignore_failure: true, + }, + }; + } + + if (processor) { + processors.push(processor!); + } + } + } + return { processors } as Pipeline; +} + +function fieldPresencePredicate(field: string) { + if ('@timestamp' === field) { + return "ctx.containsKey('@timestamp')"; + } + + const fieldPath = field.split('.'); + if (fieldPath.length === 1) { + return `ctx.${field} != null`; + } + + return `ctx.${fieldPath.join('?.')} != null`; +} + +function hasSameName(row: Mapping) { + return row.source_field === row.destination_field; +} diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts index 6601bc3b4e7f8..aec90d2c3a2eb 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts @@ -18,3 +18,5 @@ export { registerDeleteRoute } from './delete'; export { registerSimulateRoute } from './simulate'; export { registerDocumentsRoute } from './documents'; + +export { registerParseCsvRoute } from './parse_csv'; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/parse_csv.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/parse_csv.ts new file mode 100644 index 0000000000000..43bd94a52c056 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/parse_csv.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, TypeOf, Type } from '@kbn/config-schema'; + +import { API_BASE_PATH } from '../../../common/constants'; +import { FieldCopyAction } from '../../../common/types'; +import { csvToIngestPipeline } from '../../lib'; +import { RouteDependencies } from '../../types'; + +const bodySchema = schema.object({ + file: schema.string(), + copyAction: schema.string() as Type, +}); + +type ReqBody = TypeOf; + +export const registerParseCsvRoute = ({ router }: RouteDependencies): void => { + router.post( + { + path: `${API_BASE_PATH}/parse_csv`, + validate: { + body: bodySchema, + }, + }, + async (contxt, req, res) => { + const { file, copyAction } = req.body; + try { + const result = csvToIngestPipeline(file, copyAction); + return res.ok({ body: result }); + } catch (error) { + return res.badRequest({ body: error.message }); + } + } + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/index.ts index 9f6175736c414..d3d74b31c1013 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/index.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/index.ts @@ -15,6 +15,7 @@ import { registerDeleteRoute, registerSimulateRoute, registerDocumentsRoute, + registerParseCsvRoute, } from './api'; export class ApiRoutes { @@ -26,5 +27,6 @@ export class ApiRoutes { registerDeleteRoute(dependencies); registerSimulateRoute(dependencies); registerDocumentsRoute(dependencies); + registerParseCsvRoute(dependencies); } } diff --git a/x-pack/plugins/ingest_pipelines/tsconfig.json b/x-pack/plugins/ingest_pipelines/tsconfig.json index de9a8362e8c6b..0bb8031adcf77 100644 --- a/x-pack/plugins/ingest_pipelines/tsconfig.json +++ b/x-pack/plugins/ingest_pipelines/tsconfig.json @@ -18,6 +18,7 @@ { "path": "../licensing/tsconfig.json" }, { "path": "../features/tsconfig.json" }, { "path": "../security/tsconfig.json" }, + { "path": "../file_upload/tsconfig.json" }, { "path": "../../../src/plugins/es_ui_shared/tsconfig.json"}, { "path": "../../../src/plugins/kibana_react/tsconfig.json"}, { "path": "../../../src/plugins/management/tsconfig.json"}, diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap index bf8497e686e96..40eb546dfc208 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap +++ b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap @@ -3,7 +3,6 @@ exports[`DatatableComponent it renders actions column when there are row actions 1`] = ` { if (isEmpty) { return ( - + ); @@ -339,11 +335,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { }); return ( - + { }); describe('single group', () => { - it('should render the non-editable state', async () => { + it('should render the non-editable state and optional label', async () => { mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -172,8 +172,11 @@ describe('LayerPanel', () => { }); const { instance } = await mountWithProvider(); + const group = instance.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]'); expect(group).toHaveLength(1); + const optionalLabel = instance.find('[data-test-subj="lnsGroup_optional"]').first(); + expect(optionalLabel.text()).toEqual('Optional'); }); it('should render the group with a way to add a new column', async () => { 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 51d880e8f7c1c..6af3d88b17d41 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 @@ -14,6 +14,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, + EuiText, EuiIconTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -385,7 +386,7 @@ export function LayerPanel( {groups.map((group, groupIndex) => { const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; - + const isOptional = !group.required; return ( } + labelAppend={ + isOptional ? ( + + {i18n.translate('xpack.lens.editorFrame.optionalDimensionLabel', { + defaultMessage: 'Optional', + })} + + ) : null + } labelType="legend" key={group.groupId} isInvalid={isMissing} diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 563e10bb03abd..2960e74efe97f 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -364,6 +364,10 @@ export class Embeddable } }; + private onRender: ExpressionWrapperProps['onRender$'] = () => { + this.renderComplete.dispatchComplete(); + }; + /** * * @param {HTMLElement} domNode @@ -371,6 +375,7 @@ export class Embeddable */ render(domNode: HTMLElement | Element) { this.domNode = domNode; + super.render(domNode as HTMLElement); if (!this.savedVis || !this.isInitialized || this.isDestroyed) { return; } @@ -378,6 +383,10 @@ export class Embeddable this.input.onLoad(true); } + this.domNode.setAttribute('data-shared-item', ''); + + this.renderComplete.dispatchInProgress(); + const executionContext = { type: 'lens', name: this.savedVis.visualizationType ?? '', @@ -400,6 +409,7 @@ export class Embeddable searchSessionId={this.externalSearchContext.searchSessionId} handleEvent={this.handleEvent} onData$={this.updateActiveData} + onRender$={this.onRender} interactive={!input.disableTriggers} renderMode={input.renderMode} syncColors={input.syncColors} diff --git a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx index 3de914d13d69d..4c57bf7e7a53a 100644 --- a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx @@ -35,6 +35,7 @@ export interface ExpressionWrapperProps { data: unknown, inspectorAdapters?: Partial | undefined ) => void; + onRender$: () => void; renderMode?: RenderMode; syncColors?: boolean; hasCompatibleActions?: ReactExpressionRendererProps['hasCompatibleActions']; @@ -106,6 +107,7 @@ export function ExpressionWrapper({ interactive, searchSessionId, onData$, + onRender$, renderMode, syncColors, hasCompatibleActions, @@ -132,6 +134,7 @@ export function ExpressionWrapper({ searchContext={searchContext} searchSessionId={searchSessionId} onData$={onData$} + onRender$={onRender$} inspectorAdapters={lensInspector.adapters} renderMode={renderMode} syncColors={syncColors} diff --git a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx index 5e4c2f2684062..4300208109b76 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, useEffect, useMemo, useState } from 'react'; +import React, { FC, useMemo } from 'react'; import { Chart, ElementClickListener, @@ -396,23 +396,8 @@ export const HeatmapComponent: FC = ({ const MemoizedChart = React.memo(HeatmapComponent); export function HeatmapChartReportable(props: HeatmapRenderProps) { - const [state, setState] = useState({ - isReady: false, - }); - - // It takes a cycle for the chart to render. This prevents - // reporting from printing a blank chart placeholder. - useEffect(() => { - setState({ isReady: true }); - }, [setState]); - return ( - + ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimensions_editor_helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimensions_editor_helpers.tsx index 876711c5d6aaf..ce8c7d61bfc16 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimensions_editor_helpers.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimensions_editor_helpers.tsx @@ -161,7 +161,12 @@ export const DimensionEditorTabs = ({ tabs }: { tabs: DimensionEditorTab[] }) => > {tabs.map(({ id, enabled, state, onClick, label }) => { return enabled ? ( - + {label} ) : null; diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx index db70a7c8508e5..baa0a5adc3b70 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx @@ -87,8 +87,6 @@ describe('metric_expression', () => { ).toMatchInlineSnapshot(` { ).toMatchInlineSnapshot(` { ).toMatchInlineSnapshot(` { ).toMatchInlineSnapshot(` { ).toMatchInlineSnapshot(` { ).toMatchInlineSnapshot(` { ).toMatchInlineSnapshot(` ( - + ); @@ -84,11 +80,7 @@ export function MetricChart({ : Number(Number(row[accessor]).toFixed(3)).toString(); return ( - +
{value} diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx index 1b30e6c7fd932..5693d67546f99 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx @@ -106,6 +106,7 @@ export const metricVisualization: Visualization = { accessors: props.state.accessor ? [{ columnId: props.state.accessor }] : [], supportsMoreColumns: !props.state.accessor, filterOperations: (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number', + required: true, }, ], }; diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 070448978f4ef..05b9ca9c34168 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -6,7 +6,7 @@ */ import { uniq } from 'lodash'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText } from '@elastic/eui'; import { @@ -224,13 +224,6 @@ export function PieComponent( }, }); - const [isReady, setIsReady] = useState(false); - // It takes a cycle for the chart to render. This prevents - // reporting from printing a blank chart placeholder. - useEffect(() => { - setIsReady(true); - }, []); - const hasNegative = firstTable.rows.some((row) => { const value = row[metricColumn.id]; return typeof value === 'number' && value < 0; @@ -247,11 +240,7 @@ export function PieComponent( if (isEmpty) { return ( - + ); @@ -278,12 +267,7 @@ export function PieComponent( }; return ( - + { - test('renders reporting data attributes when ready', () => { - const component = mount(Hello!); - - const reportingEl = component.find('[data-shared-item]').first(); - expect(reportingEl.prop('data-render-complete')).toBeTruthy(); - expect(reportingEl.prop('data-shared-item')).toBeTruthy(); - }); - - test('does not render data attributes when not ready', () => { - const component = mount( - Hello! - ); - - const reportingEl = component.find('[data-shared-item]').first(); - expect(reportingEl.prop('data-render-complete')).toBeFalsy(); - expect(reportingEl.prop('data-shared-item')).toBeTruthy(); - }); - - test('increments counter in data attribute for each render', () => { - const component = mount(Hello!); - - let reportingEl = component.find('[data-shared-item]').first(); - expect(reportingEl.prop('data-rendering-count')).toEqual(1); - - component.setProps({ children: 'Hello2!' }); - - reportingEl = component.find('[data-shared-item]').first(); - expect(reportingEl.prop('data-rendering-count')).toEqual(2); - }); - test('renders child content', () => { - const component = mount( - Hello! - ); - - expect(component.text()).toEqual('Hello!'); - }); - - test('defaults to rendered', () => { const component = mount(Hello!); - const reportingEl = component.find('[data-shared-item]').first(); - expect(reportingEl.prop('data-render-complete')).toBeTruthy(); - expect(reportingEl.prop('data-shared-item')).toBeTruthy(); - }); - - test('renders title and description for reporting, if provided', () => { - const component = mount( - - Hello! - - ); - const reportingEl = component.find('[data-shared-item]').first(); - - expect(reportingEl.prop('data-title')).toEqual('shazam!'); - expect(reportingEl.prop('data-description')).toEqual('Description'); + expect(component.text()).toEqual('Hello!'); }); test('renders style', () => { const component = mount( Hello! ); - const reportingEl = component.find('[data-shared-item]').first(); + const el = component.find('.lnsVisualizationContainer').first(); - expect(reportingEl.prop('style')).toEqual({ color: 'blue' }); + expect(el.prop('style')).toEqual({ color: 'blue' }); }); test('combines class names with container class', () => { const component = mount( Hello! ); - const reportingEl = component.find('[data-shared-item]').first(); + const el = component.find('.lnsVisualizationContainer').first(); - expect(reportingEl.prop('className')).toEqual('myClass lnsVisualizationContainer'); + expect(el.prop('className')).toEqual('myClass lnsVisualizationContainer'); }); }); diff --git a/x-pack/plugins/lens/public/visualization_container.tsx b/x-pack/plugins/lens/public/visualization_container.tsx index 89f7f3eb0d61e..a9020278db235 100644 --- a/x-pack/plugins/lens/public/visualization_container.tsx +++ b/x-pack/plugins/lens/public/visualization_container.tsx @@ -7,44 +7,18 @@ import './visualization_container.scss'; -import React, { useRef } from 'react'; +import React from 'react'; import classNames from 'classnames'; -interface Props extends React.HTMLAttributes { - isReady?: boolean; - reportTitle?: string; - reportDescription?: string; -} - -/** - * This is a convenience component that wraps rendered Lens visualizations. It adds reporting - * attributes (data-shared-item, data-render-complete, and data-title). - */ export function VisualizationContainer({ - isReady = true, - reportTitle, - reportDescription, children, className, ...rest -}: Props) { - const counterRef = useRef(0); - counterRef.current++; - const attributes: Partial<{ 'data-title': string; 'data-description': string }> = {}; - if (reportTitle) { - attributes['data-title'] = reportTitle; - } - if (reportDescription) { - attributes['data-description'] = reportDescription; - } +}: React.HTMLAttributes) { return (
{children} diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index a16537d3aae94..60d0fe85ed546 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -7,7 +7,7 @@ import './expression.scss'; -import React, { useState, useEffect, useRef } from 'react'; +import React, { useRef } from 'react'; import ReactDOM from 'react-dom'; import { Chart, @@ -207,21 +207,8 @@ function getIconForSeriesType(seriesType: SeriesType): IconType { const MemoizedChart = React.memo(XYChart); export function XYChartReportable(props: XYChartRenderProps) { - const [isReady, setIsReady] = useState(false); - - // It takes a cycle for the XY chart to render. This prevents - // reporting from printing a blank chart placeholder. - useEffect(() => { - setIsReady(true); - }, [setIsReady]); - return ( - + ); diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index d3d461e8f8741..8ac9076919dd9 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -30,7 +30,7 @@ export class XyVisualization { const { getXyChartRenderer, getXyVisualization } = await import('../async_services'); const [, { charts, fieldFormats }] = await core.getStartServices(); const palettes = await charts.palettes.getPalettes(); - + const useLegacyTimeAxis = core.uiSettings.get(LEGACY_TIME_AXIS); expressions.registerRenderer( getXyChartRenderer({ formatFactory, @@ -38,10 +38,10 @@ export class XyVisualization { chartsActiveCursorService: charts.activeCursor, paletteService: palettes, timeZone: getTimeZone(core.uiSettings), - useLegacyTimeAxis: core.uiSettings.get(LEGACY_TIME_AXIS), + useLegacyTimeAxis, }) ); - return getXyVisualization({ paletteService: palettes, fieldFormats }); + return getXyVisualization({ paletteService: palettes, fieldFormats, useLegacyTimeAxis }); }); } } diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index 7d1bd64abe906..033e324a5d02d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -19,6 +19,7 @@ describe('#toExpression', () => { const xyVisualization = getXyVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), fieldFormats: fieldFormatsServiceMock.createStartContract(), + useLegacyTimeAxis: false, }); let mockDatasource: ReturnType; let frame: ReturnType; diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 0c3fa21708263..029cfe8ecbe40 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -40,6 +40,7 @@ const fieldFormatsMock = fieldFormatsServiceMock.createStartContract(); const xyVisualization = getXyVisualization({ paletteService: paletteServiceMock, fieldFormats: fieldFormatsMock, + useLegacyTimeAxis: false, }); describe('xy_visualization', () => { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 2f3ec7e2723d4..0ba74f4b8bb3a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -98,9 +98,11 @@ function getDescription(state?: State) { export const getXyVisualization = ({ paletteService, fieldFormats, + useLegacyTimeAxis, }: { paletteService: PaletteRegistry; fieldFormats: FieldFormatsStart; + useLegacyTimeAxis: boolean; }): Visualization => ({ id: 'lnsXY', @@ -573,7 +575,7 @@ export const getXyVisualization = ({ renderToolbar(domElement, props) { render( - + , domElement ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx index e0a30bdb2c511..4ce1667ee1008 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { XYLayerConfig, AxesSettingsConfig, AxisExtentConfig } from '../../../common/expressions'; -import { ToolbarPopover, useDebouncedValue } from '../../shared_components'; +import { ToolbarPopover, useDebouncedValue, TooltipWrapper } from '../../shared_components'; import { isHorizontalChart } from '../state_helpers'; import { EuiIconAxisBottom } from '../../assets/axis_bottom'; import { EuiIconAxisLeft } from '../../assets/axis_left'; @@ -104,6 +104,11 @@ export interface AxisSettingsPopoverProps { hasBarOrAreaOnAxis: boolean; hasPercentageAxis: boolean; dataBounds?: { min: number; max: number }; + + /** + * Toggle the visibility of legacy axis settings when using the new multilayer time axis + */ + useMultilayerTimeAxis?: boolean; } const popoverConfig = ( @@ -210,6 +215,7 @@ export const AxisSettingsPopover: React.FunctionComponent { const isHorizontal = layers?.length ? isHorizontalChart(layers) : false; const config = popoverConfig(axis, isHorizontal); @@ -314,30 +320,41 @@ export const AxisSettingsPopover: React.FunctionComponent - - value === orientation)!.id} - onChange={(optionId) => { - const newOrientation = axisOrientationOptions.find(({ id }) => id === optionId)!.value; - setOrientation(axis, newOrientation); - }} - /> - + > + value === orientation)!.id} + onChange={(optionId) => { + const newOrientation = axisOrientationOptions.find( + ({ id }) => id === optionId + )!.value; + setOrientation(axis, newOrientation); + }} + /> + + {setEndzoneVisibility && ( <> diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx index cef4a5f01ce8a..6a43be64ec1d4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx @@ -176,8 +176,10 @@ function hasPercentageAxis(axisGroups: GroupsConfiguration, groupId: string, sta ); } -export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProps) { - const { state, setState, frame } = props; +export const XyToolbar = memo(function XyToolbar( + props: VisualizationToolbarProps & { useLegacyTimeAxis?: boolean } +) { + const { state, setState, frame, useLegacyTimeAxis } = props; const shouldRotate = state?.layers.length ? isHorizontalChart(state.layers) : false; const axisGroups = getAxesConfiguration(state?.layers, shouldRotate, frame.activeData); @@ -327,6 +329,28 @@ export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProp [setState, state] ); + const filteredBarLayers = state?.layers.filter((layer) => layer.seriesType.includes('bar')); + const chartHasMoreThanOneBarSeries = + filteredBarLayers.length > 1 || + filteredBarLayers.some((layer) => layer.accessors.length > 1 || layer.splitAccessor); + + const isTimeHistogramModeEnabled = state?.layers.some( + ({ xAccessor, layerId, seriesType, splitAccessor }) => { + if (!xAccessor) { + return false; + } + const xAccessorOp = props.frame.datasourceLayers[layerId].getOperationForColumnId(xAccessor); + return ( + getScaleType(xAccessorOp, ScaleType.Linear) === ScaleType.Time && + xAccessorOp?.isBucketed && + (seriesType.includes('stacked') || !splitAccessor) && + (seriesType.includes('stacked') || + !seriesType.includes('bar') || + !chartHasMoreThanOneBarSeries) + ); + } + ); + return ( @@ -487,6 +511,9 @@ export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProp setEndzoneVisibility={onChangeEndzoneVisiblity} hasBarOrAreaOnAxis={false} hasPercentageAxis={false} + useMultilayerTimeAxis={ + isTimeHistogramModeEnabled && !useLegacyTimeAxis && !shouldRotate + } /> { diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 21fee2e3dfdce..afd6519a100c1 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -97,14 +97,20 @@ export interface ILayer { isFittable(): Promise; isIncludeInFitToBounds(): boolean; getLicensedFeatures(): Promise; - getCustomIconAndTooltipContent(): CustomIconAndTooltipContent; + + /* + * ILayer.getLayerIcon returns layer icon and associated state. + * isTocIcon is set to true when icon is generated for Table of Contents. + * Icons in Table of Contents may contain additional layer status, for example, indicate when a layer has incomplete results. + */ + getLayerIcon(isTocIcon: boolean): LayerIcon; getDescriptor(): LayerDescriptor; getGeoFieldNames(): string[]; getStyleMetaDescriptorFromLocalFeatures(): Promise; isBasemap(order: number): boolean; } -export type CustomIconAndTooltipContent = { +export type LayerIcon = { icon: ReactElement; tooltipContent?: string | null; areResultsTrimmed?: boolean; @@ -243,7 +249,7 @@ export class AbstractLayer implements ILayer { return this._descriptor.label ? this._descriptor.label : ''; } - getCustomIconAndTooltipContent(): CustomIconAndTooltipContent { + getLayerIcon(isTocIcon: boolean): LayerIcon { return { icon: , }; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx index 80da6ceecf3a6..d675b4bfb2c4d 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx @@ -28,7 +28,7 @@ import { DataRequestContext } from '../../../../actions'; import { IVectorStyle, VectorStyle } from '../../../styles/vector/vector_style'; import { ISource } from '../../../sources/source'; import { IVectorSource } from '../../../sources/vector_source'; -import { AbstractLayer, CustomIconAndTooltipContent } from '../../layer'; +import { AbstractLayer, LayerIcon } from '../../layer'; import { InnerJoin } from '../../../joins/inner_join'; import { AbstractVectorLayer, @@ -78,7 +78,7 @@ export class GeoJsonVectorLayer extends AbstractVectorLayer { : super.getBounds(syncContext); } - getCustomIconAndTooltipContent(): CustomIconAndTooltipContent { + getLayerIcon(isTocIcon: boolean): LayerIcon { const featureCollection = this._getSourceFeatureCollection(); if (!featureCollection || featureCollection.features.length === 0) { @@ -101,12 +101,12 @@ export class GeoJsonVectorLayer extends AbstractVectorLayer { const sourceDataRequest = this.getSourceDataRequest(); const { tooltipContent, areResultsTrimmed, isDeprecated } = - this.getSource().getSourceTooltipContent(sourceDataRequest); + this.getSource().getSourceStatus(sourceDataRequest); return { icon: isDeprecated ? ( ) : ( - this.getCurrentStyle().getIcon() + this.getCurrentStyle().getIcon(isTocIcon && areResultsTrimmed) ), tooltipContent, areResultsTrimmed, diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/__snapshots__/mvt_vector_layer.test.tsx.snap b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/__snapshots__/mvt_vector_layer.test.tsx.snap index 8b4911342f841..faae8892a2a85 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/__snapshots__/mvt_vector_layer.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/__snapshots__/mvt_vector_layer.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`getCustomIconAndTooltipContent Layers with non-elasticsearch sources should display icon 1`] = ` +exports[`getLayerIcon Layers with non-elasticsearch sources should display icon 1`] = ` { }); }); -describe('getCustomIconAndTooltipContent', () => { +describe('getLayerIcon', () => { it('Layers with non-elasticsearch sources should display icon', async () => { const layer: MvtVectorLayer = createLayer({}, {}); - const iconAndTooltipContent = layer.getCustomIconAndTooltipContent(); + const iconAndTooltipContent = layer.getLayerIcon(false); const component = shallow(iconAndTooltipContent.icon); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx index 237bab80ce758..79cfc8ce00a54 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx @@ -15,7 +15,6 @@ import { Feature } from 'geojson'; import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { parse as parseUrl } from 'url'; -import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; import { IVectorStyle, VectorStyle } from '../../../styles/vector/vector_style'; import { LAYER_TYPE, SOURCE_DATA_REQUEST_ID, SOURCE_TYPES } from '../../../../../common/constants'; import { @@ -35,7 +34,7 @@ import { import { MVTSingleLayerVectorSourceConfig } from '../../../sources/mvt_single_layer_vector_source/types'; import { ESSearchSource } from '../../../sources/es_search_source'; import { canSkipSourceUpdate } from '../../../util/can_skip_fetch'; -import { CustomIconAndTooltipContent } from '../../layer'; +import { LayerIcon } from '../../layer'; const ES_MVT_META_LAYER_NAME = 'meta'; const ES_MVT_HITS_TOTAL_RELATION = 'hits.total.relation'; @@ -79,12 +78,11 @@ export class MvtVectorLayer extends AbstractVectorLayer { return this._descriptor.__metaFromTiles || []; } - getCustomIconAndTooltipContent(): CustomIconAndTooltipContent { - const icon = this.getCurrentStyle().getIcon(); + getLayerIcon(isTocIcon: boolean): LayerIcon { if (!this.getSource().isESSource()) { // Only ES-sources can have a special meta-tile, not 3rd party vector tile sources return { - icon, + icon: this.getCurrentStyle().getIcon(false), tooltipContent: null, areResultsTrimmed: false, }; @@ -102,7 +100,7 @@ export class MvtVectorLayer extends AbstractVectorLayer { if (this.getSource().getType() !== SOURCE_TYPES.ES_SEARCH) { // aggregation ES sources are never trimmed return { - icon, + icon: this.getCurrentStyle().getIcon(false), tooltipContent: null, areResultsTrimmed: false, }; @@ -111,7 +109,7 @@ export class MvtVectorLayer extends AbstractVectorLayer { const maxResultWindow = this._getMaxResultWindow(); if (maxResultWindow === undefined) { return { - icon, + icon: this.getCurrentStyle().getIcon(false), tooltipContent: null, areResultsTrimmed: false, }; @@ -127,7 +125,7 @@ export class MvtVectorLayer extends AbstractVectorLayer { return NO_RESULTS_ICON_AND_TOOLTIPCONTENT; } - const isIncomplete: boolean = tileMetaFeatures.some((tileMeta: TileMetaFeature) => { + const areResultsTrimmed: boolean = tileMetaFeatures.some((tileMeta: TileMetaFeature) => { if (tileMeta?.properties?.[ES_MVT_HITS_TOTAL_RELATION] === 'gte') { return tileMeta?.properties?.[ES_MVT_HITS_TOTAL_VALUE] >= maxResultWindow + 1; } else { @@ -136,8 +134,8 @@ export class MvtVectorLayer extends AbstractVectorLayer { }); return { - icon, - tooltipContent: isIncomplete + icon: this.getCurrentStyle().getIcon(isTocIcon && areResultsTrimmed), + tooltipContent: areResultsTrimmed ? i18n.translate('xpack.maps.tiles.resultsTrimmedMsg', { defaultMessage: `Results limited to {count} documents.`, values: { @@ -150,7 +148,7 @@ export class MvtVectorLayer extends AbstractVectorLayer { count: totalFeaturesCount.toLocaleString(), }, }), - areResultsTrimmed: isIncomplete, + areResultsTrimmed, }; } @@ -347,7 +345,11 @@ export class MvtVectorLayer extends AbstractVectorLayer { ['==', ['get', ES_MVT_HITS_TOTAL_RELATION], 'gte'], ['>=', ['get', ES_MVT_HITS_TOTAL_VALUE], maxResultWindow + 1], ]); - mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-color', euiThemeVars.euiColorWarning); + mbMap.setPaintProperty( + tooManyFeaturesLayerId, + 'line-color', + this.getCurrentStyle().getPrimaryColor() + ); mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-width', 3); mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-dasharray', [2, 1]); mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-opacity', this.getAlpha()); @@ -487,12 +489,6 @@ export class MvtVectorLayer extends AbstractVectorLayer { } async getStyleMetaDescriptorFromLocalFeatures(): Promise { - const style = this.getCurrentStyle(); - if (!style) { - return null; - } - - const metaFromTiles = this._getMetaFromTiles(); - return await style.pluckStyleMetaFromTileMeta(metaFromTiles); + return await this.getCurrentStyle().pluckStyleMetaFromTileMeta(this._getMetaFromTiles()); } } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 59078c076433e..2c6bd64e1bc65 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -48,7 +48,7 @@ import { VectorStyleRequestMeta, } from '../../../../common/descriptor_types'; import { IVectorSource } from '../../sources/vector_source'; -import { CustomIconAndTooltipContent, ILayer } from '../layer'; +import { LayerIcon, ILayer } from '../layer'; import { InnerJoin } from '../../joins/inner_join'; import { IField } from '../../fields/field'; import { DataRequestContext } from '../../../actions'; @@ -270,8 +270,8 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { return true; } - getCustomIconAndTooltipContent(): CustomIconAndTooltipContent { - throw new Error('Should implement AbstractVectorLayer#getCustomIconAndTooltipContent'); + getLayerIcon(isTocIcon: boolean): LayerIcon { + throw new Error('Should implement AbstractVectorLayer#getLayerIcon'); } getLayerTypeIconName() { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.test.ts index 81472e0eacd6e..d827dab8d8533 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.test.ts @@ -8,7 +8,7 @@ import { ESGeoLineSource } from './es_geo_line_source'; import { DataRequest } from '../../util/data_request'; -describe('getSourceTooltipContent', () => { +describe('getSourceStatus', () => { const geoLineSource = new ESGeoLineSource({ indexPatternId: 'myindex', geoField: 'myGeoField', @@ -28,8 +28,7 @@ describe('getSourceTooltipContent', () => { totalEntities: 70, }, }); - const { tooltipContent, areResultsTrimmed } = - geoLineSource.getSourceTooltipContent(sourceDataRequest); + const { tooltipContent, areResultsTrimmed } = geoLineSource.getSourceStatus(sourceDataRequest); expect(areResultsTrimmed).toBe(false); expect(tooltipContent).toBe('Found 70 tracks.'); }); @@ -46,8 +45,7 @@ describe('getSourceTooltipContent', () => { totalEntities: 5000, }, }); - const { tooltipContent, areResultsTrimmed } = - geoLineSource.getSourceTooltipContent(sourceDataRequest); + const { tooltipContent, areResultsTrimmed } = geoLineSource.getSourceStatus(sourceDataRequest); expect(areResultsTrimmed).toBe(true); expect(tooltipContent).toBe('Results limited to first 1,000 tracks of ~5,000.'); }); @@ -64,8 +62,7 @@ describe('getSourceTooltipContent', () => { totalEntities: 70, }, }); - const { tooltipContent, areResultsTrimmed } = - geoLineSource.getSourceTooltipContent(sourceDataRequest); + const { tooltipContent, areResultsTrimmed } = geoLineSource.getSourceStatus(sourceDataRequest); expect(areResultsTrimmed).toBe(true); expect(tooltipContent).toBe('Found 70 tracks. 10 of 70 tracks are incomplete.'); }); @@ -82,8 +79,7 @@ describe('getSourceTooltipContent', () => { totalEntities: 5000, }, }); - const { tooltipContent, areResultsTrimmed } = - geoLineSource.getSourceTooltipContent(sourceDataRequest); + const { tooltipContent, areResultsTrimmed } = geoLineSource.getSourceStatus(sourceDataRequest); expect(areResultsTrimmed).toBe(true); expect(tooltipContent).toBe( 'Results limited to first 1,000 tracks of ~5,000. 10 of 1,000 tracks are incomplete.' diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx index 19a561caf094a..089ae8ab319fc 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx @@ -318,7 +318,7 @@ export class ESGeoLineSource extends AbstractESAggSource { }; } - getSourceTooltipContent(sourceDataRequest?: DataRequest) { + getSourceStatus(sourceDataRequest?: DataRequest) { const featureCollection = sourceDataRequest ? sourceDataRequest.getData() : null; const meta = sourceDataRequest ? (sourceDataRequest.getMeta() as ESGeoLineSourceResponseMeta) diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 488cafd07b694..f938e691c8e4f 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -60,7 +60,7 @@ import { } from '../../../../../../../src/plugins/data/common'; import { ImmutableSourceProperty, SourceEditorArgs } from '../source'; import { IField } from '../../fields/field'; -import { GeoJsonWithMeta, SourceTooltipConfig } from '../vector_source'; +import { GeoJsonWithMeta, SourceStatus } from '../vector_source'; import { ITiledSingleLayerVectorSource } from '../tiled_single_layer_vector_source'; import { ITooltipProperty } from '../../tooltips/tooltip_property'; import { DataRequest } from '../../util/data_request'; @@ -670,7 +670,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; } - getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig { + getSourceStatus(sourceDataRequest?: DataRequest): SourceStatus { const meta = sourceDataRequest ? sourceDataRequest.getMeta() : null; if (!meta) { // no tooltip content needed when there is no feature collection or meta diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap index bafb3172bbc11..513e3d4148efd 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap @@ -11,6 +11,12 @@ exports[`scaling form should disable clusters option when clustering is not supp id="xpack.maps.esSearch.scaleTitle" values={Object {}} /> +
- - - +
+
- - - +
{ + state: State = { + isPopoverOpen: false, + }; + + _togglePopover = () => { + this.setState((prevState) => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); + }; + + _closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + }; + + _renderContent() { + return ( +
+ +
+
{this.props.mvtOptionLabel}
+
+

+ +

+

+ +

+
+ +
{this.props.clustersOptionLabel}
+
+

+ +

+

+ +

+
+ +
{this.props.limitOptionLabel}
+
+

+ +

+

+ +

+
+
+
+
+ ); + } + + render() { + return ( + + } + isOpen={this.state.isPopoverOpen} + closePopover={this._closePopover} + repositionOnScroll + ownFocus + > + {this._renderContent()} + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.tsx index 896b493507322..91ef4c5ed9ec7 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.tsx @@ -26,6 +26,7 @@ import { } from '../../../../../common/constants'; import { loadIndexSettings } from './load_index_settings'; import { OnSourceChangeArgs } from '../../source'; +import { ScalingDocumenationPopover } from './scaling_documenation_popover'; interface Props { filterByMapBounds: boolean; @@ -117,6 +118,26 @@ export class ScalingForm extends Component { this._closeModal(); }; + _getLimitOptionLabel() { + return i18n.translate('xpack.maps.source.esSearch.limitScalingLabel', { + defaultMessage: 'Limit results to {maxResultWindow}', + values: { maxResultWindow: this.state.maxResultWindow }, + }); + } + + _getClustersOptionLabel() { + return i18n.translate('xpack.maps.source.esSearch.clusterScalingLabel', { + defaultMessage: 'Show clusters when results exceed {maxResultWindow}', + values: { maxResultWindow: this.state.maxResultWindow }, + }); + } + + _getMvtOptionLabel() { + return i18n.translate('xpack.maps.source.esSearch.useMVTVectorTiles', { + defaultMessage: 'Use vector tiles', + }); + } + _renderModal() { if (!this.state.showModal || this.state.nextScalingType === undefined) { return null; @@ -164,10 +185,7 @@ export class ScalingForm extends Component { const clusteringRadio = ( this._onScalingTypeSelect(SCALING_TYPES.CLUSTERS)} disabled={!this.props.supportsClustering} @@ -206,6 +224,12 @@ export class ScalingForm extends Component {
+
@@ -213,31 +237,19 @@ export class ScalingForm extends Component {
+ this._onScalingTypeSelect(SCALING_TYPES.MVT)} + /> + {this._renderClusteringRadio()} this._onScalingTypeSelect(SCALING_TYPES.LIMIT)} /> - {this._renderClusteringRadio()} - - this._onScalingTypeSelect(SCALING_TYPES.MVT)} - /> -
diff --git a/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts b/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts index 4de29fde1253c..1f9455e01afa0 100644 --- a/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/geojson_file_source/geojson_file_source.ts @@ -125,7 +125,7 @@ export class GeoJsonFileSource extends AbstractVectorSource { return true; } - getSourceTooltipContent() { + getSourceStatus() { return { tooltipContent: (this._descriptor as GeojsonFileSourceDescriptor).tooltipContent, areResultsTrimmed: (this._descriptor as GeojsonFileSourceDescriptor).areResultsTrimmed, diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx index 387bb9c3ca1ff..b2e846a24a2fe 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx @@ -208,7 +208,7 @@ export class MVTSingleLayerVectorSource return false; } - getSourceTooltipContent() { + getSourceStatus() { return { tooltipContent: null, areResultsTrimmed: false }; } diff --git a/x-pack/plugins/maps/public/classes/sources/table_source/table_source.ts b/x-pack/plugins/maps/public/classes/sources/table_source/table_source.ts index bb2b670ae9249..c48a25219874a 100644 --- a/x-pack/plugins/maps/public/classes/sources/table_source/table_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/table_source/table_source.ts @@ -23,7 +23,7 @@ import { BoundsRequestMeta, GeoJsonWithMeta, IVectorSource, - SourceTooltipConfig, + SourceStatus, } from '../vector_source'; import { DataRequest } from '../../util/data_request'; import { InlineField } from '../../fields/inline_field'; @@ -196,7 +196,7 @@ export class TableSource extends AbstractVectorSource implements ITermJoinSource throw new Error('TableSource cannot be used as a left-layer in a term join'); } - getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig { + getSourceStatus(sourceDataRequest?: DataRequest): SourceStatus { throw new Error('must add tooltip content'); } diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx index 7042374296bbe..0d6795fa4be0d 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -20,7 +20,7 @@ import { } from '../../../../common/descriptor_types'; import { DataRequest } from '../../util/data_request'; -export interface SourceTooltipConfig { +export interface SourceStatus { tooltipContent: string | null; areResultsTrimmed: boolean; isDeprecated?: boolean; @@ -74,7 +74,7 @@ export interface IVectorSource extends ISource { hasTooltipProperties(): boolean; getSupportedShapeTypes(): Promise; isBoundsAware(): boolean; - getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig; + getSourceStatus(sourceDataRequest?: DataRequest): SourceStatus; getTimesliceMaskFieldName(): Promise; supportsFeatureEditing(): Promise; getDefaultFields(): Promise>>; @@ -171,7 +171,7 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; } - getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig { + getSourceStatus(sourceDataRequest?: DataRequest): SourceStatus { return { tooltipContent: null, areResultsTrimmed: false }; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.tsx.snap index f7dea92a8a0b7..656a4eb6ca599 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.tsx.snap @@ -40,6 +40,7 @@ exports[`Renders SymbolIcon 1`] = ` fill="#ff0000" key="airfield-15#ff0000rgb(106,173,213)" stroke="rgb(106,173,213)" + style={Object {}} symbolId="airfield-15" /> `; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx index 37d80068a6b08..cec1e8ed40aa2 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Component } from 'react'; +import React, { Component, CSSProperties } from 'react'; // @ts-expect-error import { getMakiSymbolSvg, styleSvg, buildSrcUrl } from '../../symbol_utils'; @@ -13,6 +13,7 @@ interface Props { symbolId: string; fill?: string; stroke?: string; + style: CSSProperties; } interface State { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx index b836a6d1e4d0d..333ba932dc6f3 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { CSSProperties } from 'react'; import { CircleIcon } from './circle_icon'; import { LineIcon } from './line_icon'; @@ -13,6 +13,7 @@ import { PolygonIcon } from './polygon_icon'; import { SymbolIcon } from './symbol_icon'; interface Props { + borderStyle?: CSSProperties; fillColor?: string; isPointsOnly: boolean; isLinesOnly: boolean; @@ -20,11 +21,19 @@ interface Props { symbolId?: string; } -export function VectorIcon({ fillColor, isPointsOnly, isLinesOnly, strokeColor, symbolId }: Props) { +export function VectorIcon({ + borderStyle = {}, + fillColor, + isPointsOnly, + isLinesOnly, + strokeColor, + symbolId, +}: Props) { if (isLinesOnly) { const style = { stroke: strokeColor, strokeWidth: '4px', + ...borderStyle, }; return ; } @@ -33,6 +42,7 @@ export function VectorIcon({ fillColor, isPointsOnly, isLinesOnly, strokeColor, stroke: strokeColor, strokeWidth: '1px', fill: fillColor, + ...borderStyle, }; if (!isPointsOnly) { @@ -49,6 +59,7 @@ export function VectorIcon({ fillColor, isPointsOnly, isLinesOnly, strokeColor, symbolId={symbolId} fill={fillColor} stroke={strokeColor} + style={borderStyle} /> ); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index 0f17cfb18446a..5afd05366ab1d 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -6,7 +6,7 @@ */ import _ from 'lodash'; -import React, { ReactElement } from 'react'; +import React, { CSSProperties, ReactElement } from 'react'; import { FeatureIdentifier, Map as MbMap } from '@kbn/mapbox-gl'; import { FeatureCollection } from 'geojson'; import { StyleProperties, VectorStyleEditor } from './components/vector_style_editor'; @@ -91,9 +91,10 @@ export interface IVectorStyle extends IStyle { mapColors: string[] ): Promise<{ hasChanges: boolean; nextStyleDescriptor?: VectorStyleDescriptor }>; isTimeAware(): boolean; - getIcon(): ReactElement; + getPrimaryColor(): string; + getIcon(showIncompleteIndicator: boolean): ReactElement; hasLegendDetails: () => Promise; - renderLegendDetails: () => ReactElement; + renderLegendDetails: () => ReactElement; clearFeatureState: (featureCollection: FeatureCollection, mbMap: MbMap, sourceId: string) => void; setFeatureStateAndStyleProps: ( featureCollection: FeatureCollection, @@ -700,7 +701,17 @@ export class VectorStyle implements IVectorStyle { : (this._iconStyleProperty as StaticIconProperty).getOptions().value; } - _getIconFromGeometryTypes(isLinesOnly: boolean, isPointsOnly: boolean) { + getPrimaryColor() { + const primaryColorKey = this._getIsLinesOnly() + ? VECTOR_STYLES.LINE_COLOR + : VECTOR_STYLES.FILL_COLOR; + return extractColorFromStyleProperty(this._descriptor.properties[primaryColorKey], 'grey'); + } + + getIcon(showIncompleteIndicator: boolean) { + const isLinesOnly = this._getIsLinesOnly(); + const isPointsOnly = this._getIsPointsOnly(); + let strokeColor; if (isLinesOnly) { strokeColor = extractColorFromStyleProperty( @@ -720,8 +731,17 @@ export class VectorStyle implements IVectorStyle { 'grey' ); + const borderStyle: CSSProperties = showIncompleteIndicator + ? { + borderColor: this.getPrimaryColor(), + borderStyle: 'dashed', + borderWidth: '1px', + } + : {}; + return ( { return this.getDynamicPropertiesArray().filter((styleProperty) => { const styleName = styleProperty.getStyleName(); diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/header.test.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/header.test.tsx index 4a5449506feea..e48e6224d02d6 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/header.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/header.test.tsx @@ -14,7 +14,7 @@ const layerMock = { getDisplayName: async () => { return 'myLayerName'; }, - getCustomIconAndTooltipContent: () => { + getLayerIcon: () => { return { icon: mockIcon, }; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/header.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/header.tsx index 0ac0f57b259a2..793dc966642d4 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/header.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/header.tsx @@ -50,9 +50,9 @@ export class Header extends Component { return; } const layerName = await layer.getDisplayName(); - const customIconAndTooltipContent = layer.getCustomIconAndTooltipContent(); + const { icon } = layer.getLayerIcon(false); if (this._isMounted) { - this.setState({ layerIcon: customIconAndTooltipContent.icon, layerName }); + this.setState({ layerIcon: icon, layerName }); } } diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx index eb703eadeecfb..61f2ab2a6a4ca 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx @@ -100,17 +100,11 @@ export class TOCEntryButton extends Component { values: { minZoom, maxZoom }, }); } else { - const customIconAndTooltipContent = this.props.layer.getCustomIconAndTooltipContent(); - if (customIconAndTooltipContent) { - icon = customIconAndTooltipContent.icon; - if (!customIconAndTooltipContent.areResultsTrimmed) { - tooltipContent = customIconAndTooltipContent.tooltipContent; - } else { - footnotes.push({ - icon: , - message: customIconAndTooltipContent.tooltipContent, - }); - } + const { icon: layerIcon, tooltipContent: layerTooltipContent } = + this.props.layer.getLayerIcon(true); + icon = layerIcon; + if (layerTooltipContent) { + tooltipContent = layerTooltipContent; } if (this.props.isUsingSearch && this.props.layer.getQueryableIndexPatternIds().length) { diff --git a/x-pack/plugins/maps/public/index.ts b/x-pack/plugins/maps/public/index.ts index 19850e004e6b8..3370b963f548e 100644 --- a/x-pack/plugins/maps/public/index.ts +++ b/x-pack/plugins/maps/public/index.ts @@ -36,7 +36,7 @@ export type { ImmutableSourceProperty, SourceEditorArgs } from './classes/source export type { Attribution } from '../common/descriptor_types'; export type { BoundsRequestMeta, - SourceTooltipConfig, + SourceStatus, } from './classes/sources/vector_source/vector_source'; export type { IField } from './classes/fields/field'; export type { LayerWizard, RenderWizardArguments } from './classes/layers/layer_wizard_registry'; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index/advanced.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index/advanced.js index 2f76e9b5cc8bb..ed86a4bb58048 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/index/advanced.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index/advanced.js @@ -23,8 +23,6 @@ import { AlertsCallout } from '../../../alerts/callout'; export const AdvancedIndex = ({ indexSummary, metrics, alerts, ...props }) => { const metricsToShow = [ - metrics.index_1, - metrics.index_2, metrics.index_3, metrics.index_4, metrics.index_total, diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js index 9bdaa513998b5..15a297f3cac73 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js @@ -31,7 +31,6 @@ export const Index = ({ ...props }) => { const metricsToShow = [ - metrics.index_mem, metrics.index_size, metrics.index_search_request_rate, metrics.index_request_rate, diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js index 5d95aa8bec676..30ddcf09ede26 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js @@ -27,8 +27,6 @@ export const AdvancedNode = ({ nodeSummary, metrics, alerts, ...props }) => { metrics.node_gc_time, metrics.node_jvm_mem, metrics.node_cpu_utilization, - metrics.node_index_1, - metrics.node_index_2, metrics.node_index_3, metrics.node_index_4, metrics.node_index_time, diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js index 0b03f1077f9cb..0f545de7960d5 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js @@ -43,7 +43,6 @@ export const Node = ({ nodeSummary, metrics, logs, alerts, nodeId, clusterUuid, */ const metricsToShow = [ metrics.node_jvm_mem, - metrics.node_mem, metrics.node_total_io, metrics.node_cpu_metric, metrics.node_load_average, diff --git a/x-pack/plugins/monitoring/server/lib/cluster/__fixtures__/clusters.json b/x-pack/plugins/monitoring/server/lib/cluster/__fixtures__/clusters.json index 49a82492a2301..71e27d81ef9b6 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/__fixtures__/clusters.json +++ b/x-pack/plugins/monitoring/server/lib/cluster/__fixtures__/clusters.json @@ -69,13 +69,6 @@ }, "segments": { "count": 9, - "memory_in_bytes": 14272, - "terms_memory_in_bytes": 10251, - "stored_fields_memory_in_bytes": 2808, - "term_vectors_memory_in_bytes": 0, - "norms_memory_in_bytes": 320, - "points_memory_in_bytes": 9, - "doc_values_memory_in_bytes": 884, "index_writer_memory_in_bytes": 0, "version_map_memory_in_bytes": 0, "fixed_bit_set_memory_in_bytes": 0, @@ -417,13 +410,6 @@ }, "segments": { "count": 27, - "memory_in_bytes": 120198, - "terms_memory_in_bytes": 85510, - "stored_fields_memory_in_bytes": 8480, - "term_vectors_memory_in_bytes": 0, - "norms_memory_in_bytes": 4928, - "points_memory_in_bytes": 1020, - "doc_values_memory_in_bytes": 20260, "index_writer_memory_in_bytes": 174805, "version_map_memory_in_bytes": 16836, "fixed_bit_set_memory_in_bytes": 104, diff --git a/x-pack/plugins/monitoring/server/lib/details/__snapshots__/get_metrics.test.js.snap b/x-pack/plugins/monitoring/server/lib/details/__snapshots__/get_metrics.test.js.snap index 842372f633086..f6c5079091a5d 100644 --- a/x-pack/plugins/monitoring/server/lib/details/__snapshots__/get_metrics.test.js.snap +++ b/x-pack/plugins/monitoring/server/lib/details/__snapshots__/get_metrics.test.js.snap @@ -1516,7 +1516,7 @@ Object { exports[`getMetrics and getSeries should return metrics with object structure for metric 1`] = ` Object { - "index_1": Array [ + "index_3": Array [ Object { "bucket_size": "10 mins", "data": Array [ @@ -1871,14 +1871,14 @@ Object { ], "metric": Object { "app": "elasticsearch", - "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards.", - "field": "index_stats.total.segments.memory_in_bytes", + "description": "Heap memory used by Fixed Bit Sets (e.g., deeply nested documents). This is a part of Lucene Total.", + "field": "index_stats.total.segments.fixed_bit_set_memory_in_bytes", "format": "0.0 b", "hasCalculation": false, "isDerivative": false, - "label": "Lucene Total", + "label": "Fixed Bitsets", "metricAgg": "max", - "title": "Index Memory - Lucene 1", + "title": "Index Memory - Lucene", "units": "B", }, "timeRange": Object { @@ -2240,750 +2240,12 @@ Object { ], "metric": Object { "app": "elasticsearch", - "description": "Heap memory used by Stored Fields (e.g., _source). This is a part of Lucene Total.", - "field": "index_stats.total.segments.stored_fields_memory_in_bytes", + "description": "Heap memory used by Versioning (e.g., updates and deletes). This is NOT a part of Lucene Total.", + "field": "index_stats.total.segments.version_map_memory_in_bytes", "format": "0.0 b", "hasCalculation": false, "isDerivative": false, - "label": "Stored Fields", - "metricAgg": "max", - "title": "Index Memory", - "units": "B", - }, - "timeRange": Object { - "max": 1499054399999, - "min": 1498968000000, - }, - }, - Object { - "bucket_size": "10 mins", - "data": Array [ - Array [ - 1498968000000, - 2.1666666666667, - ], - Array [ - 1498968600000, - 2.3166666666667, - ], - Array [ - 1498969200000, - 2.3666666666667, - ], - Array [ - 1498969800000, - 2.5, - ], - Array [ - 1498970400000, - 2.6166666666667, - ], - Array [ - 1498971000000, - 2.85, - ], - Array [ - 1498971600000, - 2.8666666666667, - ], - Array [ - 1498972200000, - 3.0833333333333, - ], - Array [ - 1498972800000, - 1.6666666666667, - ], - Array [ - 1498973400000, - 0.11666666666667, - ], - Array [ - 1498974000000, - 0.13333333333333, - ], - Array [ - 1498974600000, - 0.43333333333333, - ], - Array [ - 1498975200000, - 1.15, - ], - Array [ - 1498975800000, - 1.1333333333333, - ], - Array [ - 1498976400000, - 1.15, - ], - Array [ - 1498977000000, - 1.05, - ], - Array [ - 1498977600000, - 1.1833333333333, - ], - Array [ - 1498978200000, - 0.91666666666667, - ], - Array [ - 1498978800000, - 0.95, - ], - Array [ - 1498979400000, - 1.0166666666667, - ], - Array [ - 1498980000000, - 1.0333333333333, - ], - Array [ - 1498980600000, - 1.05, - ], - Array [ - 1498981200000, - 1.0333333333333, - ], - Array [ - 1498981800000, - 1.0833333333333, - ], - Array [ - 1498982400000, - 1, - ], - Array [ - 1498983000000, - 1.1333333333333, - ], - Array [ - 1498983600000, - 1.3666666666667, - ], - Array [ - 1498984200000, - 1.9833333333333, - ], - Array [ - 1498984800000, - 2, - ], - Array [ - 1498985400000, - 2.1833333333333, - ], - Array [ - 1498986000000, - 2.2, - ], - Array [ - 1498986600000, - 2.3666666666667, - ], - Array [ - 1498987200000, - 2.4166666666667, - ], - Array [ - 1498987800000, - 2.55, - ], - Array [ - 1498988400000, - 2.7, - ], - Array [ - 1498989000000, - 2.8166666666667, - ], - Array [ - 1498989600000, - 2.9333333333333, - ], - Array [ - 1498990200000, - 3.0833333333333, - ], - Array [ - 1498990800000, - 3.1166666666667, - ], - Array [ - 1498991400000, - 3.3833333333333, - ], - Array [ - 1498992000000, - 3.4666666666667, - ], - Array [ - 1498992600000, - 1.0666666666667, - ], - Array [ - 1498993200000, - 1, - ], - Array [ - 1498993800000, - 1.0833333333333, - ], - Array [ - 1498994400000, - 1.1, - ], - Array [ - 1498995000000, - 1.05, - ], - Array [ - 1498995600000, - 1.0333333333333, - ], - Array [ - 1498996200000, - 1.1166666666667, - ], - Array [ - 1498996800000, - 1.0333333333333, - ], - Array [ - 1498997400000, - 1.0833333333333, - ], - Array [ - 1498998000000, - 1.0666666666667, - ], - Array [ - 1498998600000, - 1.0166666666667, - ], - Array [ - 1498999200000, - 1, - ], - Array [ - 1498999800000, - 1.05, - ], - Array [ - 1499000400000, - 1.0333333333333, - ], - Array [ - 1499001000000, - 1.8166666666667, - ], - Array [ - 1499001600000, - 2.0166666666667, - ], - Array [ - 1499002200000, - 2.0833333333333, - ], - Array [ - 1499002800000, - 2.2, - ], - Array [ - 1499003400000, - 2.3, - ], - Array [ - 1499004000000, - 2.3666666666667, - ], - Array [ - 1499004600000, - 2.5593220338983, - ], - Array [ - 1499005200000, - 2.7166666666667, - ], - Array [ - 1499005800000, - 2.8833333333333, - ], - Array [ - 1499006400000, - 3, - ], - Array [ - 1499007000000, - 3.0833333333333, - ], - Array [ - 1499007600000, - 3.25, - ], - Array [ - 1499008200000, - 3.3, - ], - Array [ - 1499008800000, - 3.5333333333333, - ], - Array [ - 1499009400000, - 3.7166666666667, - ], - Array [ - 1499010000000, - 3.7333333333333, - ], - Array [ - 1499010600000, - 3.9833333333333, - ], - Array [ - 1499011200000, - 4.1666666666667, - ], - Array [ - 1499011800000, - 2.7166666666667, - ], - Array [ - 1499012400000, - 1.0333333333333, - ], - Array [ - 1499013000000, - 1.1, - ], - Array [ - 1499013600000, - 1.2166666666667, - ], - Array [ - 1499014200000, - 1.2, - ], - Array [ - 1499014800000, - 0.98333333333333, - ], - Array [ - 1499015400000, - 1.25, - ], - Array [ - 1499016000000, - 1, - ], - Array [ - 1499016600000, - 1.2666666666667, - ], - Array [ - 1499017200000, - 1.0833333333333, - ], - Array [ - 1499017800000, - 1.05, - ], - Array [ - 1499018400000, - 0.93333333333333, - ], - Array [ - 1499019000000, - 1.05, - ], - Array [ - 1499019600000, - 1.5, - ], - ], - "metric": Object { - "app": "elasticsearch", - "description": "Heap memory used by Doc Values. This is a part of Lucene Total.", - "field": "index_stats.total.segments.doc_values_memory_in_bytes", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false, - "label": "Doc Values", - "metricAgg": "max", - "title": "Index Memory", - "units": "B", - }, - "timeRange": Object { - "max": 1499054399999, - "min": 1498968000000, - }, - }, - Object { - "bucket_size": "10 mins", - "data": Array [ - Array [ - 1498968000000, - 2.1666666666667, - ], - Array [ - 1498968600000, - 2.3166666666667, - ], - Array [ - 1498969200000, - 2.3666666666667, - ], - Array [ - 1498969800000, - 2.5, - ], - Array [ - 1498970400000, - 2.6166666666667, - ], - Array [ - 1498971000000, - 2.85, - ], - Array [ - 1498971600000, - 2.8666666666667, - ], - Array [ - 1498972200000, - 3.0833333333333, - ], - Array [ - 1498972800000, - 1.6666666666667, - ], - Array [ - 1498973400000, - 0.11666666666667, - ], - Array [ - 1498974000000, - 0.13333333333333, - ], - Array [ - 1498974600000, - 0.43333333333333, - ], - Array [ - 1498975200000, - 1.15, - ], - Array [ - 1498975800000, - 1.1333333333333, - ], - Array [ - 1498976400000, - 1.15, - ], - Array [ - 1498977000000, - 1.05, - ], - Array [ - 1498977600000, - 1.1833333333333, - ], - Array [ - 1498978200000, - 0.91666666666667, - ], - Array [ - 1498978800000, - 0.95, - ], - Array [ - 1498979400000, - 1.0166666666667, - ], - Array [ - 1498980000000, - 1.0333333333333, - ], - Array [ - 1498980600000, - 1.05, - ], - Array [ - 1498981200000, - 1.0333333333333, - ], - Array [ - 1498981800000, - 1.0833333333333, - ], - Array [ - 1498982400000, - 1, - ], - Array [ - 1498983000000, - 1.1333333333333, - ], - Array [ - 1498983600000, - 1.3666666666667, - ], - Array [ - 1498984200000, - 1.9833333333333, - ], - Array [ - 1498984800000, - 2, - ], - Array [ - 1498985400000, - 2.1833333333333, - ], - Array [ - 1498986000000, - 2.2, - ], - Array [ - 1498986600000, - 2.3666666666667, - ], - Array [ - 1498987200000, - 2.4166666666667, - ], - Array [ - 1498987800000, - 2.55, - ], - Array [ - 1498988400000, - 2.7, - ], - Array [ - 1498989000000, - 2.8166666666667, - ], - Array [ - 1498989600000, - 2.9333333333333, - ], - Array [ - 1498990200000, - 3.0833333333333, - ], - Array [ - 1498990800000, - 3.1166666666667, - ], - Array [ - 1498991400000, - 3.3833333333333, - ], - Array [ - 1498992000000, - 3.4666666666667, - ], - Array [ - 1498992600000, - 1.0666666666667, - ], - Array [ - 1498993200000, - 1, - ], - Array [ - 1498993800000, - 1.0833333333333, - ], - Array [ - 1498994400000, - 1.1, - ], - Array [ - 1498995000000, - 1.05, - ], - Array [ - 1498995600000, - 1.0333333333333, - ], - Array [ - 1498996200000, - 1.1166666666667, - ], - Array [ - 1498996800000, - 1.0333333333333, - ], - Array [ - 1498997400000, - 1.0833333333333, - ], - Array [ - 1498998000000, - 1.0666666666667, - ], - Array [ - 1498998600000, - 1.0166666666667, - ], - Array [ - 1498999200000, - 1, - ], - Array [ - 1498999800000, - 1.05, - ], - Array [ - 1499000400000, - 1.0333333333333, - ], - Array [ - 1499001000000, - 1.8166666666667, - ], - Array [ - 1499001600000, - 2.0166666666667, - ], - Array [ - 1499002200000, - 2.0833333333333, - ], - Array [ - 1499002800000, - 2.2, - ], - Array [ - 1499003400000, - 2.3, - ], - Array [ - 1499004000000, - 2.3666666666667, - ], - Array [ - 1499004600000, - 2.5593220338983, - ], - Array [ - 1499005200000, - 2.7166666666667, - ], - Array [ - 1499005800000, - 2.8833333333333, - ], - Array [ - 1499006400000, - 3, - ], - Array [ - 1499007000000, - 3.0833333333333, - ], - Array [ - 1499007600000, - 3.25, - ], - Array [ - 1499008200000, - 3.3, - ], - Array [ - 1499008800000, - 3.5333333333333, - ], - Array [ - 1499009400000, - 3.7166666666667, - ], - Array [ - 1499010000000, - 3.7333333333333, - ], - Array [ - 1499010600000, - 3.9833333333333, - ], - Array [ - 1499011200000, - 4.1666666666667, - ], - Array [ - 1499011800000, - 2.7166666666667, - ], - Array [ - 1499012400000, - 1.0333333333333, - ], - Array [ - 1499013000000, - 1.1, - ], - Array [ - 1499013600000, - 1.2166666666667, - ], - Array [ - 1499014200000, - 1.2, - ], - Array [ - 1499014800000, - 0.98333333333333, - ], - Array [ - 1499015400000, - 1.25, - ], - Array [ - 1499016000000, - 1, - ], - Array [ - 1499016600000, - 1.2666666666667, - ], - Array [ - 1499017200000, - 1.0833333333333, - ], - Array [ - 1499017800000, - 1.05, - ], - Array [ - 1499018400000, - 0.93333333333333, - ], - Array [ - 1499019000000, - 1.05, - ], - Array [ - 1499019600000, - 1.5, - ], - ], - "metric": Object { - "app": "elasticsearch", - "description": "Heap memory used by Norms (normalization factors for query-time, text scoring). This is a part of Lucene Total.", - "field": "index_stats.total.segments.norms_memory_in_bytes", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false, - "label": "Norms", + "label": "Version Map", "metricAgg": "max", "title": "Index Memory", "units": "B", diff --git a/x-pack/plugins/monitoring/server/lib/details/get_metrics.test.js b/x-pack/plugins/monitoring/server/lib/details/get_metrics.test.js index 3c991f08f6153..80234ee369aee 100644 --- a/x-pack/plugins/monitoring/server/lib/details/get_metrics.test.js +++ b/x-pack/plugins/monitoring/server/lib/details/get_metrics.test.js @@ -87,13 +87,8 @@ describe('getMetrics and getSeries', () => { const req = getMockReq(nonDerivMetricsBuckets); const metricSet = [ { - name: 'index_1', - keys: [ - 'index_mem_overall_1', - 'index_mem_stored_fields', - 'index_mem_doc_values', - 'index_mem_norms', - ], + name: 'index_3', + keys: ['index_mem_fixed_bit_set', 'index_mem_versions'], }, ]; const result = await getMetrics(req, indexPattern, metricSet); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__fixtures__/cluster_data.json b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__fixtures__/cluster_data.json index b8abe525b3fa6..a2c58a801cd8c 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__fixtures__/cluster_data.json +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__fixtures__/cluster_data.json @@ -2785,17 +2785,10 @@ }, "segments": { "count": 46, - "doc_values_memory_in_bytes": 61544, "file_sizes": {}, "fixed_bit_set_memory_in_bytes": 400, "index_writer_memory_in_bytes": 287996, "max_unsafe_auto_id_timestamp": 1518549739793, - "memory_in_bytes": 306088, - "norms_memory_in_bytes": 11520, - "points_memory_in_bytes": 3705, - "stored_fields_memory_in_bytes": 14456, - "term_vectors_memory_in_bytes": 0, - "terms_memory_in_bytes": 214863, "version_map_memory_in_bytes": 10008 }, "shards": { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/__fixtures__/cluster.json b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/__fixtures__/cluster.json index a49eb35086469..4f9daf62924c7 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/__fixtures__/cluster.json +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/shards/__fixtures__/cluster.json @@ -49,17 +49,10 @@ }, "segments": { "count": 202, - "doc_values_memory_in_bytes": 602248, "file_sizes": {}, "fixed_bit_set_memory_in_bytes": 608, "index_writer_memory_in_bytes": 2368414, "max_unsafe_auto_id_timestamp": 1505411847867, - "memory_in_bytes": 5079404, - "norms_memory_in_bytes": 475392, - "points_memory_in_bytes": 11512, - "stored_fields_memory_in_bytes": 65080, - "term_vectors_memory_in_bytes": 0, - "terms_memory_in_bytes": 3925172, "version_map_memory_in_bytes": 697814 }, "shards": { diff --git a/x-pack/plugins/monitoring/server/lib/metrics/__snapshots__/metrics.test.js.snap b/x-pack/plugins/monitoring/server/lib/metrics/__snapshots__/metrics.test.js.snap index 56d923da56202..cbd6a610d5eb8 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/__snapshots__/metrics.test.js.snap +++ b/x-pack/plugins/monitoring/server/lib/metrics/__snapshots__/metrics.test.js.snap @@ -2577,20 +2577,6 @@ Object { "units": "ms", "uuidField": "source_node.uuid", }, - "index_mem_doc_values": SingleIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Heap memory used by Doc Values. This is a part of Lucene Total.", - "field": "index_stats.total.segments.doc_values_memory_in_bytes", - "format": "0.0 b", - "label": "Doc Values", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory", - "type": "index", - "units": "B", - "uuidField": "source_node.uuid", - }, "index_mem_fielddata": IndexMemoryMetric { "app": "elasticsearch", "derivative": false, @@ -2614,91 +2600,7 @@ Object { "label": "Fixed Bitsets", "metricAgg": "max", "timestampField": "timestamp", - "title": "Index Memory", - "type": "index", - "units": "B", - "uuidField": "source_node.uuid", - }, - "index_mem_norms": SingleIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Heap memory used by Norms (normalization factors for query-time, text scoring). This is a part of Lucene Total.", - "field": "index_stats.total.segments.norms_memory_in_bytes", - "format": "0.0 b", - "label": "Norms", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory", - "type": "index", - "units": "B", - "uuidField": "source_node.uuid", - }, - "index_mem_overall": SingleIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards.", - "field": "index_stats.total.segments.memory_in_bytes", - "format": "0.0 b", - "label": "Lucene Total", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory", - "type": "index", - "units": "B", - "uuidField": "source_node.uuid", - }, - "index_mem_overall_1": SingleIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards.", - "field": "index_stats.total.segments.memory_in_bytes", - "format": "0.0 b", - "label": "Lucene Total", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory - Lucene 1", - "type": "index", - "units": "B", - "uuidField": "source_node.uuid", - }, - "index_mem_overall_2": SingleIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards.", - "field": "index_stats.total.segments.memory_in_bytes", - "format": "0.0 b", - "label": "Lucene Total", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory - Lucene 2", - "type": "index", - "units": "B", - "uuidField": "source_node.uuid", - }, - "index_mem_overall_3": SingleIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards.", - "field": "index_stats.total.segments.memory_in_bytes", - "format": "0.0 b", - "label": "Lucene Total", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory - Lucene 3", - "type": "index", - "units": "B", - "uuidField": "source_node.uuid", - }, - "index_mem_points": SingleIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Heap memory used by Points (e.g., numbers, IPs, and geo data). This is a part of Lucene Total.", - "field": "index_stats.total.segments.points_memory_in_bytes", - "format": "0.0 b", - "label": "Points", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory", + "title": "Index Memory - Lucene", "type": "index", "units": "B", "uuidField": "source_node.uuid", @@ -2731,48 +2633,6 @@ Object { "units": "B", "uuidField": "source_node.uuid", }, - "index_mem_stored_fields": SingleIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Heap memory used by Stored Fields (e.g., _source). This is a part of Lucene Total.", - "field": "index_stats.total.segments.stored_fields_memory_in_bytes", - "format": "0.0 b", - "label": "Stored Fields", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory", - "type": "index", - "units": "B", - "uuidField": "source_node.uuid", - }, - "index_mem_term_vectors": SingleIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Heap memory used by Term Vectors. This is a part of Lucene Total.", - "field": "index_stats.total.segments.term_vectors_memory_in_bytes", - "format": "0.0 b", - "label": "Term Vectors", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory", - "type": "index", - "units": "B", - "uuidField": "source_node.uuid", - }, - "index_mem_terms": SingleIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Heap memory used by Terms (e.g., text). This is a part of Lucene Total.", - "field": "index_stats.total.segments.terms_memory_in_bytes", - "format": "0.0 b", - "label": "Terms", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory", - "type": "index", - "units": "B", - "uuidField": "source_node.uuid", - }, "index_mem_versions": SingleIndexMemoryMetric { "app": "elasticsearch", "derivative": false, @@ -4145,20 +4005,6 @@ Object { "units": "ms", "uuidField": "source_node.uuid", }, - "node_index_mem_doc_values": NodeIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Heap memory used by Doc Values. This is a part of Lucene Total.", - "field": "node_stats.indices.segments.doc_values_memory_in_bytes", - "format": "0.0 b", - "label": "Doc Values", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory", - "type": "node", - "units": "B", - "uuidField": "source_node.uuid", - }, "node_index_mem_fielddata": IndexMemoryMetric { "app": "elasticsearch", "derivative": false, @@ -4182,91 +4028,7 @@ Object { "label": "Fixed Bitsets", "metricAgg": "max", "timestampField": "timestamp", - "title": "Index Memory", - "type": "node", - "units": "B", - "uuidField": "source_node.uuid", - }, - "node_index_mem_norms": NodeIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Heap memory used by Norms (normalization factors for query-time, text scoring). This is a part of Lucene Total.", - "field": "node_stats.indices.segments.norms_memory_in_bytes", - "format": "0.0 b", - "label": "Norms", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory", - "type": "node", - "units": "B", - "uuidField": "source_node.uuid", - }, - "node_index_mem_overall": NodeIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.", - "field": "node_stats.indices.segments.memory_in_bytes", - "format": "0.0 b", - "label": "Lucene Total", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory", - "type": "node", - "units": "B", - "uuidField": "source_node.uuid", - }, - "node_index_mem_overall_1": NodeIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.", - "field": "node_stats.indices.segments.memory_in_bytes", - "format": "0.0 b", - "label": "Lucene Total", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory - Lucene 1", - "type": "node", - "units": "B", - "uuidField": "source_node.uuid", - }, - "node_index_mem_overall_2": NodeIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.", - "field": "node_stats.indices.segments.memory_in_bytes", - "format": "0.0 b", - "label": "Lucene Total", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory - Lucene 2", - "type": "node", - "units": "B", - "uuidField": "source_node.uuid", - }, - "node_index_mem_overall_3": NodeIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.", - "field": "node_stats.indices.segments.memory_in_bytes", - "format": "0.0 b", - "label": "Lucene Total", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory - Lucene 3", - "type": "node", - "units": "B", - "uuidField": "source_node.uuid", - }, - "node_index_mem_points": NodeIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Heap memory used by Points (e.g., numbers, IPs, and geo data). This is a part of Lucene Total.", - "field": "node_stats.indices.segments.points_memory_in_bytes", - "format": "0.0 b", - "label": "Points", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory", + "title": "Index Memory - Lucene", "type": "node", "units": "B", "uuidField": "source_node.uuid", @@ -4299,48 +4061,6 @@ Object { "units": "B", "uuidField": "source_node.uuid", }, - "node_index_mem_stored_fields": NodeIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Heap memory used by Stored Fields (e.g., _source). This is a part of Lucene Total.", - "field": "node_stats.indices.segments.stored_fields_memory_in_bytes", - "format": "0.0 b", - "label": "Stored Fields", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory", - "type": "node", - "units": "B", - "uuidField": "source_node.uuid", - }, - "node_index_mem_term_vectors": NodeIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Heap memory used by Term Vectors. This is a part of Lucene Total.", - "field": "node_stats.indices.segments.term_vectors_memory_in_bytes", - "format": "0.0 b", - "label": "Term Vectors", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory", - "type": "node", - "units": "B", - "uuidField": "source_node.uuid", - }, - "node_index_mem_terms": NodeIndexMemoryMetric { - "app": "elasticsearch", - "derivative": false, - "description": "Heap memory used by Terms (e.g., text). This is a part of Lucene Total.", - "field": "node_stats.indices.segments.terms_memory_in_bytes", - "format": "0.0 b", - "label": "Terms", - "metricAgg": "max", - "timestampField": "timestamp", - "title": "Index Memory", - "type": "node", - "units": "B", - "uuidField": "source_node.uuid", - }, "node_index_mem_versions": NodeIndexMemoryMetric { "app": "elasticsearch", "derivative": false, diff --git a/x-pack/plugins/monitoring/server/lib/metrics/elasticsearch/metrics.js b/x-pack/plugins/monitoring/server/lib/metrics/elasticsearch/metrics.js index 6fe93619a5f9b..83841b0ecb39e 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/elasticsearch/metrics.js +++ b/x-pack/plugins/monitoring/server/lib/metrics/elasticsearch/metrics.js @@ -307,73 +307,6 @@ export const metrics = { metricAgg: 'max', units: '', }), - index_mem_overall: new SingleIndexMemoryMetric({ - field: 'memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esIndex.luceneTotalLabel', { - defaultMessage: 'Lucene Total', - }), - description: i18n.translate('xpack.monitoring.metrics.esIndex.luceneTotalDescription', { - defaultMessage: - 'Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards.', - }), - }), - index_mem_overall_1: new SingleIndexMemoryMetric({ - field: 'memory_in_bytes', - title: i18n.translate('xpack.monitoring.metrics.esIndex.indexMemoryLucene1Title', { - defaultMessage: 'Index Memory - Lucene 1', - }), - label: i18n.translate('xpack.monitoring.metrics.esIndex.indexMemoryLucene1.luceneTotalLabel', { - defaultMessage: 'Lucene Total', - }), - description: i18n.translate( - 'xpack.monitoring.metrics.esIndex.indexMemoryLucene1.luceneTotalDescription', - { - defaultMessage: - 'Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards.', - } - ), - }), - index_mem_overall_2: new SingleIndexMemoryMetric({ - field: 'memory_in_bytes', - title: i18n.translate('xpack.monitoring.metrics.esIndex.indexMemoryLucene2Title', { - defaultMessage: 'Index Memory - Lucene 2', - }), - label: i18n.translate('xpack.monitoring.metrics.esIndex.indexMemoryLucene2.luceneTotalLabel', { - defaultMessage: 'Lucene Total', - }), - description: i18n.translate( - 'xpack.monitoring.metrics.esIndex.indexMemoryLucene2.luceneTotalDescription', - { - defaultMessage: - 'Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards.', - } - ), - }), - index_mem_overall_3: new SingleIndexMemoryMetric({ - field: 'memory_in_bytes', - title: i18n.translate('xpack.monitoring.metrics.esIndex.indexMemoryLucene3Title', { - defaultMessage: 'Index Memory - Lucene 3', - }), - label: i18n.translate('xpack.monitoring.metrics.esIndex.indexMemoryLucene3.luceneTotalLabel', { - defaultMessage: 'Lucene Total', - }), - description: i18n.translate( - 'xpack.monitoring.metrics.esIndex.indexMemoryLucene3.luceneTotalDescription', - { - defaultMessage: - 'Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards.', - } - ), - }), - index_mem_doc_values: new SingleIndexMemoryMetric({ - field: 'doc_values_memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esIndex.docValuesLabel', { - defaultMessage: 'Doc Values', - }), - description: i18n.translate('xpack.monitoring.metrics.esIndex.docValuesDescription', { - defaultMessage: 'Heap memory used by Doc Values. This is a part of Lucene Total.', - }), - }), // Note: This is not segment memory, unlike SingleIndexMemoryMetrics index_mem_fielddata: new IndexMemoryMetric({ field: 'index_stats.total.fielddata.memory_size_in_bytes', @@ -389,6 +322,9 @@ export const metrics = { }), index_mem_fixed_bit_set: new SingleIndexMemoryMetric({ field: 'fixed_bit_set_memory_in_bytes', + title: i18n.translate('xpack.monitoring.metrics.esIndex.fixedBitsetsTitle', { + defaultMessage: 'Index Memory - Lucene', + }), label: i18n.translate('xpack.monitoring.metrics.esIndex.fixedBitsetsLabel', { defaultMessage: 'Fixed Bitsets', }), @@ -397,26 +333,6 @@ export const metrics = { 'Heap memory used by Fixed Bit Sets (e.g., deeply nested documents). This is a part of Lucene Total.', }), }), - index_mem_norms: new SingleIndexMemoryMetric({ - field: 'norms_memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esIndex.normsLabel', { - defaultMessage: 'Norms', - }), - description: i18n.translate('xpack.monitoring.metrics.esIndex.normsDescription', { - defaultMessage: - 'Heap memory used by Norms (normalization factors for query-time, text scoring). This is a part of Lucene Total.', - }), - }), - index_mem_points: new SingleIndexMemoryMetric({ - field: 'points_memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esIndex.pointsLabel', { - defaultMessage: 'Points', - }), - description: i18n.translate('xpack.monitoring.metrics.esIndex.pointsDescription', { - defaultMessage: - 'Heap memory used by Points (e.g., numbers, IPs, and geo data). This is a part of Lucene Total.', - }), - }), // Note: This is not segment memory, unlike SingleIndexMemoryMetrics index_mem_query_cache: new IndexMemoryMetric({ field: 'index_stats.total.query_cache.memory_size_in_bytes', @@ -448,34 +364,6 @@ export const metrics = { }), type: 'index', }), - index_mem_stored_fields: new SingleIndexMemoryMetric({ - field: 'stored_fields_memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esIndex.storedFieldsLabel', { - defaultMessage: 'Stored Fields', - }), - description: i18n.translate('xpack.monitoring.metrics.esIndex.storedFieldsDescription', { - defaultMessage: - 'Heap memory used by Stored Fields (e.g., _source). This is a part of Lucene Total.', - }), - }), - index_mem_term_vectors: new SingleIndexMemoryMetric({ - field: 'term_vectors_memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esIndex.termVectorsLabel', { - defaultMessage: 'Term Vectors', - }), - description: i18n.translate('xpack.monitoring.metrics.esIndex.termVectorsDescription', { - defaultMessage: 'Heap memory used by Term Vectors. This is a part of Lucene Total.', - }), - }), - index_mem_terms: new SingleIndexMemoryMetric({ - field: 'terms_memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esIndex.termsLabel', { - defaultMessage: 'Terms', - }), - description: i18n.translate('xpack.monitoring.metrics.esIndex.termsDescription', { - defaultMessage: 'Heap memory used by Terms (e.g., text). This is a part of Lucene Total.', - }), - }), index_mem_versions: new SingleIndexMemoryMetric({ field: 'version_map_memory_in_bytes', label: i18n.translate('xpack.monitoring.metrics.esIndex.versionMapLabel', { @@ -1050,73 +938,6 @@ export const metrics = { metricAgg: 'max', units: '', }), - node_index_mem_overall: new NodeIndexMemoryMetric({ - field: 'memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esNode.luceneTotalLabel', { - defaultMessage: 'Lucene Total', - }), - description: i18n.translate('xpack.monitoring.metrics.esNode.luceneTotalDescription', { - defaultMessage: - 'Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.', - }), - }), - node_index_mem_overall_1: new NodeIndexMemoryMetric({ - field: 'memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esNode.indexMemoryLucene1.lucenceTotalLabel', { - defaultMessage: 'Lucene Total', - }), - title: i18n.translate('xpack.monitoring.metrics.esNode.indexMemoryLucene1Title', { - defaultMessage: 'Index Memory - Lucene 1', - }), - description: i18n.translate( - 'xpack.monitoring.metrics.esNode.indexMemoryLucene1.lucenceTotalDescription', - { - defaultMessage: - 'Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.', - } - ), - }), - node_index_mem_overall_2: new NodeIndexMemoryMetric({ - field: 'memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esNode.indexMemoryLucene2.lucenceTotalLabel', { - defaultMessage: 'Lucene Total', - }), - title: i18n.translate('xpack.monitoring.metrics.esNode.indexMemoryLucene2Title', { - defaultMessage: 'Index Memory - Lucene 2', - }), - description: i18n.translate( - 'xpack.monitoring.metrics.esNode.indexMemoryLucene2.lucenceTotalDescription', - { - defaultMessage: - 'Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.', - } - ), - }), - node_index_mem_overall_3: new NodeIndexMemoryMetric({ - field: 'memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esNode.indexMemoryLucene3.lucenceTotalLabel', { - defaultMessage: 'Lucene Total', - }), - title: i18n.translate('xpack.monitoring.metrics.esNode.indexMemoryLucene3Title', { - defaultMessage: 'Index Memory - Lucene 3', - }), - description: i18n.translate( - 'xpack.monitoring.metrics.esNode.indexMemoryLucene3.lucenceTotalDescription', - { - defaultMessage: - 'Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.', - } - ), - }), - node_index_mem_doc_values: new NodeIndexMemoryMetric({ - field: 'doc_values_memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esNode.docValuesLabel', { - defaultMessage: 'Doc Values', - }), - description: i18n.translate('xpack.monitoring.metrics.esNode.docValuesDescription', { - defaultMessage: 'Heap memory used by Doc Values. This is a part of Lucene Total.', - }), - }), // Note: This is not segment memory, unlike the rest of the SingleIndexMemoryMetrics node_index_mem_fielddata: new IndexMemoryMetric({ field: 'node_stats.indices.fielddata.memory_size_in_bytes', @@ -1132,6 +953,9 @@ export const metrics = { }), node_index_mem_fixed_bit_set: new NodeIndexMemoryMetric({ field: 'fixed_bit_set_memory_in_bytes', + title: i18n.translate('xpack.monitoring.metrics.esNode.fixedBitsetsTitle', { + defaultMessage: 'Index Memory - Lucene', + }), label: i18n.translate('xpack.monitoring.metrics.esNode.fixedBitsetsLabel', { defaultMessage: 'Fixed Bitsets', }), @@ -1140,26 +964,6 @@ export const metrics = { 'Heap memory used by Fixed Bit Sets (e.g., deeply nested documents). This is a part of Lucene Total.', }), }), - node_index_mem_norms: new NodeIndexMemoryMetric({ - field: 'norms_memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esNode.normsLabel', { - defaultMessage: 'Norms', - }), - description: i18n.translate('xpack.monitoring.metrics.esNode.normsDescription', { - defaultMessage: - 'Heap memory used by Norms (normalization factors for query-time, text scoring). This is a part of Lucene Total.', - }), - }), - node_index_mem_points: new NodeIndexMemoryMetric({ - field: 'points_memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esNode.pointsLabel', { - defaultMessage: 'Points', - }), - description: i18n.translate('xpack.monitoring.metrics.esNode.pointsDescription', { - defaultMessage: - 'Heap memory used by Points (e.g., numbers, IPs, and geo data). This is a part of Lucene Total.', - }), - }), // Note: This is not segment memory, unlike SingleIndexMemoryMetrics node_index_mem_query_cache: new IndexMemoryMetric({ field: 'node_stats.indices.query_cache.memory_size_in_bytes', @@ -1191,34 +995,6 @@ export const metrics = { }), type: 'node', }), - node_index_mem_stored_fields: new NodeIndexMemoryMetric({ - field: 'stored_fields_memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esNode.storedFieldsLabel', { - defaultMessage: 'Stored Fields', - }), - description: i18n.translate('xpack.monitoring.metrics.esNode.storedFieldsDescription', { - defaultMessage: - 'Heap memory used by Stored Fields (e.g., _source). This is a part of Lucene Total.', - }), - }), - node_index_mem_term_vectors: new NodeIndexMemoryMetric({ - field: 'term_vectors_memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esNode.termVectorsLabel', { - defaultMessage: 'Term Vectors', - }), - description: i18n.translate('xpack.monitoring.metrics.esNode.termVectorsDescription', { - defaultMessage: 'Heap memory used by Term Vectors. This is a part of Lucene Total.', - }), - }), - node_index_mem_terms: new NodeIndexMemoryMetric({ - field: 'terms_memory_in_bytes', - label: i18n.translate('xpack.monitoring.metrics.esNode.termsLabel', { - defaultMessage: 'Terms', - }), - description: i18n.translate('xpack.monitoring.metrics.esNode.termsDescription', { - defaultMessage: 'Heap memory used by Terms (e.g., text). This is a part of Lucene Total.', - }), - }), node_index_mem_versions: new NodeIndexMemoryMetric({ field: 'version_map_memory_in_bytes', label: i18n.translate('xpack.monitoring.metrics.esNode.versionMapLabel', { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/metric_set_index_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/metric_set_index_detail.js index 14712ecd59f0e..fce09eac4918f 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/metric_set_index_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/metric_set_index_detail.js @@ -8,25 +8,7 @@ export const metricSet = { advanced: [ { - keys: [ - 'index_mem_overall_1', - 'index_mem_stored_fields', - 'index_mem_doc_values', - 'index_mem_norms', - ], - name: 'index_1', - }, - { - keys: ['index_mem_overall_2', 'index_mem_terms', 'index_mem_points'], - name: 'index_2', - }, - { - keys: [ - 'index_mem_overall_3', - 'index_mem_fixed_bit_set', - 'index_mem_term_vectors', - 'index_mem_versions', - ], + keys: ['index_mem_fixed_bit_set', 'index_mem_versions'], name: 'index_3', }, { @@ -82,10 +64,6 @@ export const metricSet = { keys: ['index_store_total_size', 'index_store_primaries_size'], name: 'index_size', }, - { - keys: ['index_mem_overall', 'index_mem_terms', 'index_mem_points'], - name: 'index_mem', - }, 'index_document_count', { keys: ['index_segment_count_total', 'index_segment_count_primaries'], diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/metric_set_node_detail.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/metric_set_node_detail.js index dcb5b70cf8963..5303dc452f850 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/metric_set_node_detail.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch/metric_set_node_detail.js @@ -20,25 +20,7 @@ export const metricSets = { name: 'node_gc_time', }, { - keys: [ - 'node_index_mem_overall_1', - 'node_index_mem_stored_fields', - 'node_index_mem_doc_values', - 'node_index_mem_norms', - ], - name: 'node_index_1', - }, - { - keys: ['node_index_mem_overall_2', 'node_index_mem_terms', 'node_index_mem_points'], - name: 'node_index_2', - }, - { - keys: [ - 'node_index_mem_overall_3', - 'node_index_mem_fixed_bit_set', - 'node_index_mem_term_vectors', - 'node_index_mem_versions', - ], + keys: ['node_index_mem_fixed_bit_set', 'node_index_mem_versions'], name: 'node_index_3', }, { @@ -101,10 +83,6 @@ export const metricSets = { keys: ['node_jvm_mem_max_in_bytes', 'node_jvm_mem_used_in_bytes'], name: 'node_jvm_mem', }, - { - keys: ['node_index_mem_overall', 'node_index_mem_terms', 'node_index_mem_points'], - name: 'node_mem', - }, { keys: [], name: 'node_cpu_metric', diff --git a/x-pack/plugins/observability/public/components/shared/alert_status_indicator.tsx b/x-pack/plugins/observability/public/components/shared/alert_status_indicator.tsx index e8897ed92dc8c..ad149c186ffa6 100644 --- a/x-pack/plugins/observability/public/components/shared/alert_status_indicator.tsx +++ b/x-pack/plugins/observability/public/components/shared/alert_status_indicator.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiHealth, EuiText } from '@elastic/eui'; -import { ALERT_STATUS_ACTIVE, AlertStatus } from '@kbn/rule-data-utils'; +import { ALERT_STATUS_ACTIVE, AlertStatus } from '@kbn/rule-data-utils/alerts_as_data_status'; import { useTheme } from '../../hooks/use_theme'; interface AlertStatusIndicatorProps { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx index 35ce2fc6c1a47..497435ec69a50 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; import { AllSeries, useTheme } from '../../../..'; import { LayerConfig, LensAttributes } from '../configurations/lens_attributes'; -import { ReportViewType } from '../types'; +import { AppDataType, ReportViewType } from '../types'; import { getLayerConfigs } from '../hooks/use_lens_attributes'; import { LensPublicStart, XYState } from '../../../../../../lens/public'; import { OperationTypeComponent } from '../series_editor/columns/operation_type_select'; @@ -24,6 +24,7 @@ export interface ExploratoryEmbeddableProps { showCalculationMethod?: boolean; axisTitlesVisibility?: XYState['axisTitlesVisibilitySettings']; legendIsVisible?: boolean; + dataTypesIndexPatterns?: Record; } export interface ExploratoryEmbeddableComponentProps extends ExploratoryEmbeddableProps { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx index ad84880de5eb1..e68ddfe55e6f5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx @@ -13,6 +13,7 @@ import { ObservabilityIndexPatterns } from '../utils/observability_index_pattern import { ObservabilityPublicPluginsStart } from '../../../../plugin'; import type { IndexPatternState } from '../hooks/use_app_index_pattern'; import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; +import type { AppDataType } from '../types'; const Embeddable = React.lazy(() => import('./embeddable')); @@ -36,18 +37,26 @@ export function getExploratoryViewEmbeddable( const isDarkMode = core.uiSettings.get('theme:darkMode'); - const loadIndexPattern = useCallback(async ({ dataType }) => { - setLoading(true); - try { - const obsvIndexP = new ObservabilityIndexPatterns(plugins.data); - const indPattern = await obsvIndexP.getIndexPattern(dataType, 'heartbeat-*'); - setIndexPatterns((prevState) => ({ ...(prevState ?? {}), [dataType]: indPattern })); + const loadIndexPattern = useCallback( + async ({ dataType }: { dataType: AppDataType }) => { + const dataTypesIndexPatterns = props.dataTypesIndexPatterns; - setLoading(false); - } catch (e) { - setLoading(false); - } - }, []); + setLoading(true); + try { + const obsvIndexP = new ObservabilityIndexPatterns(plugins.data); + const indPattern = await obsvIndexP.getIndexPattern( + dataType, + dataTypesIndexPatterns?.[dataType] + ); + setIndexPatterns((prevState) => ({ ...(prevState ?? {}), [dataType]: indPattern })); + + setLoading(false); + } catch (e) { + setLoading(false); + } + }, + [props.dataTypesIndexPatterns] + ); useEffect(() => { loadIndexPattern({ dataType: series.dataType }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts index f591ef63a61fb..1c14ffe3d13c9 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts @@ -16,6 +16,7 @@ import { rumFieldFormats } from '../configurations/rum/field_formats'; import { syntheticsFieldFormats } from '../configurations/synthetics/field_formats'; import { AppDataType, FieldFormat, FieldFormatParams } from '../types'; import { apmFieldFormats } from '../configurations/apm/field_formats'; +import { getDataHandler } from '../../../../data_handler'; const appFieldFormats: Record = { infra_logs: null, @@ -125,28 +126,47 @@ export class ObservabilityIndexPatterns { return fieldFormatMap; } - async getIndexPattern(app: AppDataType, indices: string): Promise { + async getDataTypeIndices(dataType: AppDataType) { + switch (dataType) { + case 'ux': + case 'synthetics': + const resultUx = await getDataHandler(dataType)?.hasData(); + return resultUx?.indices; + case 'apm': + case 'mobile': + const resultApm = await getDataHandler('apm')?.hasData(); + return resultApm?.indices.transaction; + } + } + + async getIndexPattern(app: AppDataType, indices?: string): Promise { if (!this.data) { throw new Error('data is not defined'); } + let appIndices = indices; + if (!appIndices) { + appIndices = await this.getDataTypeIndices(app); + } - try { - const indexPatternId = getAppIndexPatternId(app, indices); - const indexPatternTitle = getAppIndicesWithPattern(app, indices); - // we will get index pattern by id - const indexPattern = await this.data?.indexPatterns.get(indexPatternId); + if (appIndices) { + try { + const indexPatternId = getAppIndexPatternId(app, appIndices); + const indexPatternTitle = getAppIndicesWithPattern(app, appIndices); + // we will get index pattern by id + const indexPattern = await this.data?.indexPatterns.get(indexPatternId); - // and make sure title matches, otherwise, we will need to create it - if (indexPattern.title !== indexPatternTitle) { - return await this.createIndexPattern(app, indices); - } + // and make sure title matches, otherwise, we will need to create it + if (indexPattern.title !== indexPatternTitle) { + return await this.createIndexPattern(app, appIndices); + } - // this is intentional a non blocking call, so no await clause - this.validateFieldFormats(app, indexPattern); - return indexPattern; - } catch (e: unknown) { - if (e instanceof SavedObjectNotFound) { - return await this.createIndexPattern(app, indices); + // this is intentional a non blocking call, so no await clause + this.validateFieldFormats(app, indexPattern); + return indexPattern; + } catch (e: unknown) { + if (e instanceof SavedObjectNotFound) { + return await this.createIndexPattern(app, appIndices); + } } } } diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx index a0cc697a7a510..64d495dbbc798 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { ALERT_UUID } from '@kbn/rule-data-utils'; +import { ALERT_UUID } from '@kbn/rule-data-utils/technical_field_names'; import React, { ComponentType } from 'react'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; import { PluginContext, PluginContextValue } from '../../../context/plugin_context'; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx index bb746d0acc1cc..c5cad5f3b1c8c 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx @@ -21,24 +21,18 @@ import { EuiTitle, EuiHorizontalRule, } from '@elastic/eui'; -import type { - ALERT_DURATION as ALERT_DURATION_TYPED, - ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, - ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, - ALERT_UUID as ALERT_UUID_TYPED, - ALERT_RULE_CATEGORY as ALERT_RULE_CATEGORY_TYPED, - ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, -} from '@kbn/rule-data-utils'; import { - ALERT_DURATION as ALERT_DURATION_NON_TYPED, - ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, - ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, - ALERT_UUID as ALERT_UUID_NON_TYPED, - ALERT_RULE_CATEGORY as ALERT_RULE_CATEGORY_NON_TYPED, - ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, - // @ts-expect-error -} from '@kbn/rule-data-utils/target_node/technical_field_names'; -import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils'; + ALERT_DURATION, + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_UUID, + ALERT_RULE_CATEGORY, + ALERT_RULE_NAME, +} from '@kbn/rule-data-utils/technical_field_names'; +import { + ALERT_STATUS_ACTIVE, + ALERT_STATUS_RECOVERED, +} from '@kbn/rule-data-utils/alerts_as_data_status'; import moment from 'moment-timezone'; import React, { useMemo } from 'react'; import type { TopAlert } from '../'; @@ -58,15 +52,6 @@ type AlertsFlyoutProps = { selectedAlertId?: string; } & EuiFlyoutProps; -const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; -const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = - ALERT_EVALUATION_THRESHOLD_NON_TYPED; -const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = - ALERT_EVALUATION_VALUE_NON_TYPED; -const ALERT_UUID: typeof ALERT_UUID_TYPED = ALERT_UUID_NON_TYPED; -const ALERT_RULE_CATEGORY: typeof ALERT_RULE_CATEGORY_TYPED = ALERT_RULE_CATEGORY_NON_TYPED; -const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; - export function AlertsFlyout({ alert, alerts, diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index 523d0f19be2be..dac2b0951ae05 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -11,23 +11,14 @@ * This way plugins can do targeted imports to reduce the final code bundle */ import { - ALERT_DURATION as ALERT_DURATION_TYPED, - ALERT_REASON as ALERT_REASON_TYPED, + ALERT_DURATION, + ALERT_REASON, ALERT_RULE_CONSUMER, ALERT_RULE_PRODUCER, - ALERT_STATUS as ALERT_STATUS_TYPED, - ALERT_WORKFLOW_STATUS as ALERT_WORKFLOW_STATUS_TYPED, -} from '@kbn/rule-data-utils'; -// @ts-expect-error importing from a place other than root because we want to limit what we import from this package -import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; -import { - ALERT_DURATION as ALERT_DURATION_NON_TYPED, - ALERT_REASON as ALERT_REASON_NON_TYPED, - ALERT_STATUS as ALERT_STATUS_NON_TYPED, - ALERT_WORKFLOW_STATUS as ALERT_WORKFLOW_STATUS_NON_TYPED, + ALERT_STATUS, + ALERT_WORKFLOW_STATUS, TIMESTAMP, - // @ts-expect-error importing from a place other than root because we want to limit what we import from this package -} from '@kbn/rule-data-utils/target_node/technical_field_names'; +} from '@kbn/rule-data-utils/technical_field_names'; import { EuiButtonIcon, @@ -69,11 +60,6 @@ import { parseAlert } from './parse_alert'; import { CoreStart } from '../../../../../../src/core/public'; import { translations, paths } from '../../config'; -const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; -const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; -const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; -const ALERT_WORKFLOW_STATUS: typeof ALERT_WORKFLOW_STATUS_TYPED = ALERT_WORKFLOW_STATUS_NON_TYPED; - interface AlertsTableTGridProps { indexNames: string[]; rangeFrom: string; diff --git a/x-pack/plugins/observability/public/pages/alerts/example_data.ts b/x-pack/plugins/observability/public/pages/alerts/example_data.ts index 1354da592f796..08af9a3575405 100644 --- a/x-pack/plugins/observability/public/pages/alerts/example_data.ts +++ b/x-pack/plugins/observability/public/pages/alerts/example_data.ts @@ -13,14 +13,16 @@ import { ALERT_RULE_TYPE_ID, ALERT_START, ALERT_STATUS, - ALERT_STATUS_ACTIVE, - ALERT_STATUS_RECOVERED, ALERT_UUID, ALERT_RULE_UUID, ALERT_RULE_NAME, ALERT_RULE_CATEGORY, ALERT_RULE_PRODUCER, -} from '@kbn/rule-data-utils'; +} from '@kbn/rule-data-utils/technical_field_names'; +import { + ALERT_STATUS_ACTIVE, + ALERT_STATUS_RECOVERED, +} from '@kbn/rule-data-utils/alerts_as_data_status'; export const apmAlertResponseExample = [ { diff --git a/x-pack/plugins/observability/public/pages/alerts/parse_alert.ts b/x-pack/plugins/observability/public/pages/alerts/parse_alert.ts index e4d3b08c00fae..7b28803084067 100644 --- a/x-pack/plugins/observability/public/pages/alerts/parse_alert.ts +++ b/x-pack/plugins/observability/public/pages/alerts/parse_alert.ts @@ -5,30 +5,18 @@ * 2.0. */ -import type { - ALERT_START as ALERT_START_TYPED, - ALERT_STATUS as ALERT_STATUS_TYPED, - ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED, - ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, -} from '@kbn/rule-data-utils'; import { - ALERT_START as ALERT_START_NON_TYPED, - ALERT_STATUS as ALERT_STATUS_NON_TYPED, - ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED, - ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, - // @ts-expect-error -} from '@kbn/rule-data-utils/target_node/technical_field_names'; -import { ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils'; + ALERT_START, + ALERT_STATUS, + ALERT_RULE_TYPE_ID, + ALERT_RULE_NAME, +} from '@kbn/rule-data-utils/technical_field_names'; +import { ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils/alerts_as_data_status'; import type { TopAlert } from '.'; import { parseTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields'; import { asDuration, asPercent } from '../../../common/utils/formatters'; import { ObservabilityRuleTypeRegistry } from '../../rules/create_observability_rule_type_registry'; -const ALERT_START: typeof ALERT_START_TYPED = ALERT_START_NON_TYPED; -const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; -const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; -const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; - export const parseAlert = (observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry) => (alert: Record): TopAlert => { diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.test.tsx b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.test.tsx index 55333e8b7ea76..79a27faa96c69 100644 --- a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.test.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.test.tsx @@ -5,9 +5,11 @@ * 2.0. */ -// @ts-expect-error importing from a place other than root because we want to limit what we import from this package -import { ALERT_STATUS } from '@kbn/rule-data-utils/target_node/technical_field_names'; -import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils'; +import { + ALERT_STATUS_ACTIVE, + ALERT_STATUS_RECOVERED, +} from '@kbn/rule-data-utils/alerts_as_data_status'; +import { ALERT_STATUS } from '@kbn/rule-data-utils/technical_field_names'; import type { CellValueElementProps } from '../../../../timelines/common'; import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock'; import * as PluginHook from '../../hooks/use_plugin_context'; diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx index f7e14545048a7..80ccd4a69b281 100644 --- a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx @@ -6,26 +6,17 @@ */ import { EuiLink } from '@elastic/eui'; import React from 'react'; -/** - * We need to produce types and code transpilation at different folders during the build of the package. - * We have types and code at different imports because we don't want to import the whole package in the resulting webpack bundle for the plugin. - * This way plugins can do targeted imports to reduce the final code bundle - */ -import type { - ALERT_DURATION as ALERT_DURATION_TYPED, - ALERT_SEVERITY as ALERT_SEVERITY_TYPED, - ALERT_STATUS as ALERT_STATUS_TYPED, - ALERT_REASON as ALERT_REASON_TYPED, -} from '@kbn/rule-data-utils'; import { - ALERT_DURATION as ALERT_DURATION_NON_TYPED, - ALERT_SEVERITY as ALERT_SEVERITY_NON_TYPED, - ALERT_STATUS as ALERT_STATUS_NON_TYPED, - ALERT_REASON as ALERT_REASON_NON_TYPED, + ALERT_DURATION, + ALERT_SEVERITY, + ALERT_STATUS, + ALERT_REASON, TIMESTAMP, - // @ts-expect-error importing from a place other than root because we want to limit what we import from this package -} from '@kbn/rule-data-utils/target_node/technical_field_names'; -import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils'; +} from '@kbn/rule-data-utils/technical_field_names'; +import { + ALERT_STATUS_ACTIVE, + ALERT_STATUS_RECOVERED, +} from '@kbn/rule-data-utils/alerts_as_data_status'; import type { CellValueElementProps, TimelineNonEcsData } from '../../../../timelines/common'; import { AlertStatusIndicator } from '../../components/shared/alert_status_indicator'; import { TimestampTooltip } from '../../components/shared/timestamp_tooltip'; @@ -35,11 +26,6 @@ import { TopAlert } from '.'; import { parseAlert } from './parse_alert'; import { usePluginContext } from '../../hooks/use_plugin_context'; -const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; -const ALERT_SEVERITY: typeof ALERT_SEVERITY_TYPED = ALERT_SEVERITY_NON_TYPED; -const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; -const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; - export const getMappedNonEcsValue = ({ data, fieldName, diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts index 16f0b17cc10df..77dfde5800c1e 100644 --- a/x-pack/plugins/osquery/server/plugin.ts +++ b/x-pack/plugins/osquery/server/plugin.ts @@ -134,7 +134,7 @@ const registerFeatures = (features: SetupPlugins['features']) => { groupType: 'mutually_exclusive', privileges: [ { - api: [`${PLUGIN_ID}-writeSavedQueries`], + api: [`${PLUGIN_ID}-writeSavedQueries`, `${PLUGIN_ID}-readSavedQueries`], id: 'saved_queries_all', includeIn: 'all', name: 'All', @@ -168,7 +168,7 @@ const registerFeatures = (features: SetupPlugins['features']) => { groupType: 'mutually_exclusive', privileges: [ { - api: [`${PLUGIN_ID}-writePacks`], + api: [`${PLUGIN_ID}-writePacks`, `${PLUGIN_ID}-readPacks`], id: 'packs_all', includeIn: 'all', name: 'All', diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index fa836fd47cde3..711e930484e01 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -18,10 +18,10 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { poll: true, roles: true }, schema: ConfigSchema, deprecations: ({ unused }) => [ - unused('capture.browser.chromium.maxScreenshotDimension'), // unused since 7.8 - unused('poll.jobCompletionNotifier.intervalErrorMultiplier'), // unused since 7.10 - unused('poll.jobsRefresh.intervalErrorMultiplier'), // unused since 7.10 - unused('capture.viewport'), // deprecated as unused since 7.16 + unused('capture.browser.chromium.maxScreenshotDimension', { level: 'warning' }), // unused since 7.8 + unused('poll.jobCompletionNotifier.intervalErrorMultiplier', { level: 'warning' }), // unused since 7.10 + unused('poll.jobsRefresh.intervalErrorMultiplier', { level: 'warning' }), // unused since 7.10 + unused('capture.viewport', { level: 'warning' }), // deprecated as unused since 7.16 (settings, fromPath, addDeprecation) => { const reporting = get(settings, fromPath); if (reporting?.roles?.enabled !== false) { diff --git a/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts b/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts index 47b00fb1348eb..c17681d72477e 100644 --- a/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts +++ b/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts @@ -5,4 +5,6 @@ * 2.0. */ -export * from '@kbn/rule-data-utils'; +export * from '@kbn/rule-data-utils/technical_field_names'; +export * from '@kbn/rule-data-utils/alerts_as_data_status'; +export * from '@kbn/rule-data-utils/alerts_as_data_rbac'; diff --git a/x-pack/plugins/security_solution/common/field_maps/field_names.ts b/x-pack/plugins/security_solution/common/field_maps/field_names.ts index 1cb40063202d0..f5a293b975afa 100644 --- a/x-pack/plugins/security_solution/common/field_maps/field_names.ts +++ b/x-pack/plugins/security_solution/common/field_maps/field_names.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ALERT_NAMESPACE, ALERT_RULE_NAMESPACE } from '@kbn/rule-data-utils'; +import { ALERT_NAMESPACE, ALERT_RULE_NAMESPACE } from '@kbn/rule-data-utils/technical_field_names'; export const ALERT_ANCESTORS = `${ALERT_NAMESPACE}.ancestors` as const; export const ALERT_BUILDING_BLOCK_TYPE = `${ALERT_NAMESPACE}.building_block_type` as const; diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index 0d2eab221b49a..6177234575ec3 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -7,10 +7,8 @@ export const ADD_EXCEPTION_BTN = '[data-test-subj="add-exception-menu-item"]'; -export const ALERTS = '[data-test-subj="events-viewer-panel"] [data-test-subj="event"]'; - -export const ALERTS_COUNT = - '[data-test-subj="events-viewer-panel"] [data-test-subj="server-side-event-count"]'; +export const ALERT_COUNT_TABLE_FIRST_ROW_COUNT = + '[data-test-subj="alertsCountTable"] tr:nth-child(1) td:nth-child(2) .euiTableCellContent__text'; export const ALERT_CHECKBOX = '[data-test-subj~="select-event"].euiCheckbox__input'; @@ -28,16 +26,28 @@ export const ALERT_RULE_SEVERITY = '[data-test-subj="formatted-field-kibana.aler export const ALERT_DATA_GRID = '[data-test-subj="dataGridWrapper"]'; +export const ALERTS = '[data-test-subj="events-viewer-panel"][data-test-subj="event"]'; + +export const ALERTS_COUNT = + '[data-test-subj="events-viewer-panel"] [data-test-subj="server-side-event-count"]'; + +export const ALERTS_TREND_SIGNAL_RULE_NAME_PANEL = + '[data-test-subj="render-content-kibana.alert.rule.name"]'; + export const CLOSE_ALERT_BTN = '[data-test-subj="close-alert-status"]'; export const CLOSE_SELECTED_ALERTS_BTN = '[data-test-subj="close-alert-status"]'; export const CLOSED_ALERTS_FILTER_BTN = '[data-test-subj="closedAlerts"]'; +export const DESTINATION_IP = '[data-test-subj^=formatted-field][data-test-subj$=destination\\.ip]'; + export const EMPTY_ALERT_TABLE = '[data-test-subj="tGridEmptyState"]'; export const EXPAND_ALERT_BTN = '[data-test-subj="expand-event"]'; +export const HOST_NAME = '[data-test-subj^=formatted-field][data-test-subj$=host\\.name]'; + export const ACKNOWLEDGED_ALERTS_FILTER_BTN = '[data-test-subj="acknowledgedAlerts"]'; export const LOADING_ALERTS_PANEL = '[data-test-subj="loading-alerts-panel"]'; @@ -53,18 +63,26 @@ export const OPEN_ALERT_BTN = '[data-test-subj="open-alert-status"]'; export const OPENED_ALERTS_FILTER_BTN = '[data-test-subj="openAlerts"]'; +export const PROCESS_NAME = '[data-test-subj="formatted-field-process.name"]'; + +export const REASON = '[data-test-subj^=formatted-field][data-test-subj$=reason]'; + +export const RISK_SCORE = '[data-test-subj^=formatted-field][data-test-subj$=risk_score]'; + +export const RULE_NAME = '[data-test-subj^=formatted-field][data-test-subj$=rule\\.name]'; + export const SELECTED_ALERTS = '[data-test-subj="selectedShowBulkActionsButton"]'; export const SEND_ALERT_TO_TIMELINE_BTN = '[data-test-subj="send-alert-to-timeline-button"]'; +export const SEVERITY = '[data-test-subj^=formatted-field][data-test-subj$=severity]'; + +export const SOURCE_IP = '[data-test-subj^=formatted-field][data-test-subj$=source\\.ip]'; + export const TAKE_ACTION_POPOVER_BTN = '[data-test-subj="selectedShowBulkActionsButton"]'; export const TIMELINE_CONTEXT_MENU_BTN = '[data-test-subj="timeline-context-menu-button"]'; -export const ATTACH_ALERT_TO_CASE_BUTTON = '[data-test-subj="add-existing-case-menu-item"]'; +export const USER_NAME = '[data-test-subj^=formatted-field][data-test-subj$=user\\.name]'; -export const ALERT_COUNT_TABLE_FIRST_ROW_COUNT = - '[data-test-subj="alertsCountTable"] tr:nth-child(1) td:nth-child(2) .euiTableCellContent__text'; - -export const ALERTS_TREND_SIGNAL_RULE_NAME_PANEL = - '[data-test-subj="render-content-kibana.alert.rule.name"]'; +export const ATTACH_ALERT_TO_CASE_BUTTON = '[data-test-subj="add-existing-case-menu-item"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts index 85a4fa257a957..70ccfcea7ba2c 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts @@ -30,6 +30,28 @@ export const JSON_VIEW_TAB = '[data-test-subj="jsonViewTab"]'; export const JSON_TEXT = '[data-test-subj="jsonView"]'; +export const OVERVIEW_HOST_NAME = + '[data-test-subj="eventDetails"] [data-test-subj="host-details-button"]'; + +export const OVERVIEW_RISK_SCORE = + '[data-test-subj="eventDetails"] [data-test-subj^=formatted][data-test-subj$=risk_score]'; + +export const OVERVIEW_RULE = + '[data-test-subj="eventDetails"] [data-test-subj^=formatted][data-test-subj$=rule\\.name]'; + +export const OVERVIEW_SEVERITY = + '[data-test-subj="eventDetails"] [data-test-subj^=formatted][data-test-subj$=rule\\.severity]'; + +export const OVERVIEW_STATUS = '[data-test-subj="eventDetails"] [data-test-subj$=status]'; + +export const OVERVIEW_THRESHOLD_COUNT = + '[data-test-subj="eventDetails"] [data-test-subj^=formatted][data-test-subj$=threshold_result\\.count]'; + +export const OVERVIEW_THRESHOLD_VALUE = + '[data-test-subj="eventDetails"] [data-test-subj$=threshold_result\\.terms]'; + +export const SUMMARY_VIEW = '[data-test-subj="summary-view"]'; + export const TABLE_CELL = '.euiTableRowCell'; export const TABLE_TAB = '[data-test-subj="tableTab"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timelines.ts b/x-pack/plugins/security_solution/cypress/screens/timelines.ts index 5e64e4fbb5ece..21febda41d062 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timelines.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timelines.ts @@ -11,9 +11,7 @@ export const EXPAND_NOTES_BTN = '[data-test-subj="expand-notes"]'; export const EXPORT_TIMELINE_ACTION = '[data-test-subj="export-timeline-action"]'; -export const IMPORT_BTN = '.euiButton__text'; - -export const IMPORT_BTN_POSITION = 8; +export const IMPORT_BTN = '.euiButton.euiButton--primary.euiButton--fill'; export const IMPORT_TIMELINE_BTN = '[data-test-subj="open-import-data-modal-btn"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 84b81108f8be3..e304d08f4c68f 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -149,6 +149,10 @@ export const goToRuleDetails = () => { cy.get(RULE_NAME).first().click({ force: true }); }; +export const goToTheRuleDetailsOf = (ruleName: string) => { + cy.get(RULE_NAME).contains(ruleName).click(); +}; + export const loadPrebuiltDetectionRules = () => { cy.get(LOAD_PREBUILT_RULES_BTN).should('exist').click({ force: true }); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts index 07c752a191968..d7f69638d0c55 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts @@ -12,7 +12,6 @@ import { EXPAND_NOTES_BTN, EXPORT_TIMELINE_ACTION, IMPORT_BTN, - IMPORT_BTN_POSITION, IMPORT_TIMELINE_BTN, INPUT_FILE, TIMELINES_TABLE, @@ -34,7 +33,7 @@ export const importTimeline = (timeline: string) => { cy.get(IMPORT_TIMELINE_BTN).click(); cy.get(INPUT_FILE).should('exist'); cy.get(INPUT_FILE).trigger('click', { force: true }).attachFile(timeline).trigger('change'); - cy.get(IMPORT_BTN).eq(IMPORT_BTN_POSITION).click({ force: true }); + cy.get(IMPORT_BTN).last().click({ force: true }); cy.get(INPUT_FILE).should('not.exist'); }; diff --git a/x-pack/plugins/security_solution/cypress/upgrade_integration/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/upgrade_integration/custom_query_rule.spec.ts index 1af7a3f9bed03..e4464ae43dd62 100644 --- a/x-pack/plugins/security_solution/cypress/upgrade_integration/custom_query_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/upgrade_integration/custom_query_rule.spec.ts @@ -4,8 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { ALERT_DETAILS_CELLS, SERVER_SIDE_EVENT_COUNT } from '../screens/alerts_detection_rules'; +import { + DESTINATION_IP, + HOST_NAME, + PROCESS_NAME, + REASON, + RISK_SCORE, + RULE_NAME, + SEVERITY, + SOURCE_IP, + USER_NAME, +} from '../screens/alerts'; +import { SERVER_SIDE_EVENT_COUNT } from '../screens/alerts_detection_rules'; import { ADDITIONAL_LOOK_BACK_DETAILS, ABOUT_DETAILS, @@ -24,7 +34,7 @@ import { } from '../screens/rule_details'; import { waitForPageToBeLoaded } from '../tasks/common'; -import { waitForRulesTableToBeLoaded, goToRuleDetails } from '../tasks/alerts_detection_rules'; +import { waitForRulesTableToBeLoaded, goToTheRuleDetailsOf } from '../tasks/alerts_detection_rules'; import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../urls/navigation'; @@ -38,8 +48,8 @@ const alert = { reason: 'file event with process test, file The file to test, by Security Solution on security-solution.local created low alert Custom query rule for upgrade.', hostName: 'security-solution.local', - username: 'test', - processName: 'The file to test', + username: 'Security Solution', + processName: 'test', fileName: 'The file to test', sourceIp: '127.0.0.1', destinationIp: '127.0.0.2', @@ -49,7 +59,7 @@ const rule = { customQuery: '*:*', name: 'Custom query rule for upgrade', description: 'My description', - index: ['auditbeat-*'], + index: ['auditbeat-custom*'], severity: 'Low', riskScore: '7', timelineTemplate: 'none', @@ -62,7 +72,7 @@ describe('After an upgrade, the custom query rule', () => { before(() => { loginAndWaitForPage(DETECTIONS_RULE_MANAGEMENT_URL); waitForRulesTableToBeLoaded(); - goToRuleDetails(); + goToTheRuleDetailsOf(rule.name); waitForPageToBeLoaded(); }); @@ -89,54 +99,15 @@ describe('After an upgrade, the custom query rule', () => { }); }); - it('Displays the alert details', () => { - cy.get(ALERT_DETAILS_CELLS).first().focus(); - cy.get(ALERT_DETAILS_CELLS).first().type('{rightarrow}'); - cy.get(ALERT_DETAILS_CELLS) - .contains(alert.rule) - .then(($el) => { - cy.wrap($el).type('{rightarrow}'); - }); - cy.get(ALERT_DETAILS_CELLS) - .contains(alert.severity) - .then(($el) => { - cy.wrap($el).type('{rightarrow}'); - }); - cy.get(ALERT_DETAILS_CELLS) - .contains(alert.riskScore) - .then(($el) => { - cy.wrap($el).type('{rightarrow}'); - }); - cy.get(ALERT_DETAILS_CELLS) - .contains(alert.reason) - .then(($el) => { - cy.wrap($el).type('{rightarrow}'); - }); - cy.get(ALERT_DETAILS_CELLS) - .contains(alert.hostName) - .then(($el) => { - cy.wrap($el).type('{rightarrow}'); - }); - cy.get(ALERT_DETAILS_CELLS) - .contains(alert.username) - .then(($el) => { - cy.wrap($el).type('{rightarrow}'); - }); - cy.get(ALERT_DETAILS_CELLS) - .contains(alert.processName) - .then(($el) => { - cy.wrap($el).type('{rightarrow}'); - }); - cy.get(ALERT_DETAILS_CELLS) - .contains(alert.fileName) - .then(($el) => { - cy.wrap($el).type('{rightarrow}'); - }); - cy.get(ALERT_DETAILS_CELLS) - .contains(alert.sourceIp) - .then(($el) => { - cy.wrap($el).type('{rightarrow}'); - }); - cy.get(ALERT_DETAILS_CELLS).contains(alert.destinationIp); + it('Displays the alert details at the tgrid', () => { + cy.get(RULE_NAME).should('have.text', alert.rule); + cy.get(SEVERITY).should('have.text', alert.severity); + cy.get(RISK_SCORE).should('have.text', alert.riskScore); + cy.get(REASON).should('have.text', alert.reason).type('{rightarrow}'); + cy.get(HOST_NAME).should('have.text', alert.hostName); + cy.get(USER_NAME).should('have.text', alert.username); + cy.get(PROCESS_NAME).should('have.text', alert.processName); + cy.get(SOURCE_IP).should('have.text', alert.sourceIp); + cy.get(DESTINATION_IP).should('have.text', alert.destinationIp); }); }); diff --git a/x-pack/plugins/security_solution/cypress/upgrade_integration/import_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/upgrade_integration/import_timeline.spec.ts index 3ce3937b3e4b2..464c42d9fc220 100644 --- a/x-pack/plugins/security_solution/cypress/upgrade_integration/import_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/upgrade_integration/import_timeline.spec.ts @@ -54,7 +54,7 @@ const username = 'elastic'; const timelineDetails = { dateStart: 'Oct 11, 2020 @ 00:00:00.000', dateEnd: 'Oct 11, 2030 @ 17:13:15.851', - queryTab: 'Query2', + queryTab: 'Query4', correlationTab: 'Correlation', analyzerTab: 'Analyzer', notesTab: 'Notes2', diff --git a/x-pack/plugins/security_solution/cypress/upgrade_integration/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/upgrade_integration/threshold_rule.spec.ts new file mode 100644 index 0000000000000..b6dbcd0e3232c --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/upgrade_integration/threshold_rule.spec.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HOST_NAME, REASON, RISK_SCORE, RULE_NAME, SEVERITY } from '../screens/alerts'; +import { SERVER_SIDE_EVENT_COUNT } from '../screens/alerts_detection_rules'; +import { + ADDITIONAL_LOOK_BACK_DETAILS, + ABOUT_DETAILS, + ABOUT_RULE_DESCRIPTION, + CUSTOM_QUERY_DETAILS, + DEFINITION_DETAILS, + getDetails, + INDEX_PATTERNS_DETAILS, + RISK_SCORE_DETAILS, + RULE_NAME_HEADER, + RULE_TYPE_DETAILS, + RUNS_EVERY_DETAILS, + SCHEDULE_DETAILS, + SEVERITY_DETAILS, + THRESHOLD_DETAILS, + TIMELINE_TEMPLATE_DETAILS, +} from '../screens/rule_details'; + +import { expandFirstAlert } from '../tasks/alerts'; +import { waitForPageToBeLoaded } from '../tasks/common'; +import { waitForRulesTableToBeLoaded, goToRuleDetails } from '../tasks/alerts_detection_rules'; +import { loginAndWaitForPage } from '../tasks/login'; + +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../urls/navigation'; +import { + OVERVIEW_HOST_NAME, + OVERVIEW_RISK_SCORE, + OVERVIEW_RULE, + OVERVIEW_SEVERITY, + OVERVIEW_STATUS, + OVERVIEW_THRESHOLD_COUNT, + OVERVIEW_THRESHOLD_VALUE, + SUMMARY_VIEW, +} from '../screens/alerts_details'; + +const EXPECTED_NUMBER_OF_ALERTS = '1'; + +const alert = { + rule: 'Threshold rule', + severity: 'medium', + riskScore: '17', + reason: 'event created medium alert Threshold rule.', + hostName: 'security-solution.local', + thresholdCount: '2', +}; + +const rule = { + customQuery: '*:*', + name: 'Threshold rule', + description: 'Threshold rule for testing upgrade', + index: ['auditbeat-threshold*'], + severity: 'Medium', + riskScore: '17', + timelineTemplate: 'none', + runsEvery: '60s', + lookBack: '2999999m', + timeline: 'None', + thresholdField: 'host.name', + threholdValue: '1', +}; + +describe('After an upgrade, the threshold rule', () => { + before(() => { + loginAndWaitForPage(DETECTIONS_RULE_MANAGEMENT_URL); + waitForRulesTableToBeLoaded(); + goToRuleDetails(); + waitForPageToBeLoaded(); + }); + + it('Has the expected alerts number', () => { + cy.get(SERVER_SIDE_EVENT_COUNT).contains(EXPECTED_NUMBER_OF_ALERTS); + }); + + it('Displays the rule details', () => { + cy.get(RULE_NAME_HEADER).should('contain', rule.name); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description); + cy.get(ABOUT_DETAILS).within(() => { + getDetails(SEVERITY_DETAILS).should('have.text', rule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', rule.riskScore); + }); + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', rule.index.join('')); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.customQuery); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', rule.timeline); + getDetails(THRESHOLD_DETAILS).should( + 'have.text', + `Results aggregated by ${rule.thresholdField} >= ${rule.threholdValue}` + ); + }); + cy.get(SCHEDULE_DETAILS).within(() => { + getDetails(RUNS_EVERY_DETAILS).should('have.text', rule.runsEvery); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should('have.text', rule.lookBack); + }); + }); + + it('Displays the alert details in the TGrid', () => { + cy.get(RULE_NAME).should('have.text', alert.rule); + cy.get(SEVERITY).should('have.text', alert.severity); + cy.get(RISK_SCORE).should('have.text', alert.riskScore); + cy.get(REASON).should('have.text', alert.reason); + cy.get(HOST_NAME).should('have.text', alert.hostName); + }); + + it('Displays the Overview alert details in the alert flyout', () => { + expandFirstAlert(); + + cy.get(OVERVIEW_STATUS).should('have.text', 'open'); + cy.get(OVERVIEW_RULE).should('have.text', alert.rule); + cy.get(OVERVIEW_SEVERITY).should('have.text', alert.severity); + cy.get(OVERVIEW_RISK_SCORE).should('have.text', alert.riskScore); + cy.get(OVERVIEW_HOST_NAME).should('have.text', alert.hostName); + cy.get(OVERVIEW_THRESHOLD_COUNT).should('have.text', alert.thresholdCount); + cy.get(OVERVIEW_THRESHOLD_VALUE).should('have.text', alert.hostName); + cy.get(SUMMARY_VIEW).should('contain', `${rule.thresholdField} [threshold]`); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index fc440197e8349..fff5b465956de 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -21,6 +21,7 @@ import { SourcererScopeName } from '../../store/sourcerer/model'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; import type { EntityType } from '../../../../../timelines/common'; +import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; export interface OwnProps { end: string; @@ -79,6 +80,7 @@ const AlertsTableComponent: React.FC = ({ const dispatch = useDispatch(); const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]); const { filterManager } = useKibana().services.data.query; + const ACTION_BUTTON_COUNT = 3; const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); @@ -104,6 +106,8 @@ const AlertsTableComponent: React.FC = ({ ); }, [dispatch, filterManager, tGridEnabled, timelineId]); + const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []); + return ( = ({ end={endDate} entityType={entityType} id={timelineId} + leadingControlColumns={leadingControlColumns} renderCellValue={DefaultCellRenderer} rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.default} diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx index 6795557f17f1a..ad86b0f362234 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useMemo } from 'react'; +import React, { memo, useMemo, useRef, useEffect } from 'react'; import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiTextColor, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useTestIdGenerator } from '../../../../management/components/hooks/use_test_id_generator'; @@ -28,12 +28,20 @@ export interface EndpointHostIsolationStatusProps { export const EndpointHostIsolationStatus = memo( ({ isIsolated, pendingIsolate = 0, pendingUnIsolate = 0, 'data-test-subj': dataTestSubj }) => { const getTestId = useTestIdGenerator(dataTestSubj); - const isPendingStatuseDisabled = useIsExperimentalFeatureEnabled( + const isPendingStatusDisabled = useIsExperimentalFeatureEnabled( 'disableIsolationUIPendingStatuses' ); + const wasReleasing = useRef(false); + const wasIsolating = useRef(false); + + useEffect(() => { + wasReleasing.current = pendingIsolate === 0 && pendingUnIsolate > 0; + wasIsolating.current = pendingIsolate > 0 && pendingUnIsolate === 0; + }, [pendingIsolate, pendingUnIsolate]); + return useMemo(() => { - if (isPendingStatuseDisabled) { + if (isPendingStatusDisabled) { // If nothing is pending and host is not currently isolated, then render nothing if (!isIsolated) { return null; @@ -49,21 +57,23 @@ export const EndpointHostIsolationStatus = memo - - - ); + // If nothing is pending + if (!(pendingIsolate || pendingUnIsolate)) { + // and host is either releasing and or currently released, then render nothing + if ((!wasIsolating.current && wasReleasing.current) || !isIsolated) { + return null; + } + // else host was isolating or is isolated, then show isolation badge + else if ((!isIsolated && wasIsolating.current && !wasReleasing.current) || isIsolated) { + return ( + + + + ); + } } // If there are multiple types of pending isolation actions, then show count of actions with tooltip that displays breakdown @@ -136,7 +146,7 @@ export const EndpointHostIsolationStatus = memo ({ - useKibana: () => ({ - services: { - application: { - navigateToApp: jest.fn(), - getUrlForApp: jest.fn(), - capabilities: { - siem: { crud_alerts: true, read_alerts: true }, - }, - }, - uiSettings: { - get: jest.fn(), - }, - savedObjects: { - client: {}, - }, - timelines: { ...mockTimelines }, - }, - }), - useToasts: jest.fn().mockReturnValue({ - addError: jest.fn(), - addSuccess: jest.fn(), - addWarning: jest.fn(), - }), - useGetUserCasesPermissions: jest.fn(), - useDateFormat: jest.fn(), - useTimeZone: jest.fn(), -})); - -jest.mock('../../hooks/use_experimental_features'); -const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; - -jest.mock('../../../timelines/components/graph_overlay', () => ({ - GraphOverlay: jest.fn(() =>
), -})); - -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - return { - ...original, - useDispatch: () => mockDispatch, - }; -}); - -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); - return { - ...original, - useDataGridColumnSorting: jest.fn(), - }; -}); -jest.mock('../../../timelines/containers', () => ({ - useTimelineEvents: jest.fn(), -})); - -jest.mock('../../components/url_state/normalize_time_range.ts'); - -const mockUseSourcererDataView: jest.Mock = useSourcererDataView as jest.Mock; -jest.mock('../../containers/sourcerer'); - -const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; -jest.mock('use-resize-observer/polyfilled'); -mockUseResizeObserver.mockImplementation(() => ({})); - -const mockUseTimelineEvents: jest.Mock = useTimelineEvents as jest.Mock; -jest.mock('../../../timelines/containers'); - -const from = '2019-08-26T22:10:56.791Z'; -const to = '2019-08-27T22:10:56.794Z'; - -const defaultMocks = { - browserFields: mockBrowserFields, - docValueFields: mockDocValueFields, - runtimeMappings: mockRuntimeMappings, - indexPattern: mockIndexPattern, - loading: false, - selectedPatterns: mockIndexNames, -}; - -const utilityBar = (refetch: inputsModel.Refetch, totalCount: number) => ( -
-); - -const eventsViewerDefaultProps = { - browserFields: {}, - columns: [], - dataProviders: [], - deletedEventIds: [], - docValueFields: [], - end: to, - entityType: EntityType.ALERTS, - filters: [], - id: TimelineId.detectionsPage, - indexNames: mockIndexNames, - indexPattern: mockIndexPattern, - isLive: false, - isLoadingIndexPattern: false, - itemsPerPage: 10, - itemsPerPageOptions: [], - kqlMode: 'filter' as KqlMode, - query: { - query: '', - language: 'kql', - }, - renderCellValue: DefaultCellRenderer, - rowRenderers: defaultRowRenderers, - runtimeMappings: {}, - start: from, - sort: [ - { - columnId: 'foo', - columnType: 'number', - sortDirection: 'asc' as SortDirection, - }, - ], - scopeId: SourcererScopeName.timeline, - utilityBar, -}; - -describe('EventsViewer', () => { - const mount = useMountAppended(); - - let testProps = { - defaultCellActions, - defaultModel: eventsDefaultModel, - end: to, - entityType: EntityType.ALERTS, - id: TimelineId.test, - renderCellValue: DefaultCellRenderer, - rowRenderers: defaultRowRenderers, - start: from, - scopeId: SourcererScopeName.timeline, - }; - beforeEach(() => { - mockUseTimelineEvents.mockReset(); - }); - beforeAll(() => { - mockUseSourcererDataView.mockImplementation(() => defaultMocks); - }); - - describe('event details', () => { - useIsExperimentalFeatureEnabledMock.mockReturnValue(false); - beforeEach(() => { - mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponseWithEvents]); - }); - - test('call the right reduce action to show event details', async () => { - const wrapper = mount( - - - - ); - - act(() => { - wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click'); - }); - - await waitFor(() => { - expect(mockDispatch).toBeCalledTimes(3); - expect(mockDispatch.mock.calls[1][0]).toEqual({ - payload: { - id: 'test', - isLoading: false, - }, - type: 'x-pack/timelines/t-grid/UPDATE_LOADING', - }); - }); - }); - }); - - describe('rendering', () => { - beforeEach(() => { - mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]); - }); - - test('it renders the "Showing..." subtitle with the expected event count by default', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().text()).toEqual( - 'Showing: 12 events' - ); - }); - - test('should not render the "Showing..." subtitle with the expected event count if showTotalCount is set to false ', () => { - const disableSubTitle = { - ...eventsViewerDefaultProps, - showTotalCount: false, - }; - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().text()).toEqual(''); - }); - - test('it renders the Fields Browser as a settings gear', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="field-browser"]`).first().exists()).toBe(true); - }); - - test('it renders the footer containing the pagination', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="timeline-pagination"]`).first().exists()).toBe(true); - }); - - defaultHeaders.forEach((header) => { - test(`it renders the ${header.id} default EventsViewer column header`, () => { - testProps = { - ...testProps, - // Update with a new id, to force columns back to default. - id: TimelineId.alternateTest, - }; - const wrapper = mount( - - - - ); - - defaultHeaders.forEach((h) => { - expect(wrapper.find(`[data-test-subj="header-text-${header.id}"]`).first().exists()).toBe( - true - ); - }); - }); - }); - }); - - describe('loading', () => { - beforeAll(() => { - mockUseSourcererDataView.mockImplementation(() => ({ ...defaultMocks, loading: true })); - }); - beforeEach(() => { - mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]); - }); - - test('it does NOT render fetch index pattern is loading', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( - false - ); - }); - - test('it does NOT render when start is empty', () => { - testProps = { - ...testProps, - start: '', - }; - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( - false - ); - }); - - test('it does NOT render when end is empty', () => { - testProps = { - ...testProps, - end: '', - }; - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( - false - ); - }); - }); - - describe('headerFilterGroup', () => { - beforeEach(() => { - mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]); - }); - - test('it renders the provided headerFilterGroup', () => { - const wrapper = mount( - - - } - /> - - ); - expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true); - }); - - test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is undefined', () => { - const wrapper = mount( - - - } - /> - - ); - expect( - wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first() - ).not.toHaveStyleRule('visibility', 'hidden'); - }); - - test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is an empty string', () => { - const wrapper = mount( - - - } - /> - - ); - expect( - wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first() - ).not.toHaveStyleRule('visibility', 'hidden'); - }); - - test('it does NOT have a visible HeaderFilterGroupWrapper when Resolver is showing, because graphEventId is a valid id', () => { - const wrapper = mount( - - - } - /> - - ); - expect( - wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first() - ).toHaveStyleRule('visibility', 'hidden'); - }); - - test('it (still) renders an invisible headerFilterGroup (to maintain state while hidden) when Resolver is showing, because graphEventId is a valid id', () => { - const wrapper = mount( - - - } - /> - - ); - expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true); - }); - }); - - describe('utilityBar', () => { - beforeEach(() => { - mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]); - }); - - test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is undefined', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true); - }); - - test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is an empty string', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true); - }); - - test('it does NOT render the provided utilityBar when Resolver is showing, because graphEventId is a valid id', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(false); - }); - }); - - describe('header inspect button', () => { - beforeEach(() => { - mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]); - }); - - test('it renders the inspect button when Resolver is NOT showing, because graphEventId is undefined', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true); - }); - - test('it renders the inspect button when Resolver is NOT showing, because graphEventId is an empty string', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true); - }); - - test('it does NOT render the inspect button when Resolver is showing, because graphEventId is a valid id', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(false); - }); - }); -}); 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 deleted file mode 100644 index 5a3aa2e6dc38a..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ /dev/null @@ -1,395 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; - -import { useDispatch } from 'react-redux'; -import { DataViewBase, Filter, Query } from '@kbn/es-query'; -import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { Direction } from '../../../../common/search_strategy'; -import { BrowserFields, DocValueFields } from '../../containers/source'; -import { useTimelineEvents } from '../../../timelines/containers'; -import { useKibana } from '../../lib/kibana'; -import { KqlMode } from '../../../timelines/store/timeline/model'; -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'; -import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { Footer, footerHeight } from '../../../timelines/components/timeline/footer'; -import { - calculateTotalPages, - combineQueries, - resolverIsShowing, -} from '../../../timelines/components/timeline/helpers'; -import { TimelineRefetch } from '../../../timelines/components/timeline/refetch_timeline'; -import { EventDetailsWidthProvider } from './event_details_width_context'; -import * as i18n from './translations'; -import { esQuery } from '../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../store'; -import { ExitFullScreen } from '../exit_full_screen'; -import { useGlobalFullScreen } from '../../containers/use_full_screen'; -import { - ColumnHeaderOptions, - ControlColumnProps, - RowRenderer, - TimelineId, - TimelineTabs, -} from '../../../../common/types/timeline'; -import { GraphOverlay } from '../../../timelines/components/graph_overlay'; -import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; -import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; -import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline'; -import { useDeepEqualSelector } from '../../hooks/use_selector'; -import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; -import { TimelineContext } from '../../../../../timelines/public'; - -export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px -const UTILITY_BAR_HEIGHT = 19; // px -const COMPACT_HEADER_HEIGHT = EVENTS_VIEWER_HEADER_HEIGHT - UTILITY_BAR_HEIGHT; // px - -const UtilityBar = styled.div` - height: ${UTILITY_BAR_HEIGHT}px; -`; - -const TitleText = styled.span` - margin-right: 12px; -`; - -const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>` - display: flex; - flex-direction: column; - - ${({ $isFullScreen }) => - $isFullScreen && - ` - border: 0; - box-shadow: none; - padding-top: 0; - padding-bottom: 0; - `} -`; - -const TitleFlexGroup = styled(EuiFlexGroup)` - margin-top: 8px; -`; - -const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({ - className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`, -}))` - width: 100%; - overflow: hidden; - flex: 1; - display: flex; - flex-direction: column; -`; - -const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` - overflow: hidden; - margin: 0; - display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; -`; - -const ScrollableFlexItem = styled(EuiFlexItem)` - overflow: auto; -`; - -/** - * Hides stateful headerFilterGroup implementations, but prevents the component - * from being unmounted, to preserve the state of the component - */ -const HeaderFilterGroupWrapper = styled.header<{ show: boolean }>` - ${({ show }) => (show ? '' : 'visibility: hidden;')} -`; - -interface Props { - browserFields: BrowserFields; - columns: ColumnHeaderOptions[]; - dataProviders: DataProvider[]; - deletedEventIds: Readonly; - docValueFields: DocValueFields[]; - end: string; - filters: Filter[]; - headerFilterGroup?: React.ReactNode; - id: TimelineId; - indexNames: string[]; - indexPattern: DataViewBase; - isLive: boolean; - isLoadingIndexPattern: boolean; - itemsPerPage: number; - itemsPerPageOptions: number[]; - kqlMode: KqlMode; - query: Query; - onRuleChange?: () => void; - renderCellValue: (props: CellValueElementProps) => React.ReactNode; - rowRenderers: RowRenderer[]; - runtimeMappings: MappingRuntimeFields; - start: string; - sort: Sort[]; - showTotalCount?: boolean; - utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; - // If truthy, the graph viewer (Resolver) is showing - graphEventId: string | undefined; -} - -const EventsViewerComponent: React.FC = ({ - browserFields, - columns, - dataProviders, - deletedEventIds, - docValueFields, - end, - filters, - headerFilterGroup, - id, - indexNames, - indexPattern, - isLive, - isLoadingIndexPattern, - itemsPerPage, - itemsPerPageOptions, - kqlMode, - onRuleChange, - query, - renderCellValue, - rowRenderers, - runtimeMappings, - start, - sort, - showTotalCount = true, - utilityBar, - graphEventId, -}) => { - const dispatch = useDispatch(); - const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); - const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; - const kibana = useKibana(); - const [isQueryLoading, setIsQueryLoading] = useState(false); - - useEffect(() => { - dispatch(timelineActions.updateIsLoading({ id, isLoading: isQueryLoading })); - }, [dispatch, id, isQueryLoading]); - - const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); - const unit = useMemo(() => (n: number) => i18n.UNIT(n), []); - const { queryFields, title } = useDeepEqualSelector((state) => getManageTimeline(state, id)); - - const justTitle = useMemo(() => {title}, [title]); - - const titleWithExitFullScreen = useMemo( - () => ( - - {justTitle} - - - - - ), - [globalFullScreen, justTitle, setGlobalFullScreen] - ); - - const combinedQueries = combineQueries({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - dataProviders, - indexPattern, - browserFields, - filters, - kqlQuery: query, - kqlMode, - isEventViewer: true, - }); - - const canQueryTimeline = useMemo( - () => - combinedQueries != null && - isLoadingIndexPattern != null && - !isLoadingIndexPattern && - !isEmpty(start) && - !isEmpty(end), - [isLoadingIndexPattern, combinedQueries, start, end] - ); - - const fields = useMemo( - () => [...columnsHeader.map((c) => c.id), ...(queryFields ?? [])], - [columnsHeader, queryFields] - ); - - const sortField = useMemo( - () => - sort.map(({ columnId, columnType, sortDirection }) => ({ - field: columnId, - type: columnType, - direction: sortDirection as Direction, - })), - [sort] - ); - - const [loading, { events, updatedAt, inspect, loadPage, pageInfo, refetch, totalCount = 0 }] = - useTimelineEvents({ - docValueFields, - fields, - filterQuery: combinedQueries?.filterQuery, - id, - indexNames, - limit: itemsPerPage, - runtimeMappings, - sort: sortField, - startDate: start, - endDate: end, - skip: !canQueryTimeline || combinedQueries?.filterQuery === undefined, // When the filterQuery comes back as undefined, it means an error has been thrown and the request should be skipped - }); - - const totalCountMinusDeleted = useMemo( - () => (totalCount > 0 ? totalCount - deletedEventIds.length : 0), - [deletedEventIds.length, totalCount] - ); - - const subtitle = useMemo( - () => - showTotalCount - ? `${i18n.SHOWING}: ${totalCountMinusDeleted.toLocaleString()} ${unit( - totalCountMinusDeleted - )}` - : null, - [showTotalCount, totalCountMinusDeleted, unit] - ); - - const nonDeletedEvents = useMemo( - () => events.filter((e) => !deletedEventIds.includes(e._id)), - [deletedEventIds, events] - ); - - const HeaderSectionContent = useMemo( - () => - headerFilterGroup && ( - - {headerFilterGroup} - - ), - [graphEventId, headerFilterGroup] - ); - - useEffect(() => { - setIsQueryLoading(loading); - }, [loading]); - - const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn]; - const trailingControlColumns: ControlColumnProps[] = []; - const timelineContext = useMemo(() => ({ timelineId: id }), [id]); - return ( - - {canQueryTimeline ? ( - - <> - - {HeaderSectionContent} - - {utilityBar && !resolverIsShowing(graphEventId) && ( - {utilityBar?.(refetch, totalCountMinusDeleted)} - )} - - - - - {graphEventId && } - - - -