diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index 4966a0b5063175..f4e62648a97413 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -8,8 +8,16 @@ jobs: name: Assign issue or PR to project based on label steps: - name: Assign to project - uses: elastic/github-actions/project-assigner@v2.0.0 + uses: elastic/github-actions/project-assigner@v2.1.0 id: project_assigner with: - issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}]' + issue-mappings: | + [ + {"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, + {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, + {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, + {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}, + {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}, + {"label": "Team:Security", "projectNumber": 320, "columnName": "Awaiting triage", "projectScope": "org"} + ] ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} diff --git a/config/kibana.yml b/config/kibana.yml index eefb6bb8bacdab..dea9849f17b286 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -42,6 +42,10 @@ #elasticsearch.username: "kibana_system" #elasticsearch.password: "pass" +# Kibana can also authenticate to Elasticsearch via "service account tokens". +# If may use this token instead of a username/password. +# elasticsearch.serviceAccountToken: "my_token" + # Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively. # These settings enable SSL for outgoing requests from the Kibana server to the browser. #server.ssl.enabled: false diff --git a/docs/apm/agent-configuration.asciidoc b/docs/apm/agent-configuration.asciidoc index 2574d254ac14c1..f2e07412c4a38f 100644 --- a/docs/apm/agent-configuration.asciidoc +++ b/docs/apm/agent-configuration.asciidoc @@ -43,6 +43,7 @@ Supported configurations are also tagged with the image:./images/dynamic-config. [horizontal] Go Agent:: {apm-go-ref}/configuration.html[Configuration reference] +iOS agent:: _Not yet supported_ Java Agent:: {apm-java-ref}/configuration.html[Configuration reference] .NET Agent:: {apm-dotnet-ref}/configuration.html[Configuration reference] Node.js Agent:: {apm-node-ref}/configuration.html[Configuration reference] diff --git a/docs/apm/apm-alerts.asciidoc b/docs/apm/apm-alerts.asciidoc index 3e3e2b178ff10b..42016ac08bfc78 100644 --- a/docs/apm/apm-alerts.asciidoc +++ b/docs/apm/apm-alerts.asciidoc @@ -1,69 +1,57 @@ [role="xpack"] [[apm-alerts]] -=== Alerts +=== Alerts and rules ++++ Create an alert ++++ +The APM app allows you to define **rules** to detect complex conditions within your APM data +and trigger built-in **actions** when those conditions are met. -The APM app integrates with Kibana's {kibana-ref}/alerting-getting-started.html[alerting and actions] feature. -It provides a set of built-in **actions** and APM specific threshold **alerts** for you to use -and enables central management of all alerts from <>. +The following **rules** are supported: + +* Latency anomaly rule: +Alert when latency of a service is abnormal +* Transaction error rate threshold rule: +Alert when the service's transaction error rate is above the defined threshold +* Error count threshold rule: +Alert when the number of errors in a service exceeds a defined threshold [role="screenshot"] image::apm/images/apm-alert.png[Create an alert in the APM app] -For a walkthrough of the alert flyout panel, including detailed information on each configurable property, -see Kibana's <>. - -The APM app supports four different types of alerts: - -* Transaction duration anomaly: -alerts when the service's transaction duration reaches a certain anomaly score -* Transaction duration threshold: -alerts when the service's transaction duration exceeds a given time limit over a given time frame -* Transaction error rate threshold: -alerts when the service's transaction error rate is above the selected rate over a given time frame -* Error count threshold: -alerts when service exceeds a selected number of errors over a given time frame +For a complete walkthrough of the **Create rule** flyout panel, including detailed information on each configurable property, +see Kibana's <>. -Below, we'll walk through the creation of two of these alerts. +Below, we'll walk through the creation of two APM rules. [float] [[apm-create-transaction-alert]] -=== Example: create a transaction duration alert +=== Example: create a latency anomaly rule -Transaction duration alerts trigger when the duration of a specific transaction type in a service exceeds a defined threshold. -This guide will create an alert for the `opbeans-java` service based on the following criteria: +Latency anomaly rules trigger when the latency of a service is abnormal. +This guide will create an alert for all services based on the following criteria: -* Environment: Production -* Transaction type: `transaction.type:request` -* Average request is above `1500ms` for the last 5 minutes -* Check every 10 minutes, and repeat the alert every 30 minutes -* Send the alert via Slack +* Environment: production +* Severity level: critical +* Run every five minutes +* Send an alert to a Slack channel only when the rule status changes -From the APM app, navigate to the `opbeans-java` service and select -**Alerts** > **Create threshold alert** > **Transaction duration**. +From any page in the APM app, select **Alerts and rules** > **Latency** > **Create anomaly rule**. +Change the name of the alert, but do not edit the tags. -`Transaction duration | opbeans-java` is automatically set as the name of the alert, -and `apm` and `service.name:opbeans-java` are added as tags. -It's fine to change the name of the alert, but do not edit the tags. +Based on the criteria above, define the following rule details: -Based on the alert criteria, define the following alert details: +* **Check every** - `5 minutes` +* **Notify** - "Only on status change" +* **Environment** - `all` +* **Has anomaly with severity** - `critical` -* **Check every** - `10 minutes` -* **Notify every** - `30 minutes` -* **TYPE** - `request` -* **WHEN** - `avg` -* **IS ABOVE** - `1500ms` -* **FOR THE LAST** - `5 minutes` - -Select an action type. -Multiple action types can be selected, but in this example, we want to post to a Slack channel. +Next, add a connector. Multiple connectors can be selected, but in this example we're interested in Slack. Select **Slack** > **Create a connector**. Enter a name for the connector, -and paste the webhook URL. +and paste your Slack webhook URL. See Slack's webhook documentation if you need to create one. A default message is provided as a starting point for your alert. @@ -72,35 +60,32 @@ to pass additional alert values at the time a condition is detected to an action A list of available variables can be accessed by selecting the **add variable** button image:apm/images/add-variable.png[add variable button]. -Select **Save**. The alert has been created and is now active! +Click **Save**. The rule has been created and is now active! [float] [[apm-create-error-alert]] -=== Example: create an error rate alert +=== Example: create an error count threshold alert -Error rate alerts trigger when the number of errors in a service exceeds a defined threshold. -This guide creates an alert for the `opbeans-python` service based on the following criteria: +The error count threshold alert triggers when the number of errors in a service exceeds a defined threshold. +This guide will create an alert for all services based on the following criteria: -* Environment: Production +* All environments * Error rate is above 25 for the last minute -* Check every 1 minute, and repeat the alert every 10 minutes -* Send the alert via email to the `opbeans-python` team - -From the APM app, navigate to the `opbeans-python` service and select -**Alerts** > **Create threshold alert** > **Error rate**. +* Check every 1 minute, and alert every time the rule is active +* Send the alert via email to the site reliability team -`Error rate | opbeans-python` is automatically set as the name of the alert, -and `apm` and `service.name:opbeans-python` are added as tags. -It's fine to change the name of the alert, but do not edit the tags. +From any page in the APM app, select **Alerts and rules** > **Error count** > **Create threshold rule**. +Change the name of the alert, but do not edit the tags. -Based on the alert criteria, define the following alert details: +Based on the criteria above, define the following rule details: * **Check every** - `1 minute` -* **Notify every** - `10 minutes` -* **IS ABOVE** - `25 errors` -* **FOR THE LAST** - `1 minute` +* **Notify** - "Every time alert is active" +* **Environment** - `all` +* **Is above** - `25 errors` +* **For the last** - `1 minute` -Select the **Email** action type and click **Create a connector**. +Select the **Email** connector and click **Create a connector**. Fill out the required details: sender, host, port, etc., and click **save**. A default message is provided as a starting point for your alert. @@ -109,14 +94,14 @@ to pass additional alert values at the time a condition is detected to an action A list of available variables can be accessed by selecting the **add variable** button image:apm/images/add-variable.png[add variable button]. -Select **Save**. The alert has been created and is now active! +Click **Save**. The alert has been created and is now active! [float] [[apm-alert-manage]] -=== Manage alerts and actions +=== Manage alerts and rules -From the APM app, select **Alerts** > **View active alerts** to be taken to the Kibana alerts and actions management page. -From this page, you can create, edit, disable, mute, and delete alerts, and create, edit, and disable connectors. +From the APM app, select **Alerts and rules** > **Manage rules** to be taken to the Kibana **Rules and Connectors** page. +From this page, you can disable, mute, and delete APM alerts. [float] [[apm-alert-more-info]] @@ -126,4 +111,4 @@ See {kibana-ref}/alerting-getting-started.html[alerting and actions] for more in NOTE: If you are using an **on-premise** Elastic Stack deployment with security, communication between Elasticsearch and Kibana must have TLS configured. -More information is in the alerting {kibana-ref}/alerting-setup.html#alerting-prerequisites[prerequisites]. \ No newline at end of file +More information is in the alerting {kibana-ref}/alerting-setup.html#alerting-prerequisites[prerequisites]. diff --git a/docs/apm/filters.asciidoc b/docs/apm/filters.asciidoc index 56602ab7c05c90..c0ea81c87378bb 100644 --- a/docs/apm/filters.asciidoc +++ b/docs/apm/filters.asciidoc @@ -36,6 +36,7 @@ It's vital to be consistent when naming environments in your agents. To learn how to configure service environments, see the specific agent documentation: * *Go:* {apm-go-ref}/configuration.html#config-environment[`ELASTIC_APM_ENVIRONMENT`] +* *iOS agent:* _Not yet supported_ * *Java:* {apm-java-ref}/config-core.html#config-environment[`environment`] * *.NET:* {apm-dotnet-ref}/config-core.html#config-environment[`Environment`] * *Node.js:* {apm-node-ref}/configuration.html#environment[`environment`] diff --git a/docs/apm/images/apm-agent-configuration.png b/docs/apm/images/apm-agent-configuration.png index 07398f0609187d..22fd9d75c3d730 100644 Binary files a/docs/apm/images/apm-agent-configuration.png and b/docs/apm/images/apm-agent-configuration.png differ diff --git a/docs/apm/images/apm-alert.png b/docs/apm/images/apm-alert.png index 2ac91b6b192190..a845d65dd24a53 100644 Binary files a/docs/apm/images/apm-alert.png and b/docs/apm/images/apm-alert.png differ diff --git a/docs/apm/images/apm-error-group.png b/docs/apm/images/apm-error-group.png index 359bdc6b704e94..1326e97f757d63 100644 Binary files a/docs/apm/images/apm-error-group.png and b/docs/apm/images/apm-error-group.png differ diff --git a/docs/apm/images/apm-logs-tab.png b/docs/apm/images/apm-logs-tab.png index 77aecf744bc7f5..891d2b7a1dd692 100644 Binary files a/docs/apm/images/apm-logs-tab.png and b/docs/apm/images/apm-logs-tab.png differ diff --git a/docs/apm/images/apm-services-overview.png b/docs/apm/images/apm-services-overview.png index 1c16ac5b572c3d..7aeb5f1ac379f3 100644 Binary files a/docs/apm/images/apm-services-overview.png and b/docs/apm/images/apm-services-overview.png differ diff --git a/docs/apm/images/apm-settings.png b/docs/apm/images/apm-settings.png index c821b7fb76e793..2201ed5fcaa725 100644 Binary files a/docs/apm/images/apm-settings.png and b/docs/apm/images/apm-settings.png differ diff --git a/docs/apm/images/apm-span-detail.png b/docs/apm/images/apm-span-detail.png index bacb2d372c1662..c9f55575b2232a 100644 Binary files a/docs/apm/images/apm-span-detail.png and b/docs/apm/images/apm-span-detail.png differ diff --git a/docs/apm/images/apm-traces.png b/docs/apm/images/apm-traces.png index 0e9062ee448b43..ee16f9ed16a180 100644 Binary files a/docs/apm/images/apm-traces.png and b/docs/apm/images/apm-traces.png differ diff --git a/docs/apm/images/apm-transaction-duration-dist.png b/docs/apm/images/apm-transaction-duration-dist.png index 863f493f20db49..91ae6c3a59ad26 100644 Binary files a/docs/apm/images/apm-transaction-duration-dist.png and b/docs/apm/images/apm-transaction-duration-dist.png differ diff --git a/docs/apm/images/apm-transaction-response-dist.png b/docs/apm/images/apm-transaction-response-dist.png index 2f3e69f263a28c..70e5ad7041287f 100644 Binary files a/docs/apm/images/apm-transaction-response-dist.png and b/docs/apm/images/apm-transaction-response-dist.png differ diff --git a/docs/apm/images/apm-transaction-sample.png b/docs/apm/images/apm-transaction-sample.png index 0e4bc5f3f878af..54eea902f03111 100644 Binary files a/docs/apm/images/apm-transaction-sample.png and b/docs/apm/images/apm-transaction-sample.png differ diff --git a/docs/apm/images/apm-transactions-overview.png b/docs/apm/images/apm-transactions-overview.png index be292c37e24e01..66cf739a861b7f 100644 Binary files a/docs/apm/images/apm-transactions-overview.png and b/docs/apm/images/apm-transactions-overview.png differ diff --git a/docs/apm/images/service-maps-java.png b/docs/apm/images/service-maps-java.png index d7c0786e406d94..25600b690a5bd2 100644 Binary files a/docs/apm/images/service-maps-java.png and b/docs/apm/images/service-maps-java.png differ diff --git a/docs/apm/images/service-maps.png b/docs/apm/images/service-maps.png index 190b7af3c560e8..511d8401b22f39 100644 Binary files a/docs/apm/images/service-maps.png and b/docs/apm/images/service-maps.png differ diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index 99a6205ae010e4..f43253d8194290 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -108,6 +108,7 @@ Service maps are supported for the following Agent versions: [horizontal] Go agent:: ≥ v1.7.0 +iOS agent:: _Not yet supported_ Java agent:: ≥ v1.13.0 .NET agent:: ≥ v1.3.0 Node.js agent:: ≥ v3.6.0 diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index c2a3e0bc2502dd..76006d375d0750 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -100,22 +100,22 @@ the selected transaction group. image::apm/images/apm-transaction-response-dist.png[Example view of response time distribution] [[transaction-duration-distribution]] -==== Transactions duration distribution +==== Latency distribution -This chart plots all transaction durations for the given time period. +A plot of all transaction durations for the given time period. The screenshot below shows a typical distribution, and indicates most of our requests were served quickly -- awesome! -It's the requests on the right, the ones taking longer than average, that we probably want to focus on. +It's the requests on the right, the ones taking longer than average, that we probably need to focus on. [role="screenshot"] -image::apm/images/apm-transaction-duration-dist.png[Example view of transactions duration distribution graph] +image::apm/images/apm-transaction-duration-dist.png[Example view of latency distribution graph] -Select a transaction duration _bucket_ to display up to ten trace samples. +Select a latency duration _bucket_ to display up to ten trace samples. [[transaction-trace-sample]] ==== Trace sample -Trace samples are based on the _bucket_ selection in the *Transactions duration distribution* chart; +Trace samples are based on the _bucket_ selection in the *Latency distribution* chart; update the samples by selecting a new _bucket_. The number of requests per bucket is displayed when hovering over the graph, and the selected bucket is highlighted to stand out. diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index 8cab7bb03da75c..4a62f71528676c 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -15,6 +15,7 @@ don't forget to check our other troubleshooting guides or discussion forum: * {apm-server-ref}/troubleshooting.html[APM Server troubleshooting] * {apm-dotnet-ref}/troubleshooting.html[.NET agent troubleshooting] * {apm-go-ref}/troubleshooting.html[Go agent troubleshooting] +* {apm-ios-ref}/troubleshooting.html[iOS agent troubleshooting] * {apm-java-ref}/trouble-shooting.html[Java agent troubleshooting] * {apm-node-ref}/troubleshooting.html[Node.js agent troubleshooting] * {apm-php-ref}/troubleshooting.html[PHP agent troubleshooting] diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index eee92ba4337213..2144fd171ff7a1 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -393,7 +393,7 @@ security and spaces filtering as well as performing audit logging. |{kib-repo}blob/{branch}/x-pack/plugins/enterprise_search/README.md[enterpriseSearch] -|This plugin's goal is to provide a Kibana user interface to the Enterprise Search solution's products (App Search and Workplace Search). In it's current MVP state, the plugin provides the following with the goal of gathering user feedback and raising product awareness: +|This plugin provides beta Kibana user interfaces for managing the Enterprise Search solution and its products, App Search and Workplace Search. |{kib-repo}blob/{branch}/x-pack/plugins/event_log/README.md[eventLog] diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md index a854e5ddad19a6..208e0e0175d715 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md @@ -9,7 +9,7 @@ Configuration options to be used to create a [cluster client](./kibana-plugin-co Signature: ```typescript -export declare type ElasticsearchClientConfig = Pick & { +export declare type ElasticsearchClientConfig = Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; ssl?: Partial; diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md index d87ea63d59b8dd..a9ed614ba7552b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md @@ -31,10 +31,11 @@ export declare class ElasticsearchConfig | [pingTimeout](./kibana-plugin-core-server.elasticsearchconfig.pingtimeout.md) | | Duration | Timeout after which PING HTTP request will be aborted and retried. | | [requestHeadersWhitelist](./kibana-plugin-core-server.elasticsearchconfig.requestheaderswhitelist.md) | | string[] | List of Kibana client-side headers to send to Elasticsearch when request scoped cluster client is used. If this is an empty array then \*no\* client-side will be sent. | | [requestTimeout](./kibana-plugin-core-server.elasticsearchconfig.requesttimeout.md) | | Duration | Timeout after which HTTP request will be aborted and retried. | +| [serviceAccountToken](./kibana-plugin-core-server.elasticsearchconfig.serviceaccounttoken.md) | | string | If Elasticsearch security features are enabled, this setting provides the service account token that the Kibana server users to perform its administrative functions.This is an alternative to specifying a username and password. | | [shardTimeout](./kibana-plugin-core-server.elasticsearchconfig.shardtimeout.md) | | Duration | Timeout for Elasticsearch to wait for responses from shards. Set to 0 to disable. | | [sniffInterval](./kibana-plugin-core-server.elasticsearchconfig.sniffinterval.md) | | false | Duration | Interval to perform a sniff operation and make sure the list of nodes is complete. If false then sniffing is disabled. | | [sniffOnConnectionFault](./kibana-plugin-core-server.elasticsearchconfig.sniffonconnectionfault.md) | | boolean | Specifies whether the client should immediately sniff for a more current list of nodes when a connection dies. | | [sniffOnStart](./kibana-plugin-core-server.elasticsearchconfig.sniffonstart.md) | | boolean | Specifies whether the client should attempt to detect the rest of the cluster when it is first instantiated. | | [ssl](./kibana-plugin-core-server.elasticsearchconfig.ssl.md) | | Pick<SslConfigSchema, Exclude<keyof SslConfigSchema, 'certificateAuthorities' | 'keystore' | 'truststore'>> & {
certificateAuthorities?: string[];
} | Set of settings configure SSL connection between Kibana and Elasticsearch that are required when xpack.ssl.verification_mode in Elasticsearch is set to either certificate or full. | -| [username](./kibana-plugin-core-server.elasticsearchconfig.username.md) | | string | If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. | +| [username](./kibana-plugin-core-server.elasticsearchconfig.username.md) | | string | If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. Cannot be used in conjunction with serviceAccountToken. | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.serviceaccounttoken.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.serviceaccounttoken.md new file mode 100644 index 00000000000000..5934e83de17a40 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.serviceaccounttoken.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchConfig](./kibana-plugin-core-server.elasticsearchconfig.md) > [serviceAccountToken](./kibana-plugin-core-server.elasticsearchconfig.serviceaccounttoken.md) + +## ElasticsearchConfig.serviceAccountToken property + +If Elasticsearch security features are enabled, this setting provides the service account token that the Kibana server users to perform its administrative functions. + +This is an alternative to specifying a username and password. + +Signature: + +```typescript +readonly serviceAccountToken?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.username.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.username.md index 14db9f2e36ccf2..959870ff43a0f4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.username.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.username.md @@ -4,7 +4,7 @@ ## ElasticsearchConfig.username property -If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. +If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. Cannot be used in conjunction with serviceAccountToken. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md index b028a09bee4531..a80ebe2fee4937 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md @@ -11,7 +11,7 @@ Signature: ```typescript -export declare type LegacyElasticsearchClientConfig = Pick & Pick & { +export declare type LegacyElasticsearchClientConfig = Pick & Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ConfigOptions['requestTimeout']; sniffInterval?: ElasticsearchConfig['sniffInterval'] | ConfigOptions['sniffInterval']; diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index 79fa9a642428af..dfb239f0e26c06 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -18,7 +18,7 @@ It is enabled by default. // Any changes made in this file will be seen there as well. // tag::apm-indices-settings[] -Index defaults can be changed in Kibana. Open the main menu, then click *APM > Settings > Indices*. +Index defaults can be changed in the APM app. Select **Settings** > **Indices**. Index settings in the APM app take precedence over those set in `kibana.yml`. [role="screenshot"] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index ba333deeb1609c..15abd0fa4ad962 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -284,6 +284,11 @@ the username and password that the {kib} server uses to perform maintenance on the {kib} index at startup. {kib} users still need to authenticate with {es}, which is proxied through the {kib} server. +|[[elasticsearch-service-account-token]] `elasticsearch.serviceAccountToken:` + | beta[]. If your {es} is protected with basic authentication, this token provides the credentials +that the {kib} server uses to perform maintenance on the {kib} index at startup. This setting +is an alternative to `elasticsearch.username` and `elasticsearch.password`. + | `enterpriseSearch.host` | The URL of your Enterprise Search instance diff --git a/docs/user/dashboard/tsvb.asciidoc b/docs/user/dashboard/tsvb.asciidoc index 89da3f7285924e..11fe71b7639bb5 100644 --- a/docs/user/dashboard/tsvb.asciidoc +++ b/docs/user/dashboard/tsvb.asciidoc @@ -148,6 +148,27 @@ The *Markdown* visualization supports Markdown with Handlebar (mustache) syntax For answers to frequently asked *TSVB* question, review the following. +[float] +===== How do I create dashboard drilldowns for Top N and Table visualizations? + +You can create dashboard drilldowns that include the specified time range for *Top N* and *Table* visualizations. + +. Open the dashboard that you want to link to, then copy the URL. + +. Open the dashboard with the *Top N* and *Table* visualization panel, then click *Edit* in the toolbar. + +. Open the *Top N* or *Table* panel menu, then select *Edit visualization*. + +. Click *Panel options*. + +. In the *Item URL* field, enter the URL. ++ +For example `dashboards#/view/f193ca90-c9f4-11eb-b038-dd3270053a27`. + +. Click *Save and return*. + +. In the toolbar, cick *Save as*, then make sure *Store time with dashboard* is deselected. + [float] ===== Why is my TSVB visualization missing data? diff --git a/package.json b/package.json index 22eedde59c5e7d..5cf72e2110982f 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "dependencies": { "@elastic/apm-rum": "^5.8.0", "@elastic/apm-rum-react": "^1.2.11", - "@elastic/charts": "31.1.0", + "@elastic/charts": "32.0.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13", "@elastic/ems-client": "7.14.0", diff --git a/src/plugins/kibana_legacy/public/notify/toasts/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts similarity index 85% rename from src/plugins/kibana_legacy/public/notify/toasts/index.ts rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts index cdd7df04548fbb..b03ee16d2f7463 100644 --- a/src/plugins/kibana_legacy/public/notify/toasts/index.ts +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { ToastNotifications } from './toast_notifications'; +// stub diff --git a/packages/kbn-optimizer/src/common/__snapshots__/parse_path.test.ts.snap b/packages/kbn-optimizer/src/common/__snapshots__/parse_path.test.ts.snap index f537674c3fff7e..2a30694afb8263 100644 --- a/packages/kbn-optimizer/src/common/__snapshots__/parse_path.test.ts.snap +++ b/packages/kbn-optimizer/src/common/__snapshots__/parse_path.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`parseDirPath() parses / 1`] = ` -Object { +ParsedPath { "dirs": Array [], "filename": undefined, "query": undefined, @@ -10,7 +10,7 @@ Object { `; exports[`parseDirPath() parses /foo 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", ], @@ -21,7 +21,7 @@ Object { `; exports[`parseDirPath() parses /foo/bar/baz 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -34,7 +34,7 @@ Object { `; exports[`parseDirPath() parses /foo/bar/baz/ 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -47,7 +47,7 @@ Object { `; exports[`parseDirPath() parses c:\\ 1`] = ` -Object { +ParsedPath { "dirs": Array [], "filename": undefined, "query": undefined, @@ -56,7 +56,7 @@ Object { `; exports[`parseDirPath() parses c:\\foo 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", ], @@ -67,7 +67,7 @@ Object { `; exports[`parseDirPath() parses c:\\foo\\bar\\baz 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -80,7 +80,7 @@ Object { `; exports[`parseDirPath() parses c:\\foo\\bar\\baz\\ 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -93,7 +93,7 @@ Object { `; exports[`parseFilePath() parses /foo 1`] = ` -Object { +ParsedPath { "dirs": Array [], "filename": "foo", "query": undefined, @@ -102,7 +102,7 @@ Object { `; exports[`parseFilePath() parses /foo/bar/baz 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -114,7 +114,7 @@ Object { `; exports[`parseFilePath() parses /foo/bar/baz.json 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -126,7 +126,7 @@ Object { `; exports[`parseFilePath() parses /foo/bar/baz.json?light 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -140,7 +140,7 @@ Object { `; exports[`parseFilePath() parses /foo/bar/baz.json?light=true&dark=false 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -155,7 +155,7 @@ Object { `; exports[`parseFilePath() parses c:/foo/bar/baz.json 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -167,7 +167,7 @@ Object { `; exports[`parseFilePath() parses c:\\foo 1`] = ` -Object { +ParsedPath { "dirs": Array [], "filename": "foo", "query": undefined, @@ -176,7 +176,7 @@ Object { `; exports[`parseFilePath() parses c:\\foo\\bar\\baz 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -188,7 +188,7 @@ Object { `; exports[`parseFilePath() parses c:\\foo\\bar\\baz.json 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -200,7 +200,7 @@ Object { `; exports[`parseFilePath() parses c:\\foo\\bar\\baz.json?dark 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -214,7 +214,7 @@ Object { `; exports[`parseFilePath() parses c:\\foo\\bar\\baz.json?dark=true&light=false 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", diff --git a/packages/kbn-optimizer/src/common/parse_path.ts b/packages/kbn-optimizer/src/common/parse_path.ts index 7ea0042db25c97..da3744ba477bdd 100644 --- a/packages/kbn-optimizer/src/common/parse_path.ts +++ b/packages/kbn-optimizer/src/common/parse_path.ts @@ -9,17 +9,61 @@ import normalizePath from 'normalize-path'; import Qs from 'querystring'; +class ParsedPath { + constructor( + public readonly root: string, + public readonly dirs: string[], + public readonly query?: Record, + public readonly filename?: string + ) {} + + private indexOfDir(match: string | RegExp, fromIndex: number = 0) { + for (let i = fromIndex; i < this.dirs.length; i++) { + if (this.matchDir(i, match)) { + return i; + } + } + + return -1; + } + + private matchDir(i: number, match: string | RegExp) { + return typeof match === 'string' ? this.dirs[i] === match : match.test(this.dirs[i]); + } + + matchDirs(...segments: Array) { + const [first, ...rest] = segments; + let fromIndex = 0; + while (true) { + // do the dirs include the first segment to match? + const startIndex = this.indexOfDir(first, fromIndex); + if (startIndex === -1) { + return; + } + + // are all of the ...rest segments also matched at this point? + if (!rest.length || rest.every((seg, i) => this.matchDir(startIndex + 1 + i, seg))) { + return { startIndex, endIndex: startIndex + rest.length }; + } + + // no match, search again, this time looking at instances after the matched instance + fromIndex = startIndex + 1; + } + } +} + /** * Parse an absolute path, supporting normalized paths from webpack, * into a list of directories and root */ export function parseDirPath(path: string) { const filePath = parseFilePath(path); - return { - ...filePath, - dirs: [...filePath.dirs, ...(filePath.filename ? [filePath.filename] : [])], - filename: undefined, - }; + return new ParsedPath( + filePath.root, + [...filePath.dirs, ...(filePath.filename ? [filePath.filename] : [])], + filePath.query, + undefined + ); } export function parseFilePath(path: string) { @@ -32,10 +76,10 @@ export function parseFilePath(path: string) { } const [root, ...others] = normalized.split('/'); - return { - root: root === '' ? '/' : root, - dirs: others.slice(0, -1), + return new ParsedPath( + root === '' ? '/' : root, + others.slice(0, -1), query, - filename: others[others.length - 1] || undefined, - }; + others[others.length - 1] || undefined + ); } diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts index 97a7f33be673d0..48d36b706b8312 100644 --- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -15,7 +15,7 @@ import cpy from 'cpy'; import del from 'del'; import { tap, filter } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/utils'; -import { ToolingLog, createReplaceSerializer } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; import { runOptimizer, OptimizerConfig, OptimizerUpdate, logOptimizerState } from '../index'; import { allValuesFrom } from '../common'; @@ -29,8 +29,6 @@ expect.addSnapshotSerializer({ test: (value: any) => typeof value === 'string' && value.includes(REPO_ROOT), }); -expect.addSnapshotSerializer(createReplaceSerializer(/\w+-fastbuild/, '-fastbuild')); - const log = new ToolingLog({ level: 'error', writeTo: { @@ -132,7 +130,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { expect(foo.cache.getModuleCount()).toBe(6); expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/bazel-out/-fastbuild/bin/packages/kbn-ui-shared-deps/target/public_path_module_creator.js, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/kibana.json, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/async_import.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, @@ -155,7 +153,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /node_modules/@kbn/optimizer/postcss.config.js, /node_modules/css-loader/package.json, /node_modules/style-loader/package.json, - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/bazel-out/-fastbuild/bin/packages/kbn-ui-shared-deps/target/public_path_module_creator.js, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, @@ -175,7 +173,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { expect(baz.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/bazel-out/-fastbuild/bin/packages/kbn-ui-shared-deps/target/public_path_module_creator.js, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/public/index.ts, /packages/kbn-optimizer/src/worker/entry_point_creator.ts, diff --git a/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts b/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts index 8d890b31b639da..a3455d7ddf2b96 100644 --- a/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts +++ b/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import webpack from 'webpack'; - import Path from 'path'; import { inspect } from 'util'; +import webpack from 'webpack'; + import { Bundle, WorkerConfig, ascending, parseFilePath } from '../common'; import { BundleRefModule } from './bundle_ref_module'; import { @@ -21,6 +21,20 @@ import { getModulePath, } from './webpack_helpers'; +function tryToResolveRewrittenPath(from: string, toResolve: string) { + try { + return require.resolve(toResolve); + } catch (error) { + if (error.code === 'MODULE_NOT_FOUND') { + throw new Error( + `attempted to rewrite bazel-out path [${from}] to [${toResolve}] but couldn't find the rewrite target` + ); + } + + throw error; + } +} + /** * sass-loader creates about a 40% overhead on the overall optimizer runtime, and * so this constant is used to indicate to assignBundlesToWorkers() that there is @@ -57,17 +71,44 @@ export class PopulateBundleCachePlugin { let path = getModulePath(module); let parsedPath = parseFilePath(path); - if (parsedPath.dirs.includes('bazel-out')) { - const index = parsedPath.dirs.indexOf('bazel-out'); - path = Path.join( - workerConfig.repoRoot, - 'bazel-out', - ...parsedPath.dirs.slice(index + 1), - parsedPath.filename ?? '' + const bazelOut = parsedPath.matchDirs( + 'bazel-out', + /-fastbuild$/, + 'bin', + 'packages', + /.*/, + 'target' + ); + + // if the module is referenced from one of our packages and resolved to the `bazel-out` dir + // we should rewrite our reference to point to the source file so that we can track the + // modified time of that file rather than the built output which is rebuilt all the time + // without actually changing + if (bazelOut) { + const packageDir = parsedPath.dirs[bazelOut.endIndex - 1]; + const subDirs = parsedPath.dirs.slice(bazelOut.endIndex + 1); + path = tryToResolveRewrittenPath( + path, + Path.join( + workerConfig.repoRoot, + 'packages', + packageDir, + 'src', + ...subDirs, + parsedPath.filename + ? Path.basename(parsedPath.filename, Path.extname(parsedPath.filename)) + : '' + ) ); parsedPath = parseFilePath(path); } + if (parsedPath.matchDirs('bazel-out')) { + throw new Error( + `a bazel-out dir is being referenced by module [${path}] and not getting rewritten to its source location` + ); + } + if (!parsedPath.dirs.includes('node_modules')) { referencedFiles.add(path); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index ad83965efde338..be949350f72297 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -68,12 +68,14 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { delete extraCliOptions.env; if (opts.dev) { - if (!has('elasticsearch.username')) { - set('elasticsearch.username', 'kibana_system'); - } + if (!has('elasticsearch.serviceAccountToken')) { + if (!has('elasticsearch.username')) { + set('elasticsearch.username', 'kibana_system'); + } - if (!has('elasticsearch.password')) { - set('elasticsearch.password', 'changeme'); + if (!has('elasticsearch.password')) { + set('elasticsearch.password', 'changeme'); + } } if (opts.ssl) { diff --git a/src/core/public/chrome/ui/header/header_help_menu.tsx b/src/core/public/chrome/ui/header/header_help_menu.tsx index c6a09c1177a5e8..cbf89bba2ca443 100644 --- a/src/core/public/chrome/ui/header/header_help_menu.tsx +++ b/src/core/public/chrome/ui/header/header_help_menu.tsx @@ -211,7 +211,7 @@ export class HeaderHelpMenu extends Component { return ( - + { - + { - + { @@ -330,7 +330,7 @@ export class HeaderHelpMenu extends Component { {customLinks} {content && ( <> - {customLinks && } + {customLinks && } )} @@ -383,7 +383,7 @@ const createCustomLink = ( ) => { return ( - + {text} {addSpacer && } diff --git a/src/core/server/elasticsearch/client/client_config.test.ts b/src/core/server/elasticsearch/client/client_config.test.ts index faca79b3aa6fa3..7e16339b402356 100644 --- a/src/core/server/elasticsearch/client/client_config.test.ts +++ b/src/core/server/elasticsearch/client/client_config.test.ts @@ -204,11 +204,27 @@ describe('parseClientOptions', () => { ); }); + it('adds an authorization header if `serviceAccountToken` is set', () => { + expect( + parseClientOptions( + createConfig({ + serviceAccountToken: 'ABC123', + }), + false + ) + ).toEqual( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: `Bearer ABC123`, + }), + }) + ); + }); + it('does not add auth to the nodes', () => { const options = parseClientOptions( createConfig({ - username: 'user', - password: 'pass', + serviceAccountToken: 'ABC123', hosts: ['http://node-A:9200'], }), true @@ -252,6 +268,34 @@ describe('parseClientOptions', () => { ] `); }); + + it('does not add the authorization header even if `serviceAccountToken` is set', () => { + expect( + parseClientOptions( + createConfig({ + serviceAccountToken: 'ABC123', + }), + true + ).headers + ).not.toHaveProperty('authorization'); + }); + + it('does not add auth to the nodes even if `serviceAccountToken` is set', () => { + const options = parseClientOptions( + createConfig({ + serviceAccountToken: 'ABC123', + hosts: ['http://node-A:9200'], + }), + true + ); + expect(options.nodes).toMatchInlineSnapshot(` + Array [ + Object { + "url": "http://node-a:9200/", + }, + ] + `); + }); }); }); diff --git a/src/core/server/elasticsearch/client/client_config.ts b/src/core/server/elasticsearch/client/client_config.ts index 3044b277db9026..bbbb1ac247b3be 100644 --- a/src/core/server/elasticsearch/client/client_config.ts +++ b/src/core/server/elasticsearch/client/client_config.ts @@ -29,6 +29,7 @@ export type ElasticsearchClientConfig = Pick< | 'hosts' | 'username' | 'password' + | 'serviceAccountToken' > & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; @@ -74,11 +75,16 @@ export function parseClientOptions( }; } - if (config.username && config.password && !scoped) { - clientOptions.auth = { - username: config.username, - password: config.password, - }; + if (!scoped) { + if (config.username && config.password) { + clientOptions.auth = { + username: config.username, + password: config.password, + }; + } else if (config.serviceAccountToken) { + // TODO: change once ES client has native support for service account tokens: https://github.com/elastic/elasticsearch-js/issues/1477 + clientOptions.headers!.authorization = `Bearer ${config.serviceAccountToken}`; + } } clientOptions.nodes = config.hosts.map((host) => convertHost(host)); diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index f8ef1a7a20a837..6e05baac88e34f 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -41,6 +41,7 @@ test('set correct defaults', () => { "authorization", ], "requestTimeout": "PT30S", + "serviceAccountToken": undefined, "shardTimeout": "PT30S", "sniffInterval": false, "sniffOnConnectionFault": false, @@ -377,3 +378,22 @@ test('#username throws if equal to "elastic", only while running from source', ( ); expect(() => config.schema.validate(obj, { dist: true })).not.toThrow(); }); + +test('serviceAccountToken throws if username is also set', () => { + const obj = { + username: 'elastic', + serviceAccountToken: 'abc123', + }; + + expect(() => config.schema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"[serviceAccountToken]: serviceAccountToken cannot be specified when \\"username\\" is also set."` + ); +}); + +test('serviceAccountToken does not throw if username is not set', () => { + const obj = { + serviceAccountToken: 'abc123', + }; + + expect(() => config.schema.validate(obj)).not.toThrow(); +}); diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index b2b25cda3ac2a5..e756d9da867b3d 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -53,6 +53,18 @@ export const configSchema = schema.object({ ) ), password: schema.maybe(schema.string()), + serviceAccountToken: schema.maybe( + schema.conditional( + schema.siblingRef('username'), + schema.never(), + schema.string(), + schema.string({ + validate: () => { + return `serviceAccountToken cannot be specified when "username" is also set.`; + }, + }) + ) + ), requestHeadersWhitelist: schema.oneOf( [ schema.string({ @@ -272,6 +284,7 @@ export class ElasticsearchConfig { /** * If Elasticsearch is protected with basic authentication, this setting provides * the username that the Kibana server uses to perform its administrative functions. + * Cannot be used in conjunction with serviceAccountToken. */ public readonly username?: string; @@ -281,6 +294,14 @@ export class ElasticsearchConfig { */ public readonly password?: string; + /** + * If Elasticsearch security features are enabled, this setting provides the service account + * token that the Kibana server users to perform its administrative functions. + * + * This is an alternative to specifying a username and password. + */ + public readonly serviceAccountToken?: string; + /** * Set of settings configure SSL connection between Kibana and Elasticsearch that * are required when `xpack.ssl.verification_mode` in Elasticsearch is set to @@ -314,6 +335,7 @@ export class ElasticsearchConfig { this.healthCheckDelay = rawConfig.healthCheck.delay; this.username = rawConfig.username; this.password = rawConfig.password; + this.serviceAccountToken = rawConfig.serviceAccountToken; this.customHeaders = rawConfig.customHeaders; const { alwaysPresentCertificate, verificationMode } = rawConfig.ssl; diff --git a/src/core/server/elasticsearch/legacy/cluster_client.test.ts b/src/core/server/elasticsearch/legacy/cluster_client.test.ts index 2ce19570677c5b..52bc4bd45660e2 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.test.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.test.ts @@ -101,6 +101,30 @@ describe('#callAsInternalUser', () => { expect(mockEsClientInstance.ping).toHaveBeenLastCalledWith(mockParams); }); + test('sets the authorization header when a service account token is configured', async () => { + clusterClient = new LegacyClusterClient( + { apiVersion: 'es-version', serviceAccountToken: 'ABC123' } as any, + logger.get(), + 'custom-type' + ); + + const mockResponse = { data: 'ping' }; + const mockParams = { param: 'ping' }; + mockEsClientInstance.ping.mockImplementation(function mockCall(this: any) { + return Promise.resolve({ + context: this, + response: mockResponse, + }); + }); + + await clusterClient.callAsInternalUser('ping', mockParams); + + expect(mockEsClientInstance.ping).toHaveBeenCalledWith({ + headers: { authorization: 'Bearer ABC123' }, + param: 'ping', + }); + }); + test('correctly deals with nested endpoint', async () => { const mockResponse = { data: 'authenticate' }; const mockParams = { param: 'authenticate' }; @@ -355,6 +379,31 @@ describe('#asScoped', () => { ); }); + test('does not set the authorization header when a service account token is configured', async () => { + clusterClient = new LegacyClusterClient( + { + apiVersion: 'es-version', + requestHeadersWhitelist: ['zero'], + serviceAccountToken: 'ABC123', + } as any, + logger.get(), + 'custom-type' + ); + + clusterClient.asScoped( + httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } }) + ); + + const expectedHeaders = { zero: '0' }; + + expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); + expect(MockScopedClusterClient).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function), + expectedHeaders + ); + }); + test('both scoped and internal API caller fail if cluster client is closed', async () => { clusterClient.asScoped( httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } }) diff --git a/src/core/server/elasticsearch/legacy/cluster_client.ts b/src/core/server/elasticsearch/legacy/cluster_client.ts index bdb2ca4d01b3c9..6a6765b67da9f2 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.ts @@ -147,6 +147,13 @@ export class LegacyClusterClient implements ILegacyClusterClient { ) => { this.assertIsNotClosed(); + if (this.config.serviceAccountToken) { + clientParams.headers = { + ...clientParams.headers, + authorization: `Bearer ${this.config.serviceAccountToken}`, + }; + } + return await (callAPI.bind(null, this.client) as LegacyAPICaller)( endpoint, clientParams, diff --git a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts index 6239ad270d5b57..a343c0d5d2ad15 100644 --- a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts +++ b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts @@ -333,6 +333,128 @@ describe('#auth', () => { }); }); +describe('#serviceAccountToken', () => { + it('is set when #auth is true, and a token is provided', () => { + expect( + parseElasticsearchClientConfig( + { + apiVersion: 'v7.0.0', + customHeaders: { xsrf: 'something' }, + sniffOnStart: true, + sniffOnConnectionFault: true, + hosts: ['https://es.local'], + requestHeadersWhitelist: [], + serviceAccountToken: 'ABC123', + }, + logger.get(), + 'custom-type', + { auth: true } + ) + ).toMatchInlineSnapshot(` + Object { + "apiVersion": "v7.0.0", + "hosts": Array [ + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + "xsrf": "something", + }, + "host": "es.local", + "path": "/", + "port": "443", + "protocol": "https:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "serviceAccountToken": "ABC123", + "sniffOnConnectionFault": true, + "sniffOnStart": true, + } + `); + }); + + it('is not set when #auth is true, and a token is not provided', () => { + expect( + parseElasticsearchClientConfig( + { + apiVersion: 'v7.0.0', + customHeaders: { xsrf: 'something' }, + sniffOnStart: true, + sniffOnConnectionFault: true, + hosts: ['https://es.local'], + requestHeadersWhitelist: [], + }, + logger.get(), + 'custom-type', + { auth: true } + ) + ).toMatchInlineSnapshot(` + Object { + "apiVersion": "v7.0.0", + "hosts": Array [ + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + "xsrf": "something", + }, + "host": "es.local", + "path": "/", + "port": "443", + "protocol": "https:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "sniffOnConnectionFault": true, + "sniffOnStart": true, + } + `); + }); + + it('is not set when #auth is false, and a token is provided', () => { + expect( + parseElasticsearchClientConfig( + { + apiVersion: 'v7.0.0', + customHeaders: { xsrf: 'something' }, + sniffOnStart: true, + sniffOnConnectionFault: true, + hosts: ['https://es.local'], + requestHeadersWhitelist: [], + serviceAccountToken: 'ABC123', + }, + logger.get(), + 'custom-type', + { auth: false } + ) + ).toMatchInlineSnapshot(` + Object { + "apiVersion": "v7.0.0", + "hosts": Array [ + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + "xsrf": "something", + }, + "host": "es.local", + "path": "/", + "port": "443", + "protocol": "https:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "sniffOnConnectionFault": true, + "sniffOnStart": true, + } + `); + }); +}); + describe('#customHeaders', () => { test('override the default headers', () => { const headerKey = Object.keys(DEFAULT_HEADERS)[0]; diff --git a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts index d68e7635c57cb4..3d81caefad4576 100644 --- a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts +++ b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts @@ -35,6 +35,7 @@ export type LegacyElasticsearchClientConfig = Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ConfigOptions['requestTimeout']; @@ -61,6 +62,7 @@ interface LegacyElasticsearchClientConfigOverrides { /** @internal */ type ExtendedConfigOptions = ConfigOptions & Partial<{ + serviceAccountToken?: string; ssl: Partial<{ rejectUnauthorized: boolean; checkServerIdentity: typeof checkServerIdentity; @@ -106,9 +108,14 @@ export function parseElasticsearchClientConfig( esClientConfig.sniffInterval = getDurationAsMs(config.sniffInterval); } - const needsAuth = auth !== false && config.username && config.password; + const needsAuth = + auth !== false && ((config.username && config.password) || config.serviceAccountToken); if (needsAuth) { - esClientConfig.httpAuth = `${config.username}:${config.password}`; + if (config.username) { + esClientConfig.httpAuth = `${config.username}:${config.password}`; + } else if (config.serviceAccountToken) { + esClientConfig.serviceAccountToken = config.serviceAccountToken; + } } if (Array.isArray(config.hosts)) { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ed55c6e3d09cbe..65ea082c9d8a8e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -345,6 +345,7 @@ export const config: { hosts: Type; username: Type; password: Type; + serviceAccountToken: Type; requestHeadersWhitelist: Type; customHeaders: Type>; shardTimeout: Type; @@ -948,7 +949,7 @@ export type ElasticsearchClient = Omit & { +export type ElasticsearchClientConfig = Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; ssl?: Partial; @@ -968,6 +969,7 @@ export class ElasticsearchConfig { readonly pingTimeout: Duration; readonly requestHeadersWhitelist: string[]; readonly requestTimeout: Duration; + readonly serviceAccountToken?: string; readonly shardTimeout: Duration; readonly sniffInterval: false | Duration; readonly sniffOnConnectionFault: boolean; @@ -1675,7 +1677,7 @@ export class LegacyClusterClient implements ILegacyClusterClient { } // @public @deprecated (undocumented) -export type LegacyElasticsearchClientConfig = Pick & Pick & { +export type LegacyElasticsearchClientConfig = Pick & Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ConfigOptions['requestTimeout']; sniffInterval?: ElasticsearchConfig['sniffInterval'] | ConfigOptions['sniffInterval']; 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 39a7665f1ce5e5..c7a129418765b4 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 @@ -51,6 +51,7 @@ kibana_vars=( elasticsearch.pingTimeout elasticsearch.requestHeadersWhitelist elasticsearch.requestTimeout + elasticsearch.serviceAccountToken elasticsearch.shardTimeout elasticsearch.sniffInterval elasticsearch.sniffOnConnectionFault diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index ba18c085b649d0..57ae640da3c845 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -77,7 +77,11 @@ export const IGNORE_FILE_GLOBS = [ * * @type {Array} */ -export const KEBAB_CASE_DIRECTORY_GLOBS = ['packages/*', 'x-pack']; +export const KEBAB_CASE_DIRECTORY_GLOBS = [ + 'packages/*', + 'x-pack', + 'packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps', +]; /** * These patterns are matched against directories and indicate diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index 4655bbf8e91a55..cc796ad749f0bc 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -77,7 +77,7 @@ function FilterBarUI(props: Props) { const button = ( setIsAddFilterPopoverOpen(true)} data-test-subj="addFilter" className="globalFilterBar__addButton" diff --git a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js b/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js index a087ac86971838..1a3b34c45d05e2 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js +++ b/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js @@ -19,6 +19,7 @@ import { setScopedHistory, setServices, setDocViewsRegistry } from '../../../../ import { coreMock } from '../../../../../../../core/public/mocks'; import { dataPluginMock } from '../../../../../../data/public/mocks'; import { navigationPluginMock } from '../../../../../../navigation/public/mocks'; +import { initAngularBootstrap } from '../../../../../../kibana_legacy/public/angular_bootstrap'; import { getInnerAngularModule } from '../../get_inner_angular'; import { createBrowserHistory } from 'history'; @@ -41,6 +42,9 @@ describe('Doc Table', () => { // Stub out a minimal mapping of 4 fields let mapping; + beforeAll(async () => { + await initAngularBootstrap(); + }); beforeAll(() => setScopedHistory(createBrowserHistory())); beforeEach(() => { angular.element.prototype.slice = jest.fn(function (index) { diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js b/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js index 1db35ddf18089e..097f32965b1413 100644 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js +++ b/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js @@ -17,6 +17,7 @@ import hits from '../../../__fixtures__/real_hits'; import { coreMock } from '../../../../../../core/public/mocks'; import { dataPluginMock } from '../../../../../data/public/mocks'; import { navigationPluginMock } from '../../../../../navigation/public/mocks'; +import { initAngularBootstrap } from '../../../../../kibana_legacy/public/angular_bootstrap'; import { setScopedHistory, setServices } from '../../../kibana_services'; import { getInnerAngularModule } from '../get_inner_angular'; @@ -54,6 +55,9 @@ describe('docTable', () => { const core = coreMock.createStart(); let $elem; + beforeAll(async () => { + await initAngularBootstrap(); + }); beforeAll(() => setScopedHistory(createBrowserHistory())); beforeEach(() => { angular.element.prototype.slice = jest.fn(() => { diff --git a/src/plugins/discover/public/application/angular/get_inner_angular.ts b/src/plugins/discover/public/application/angular/get_inner_angular.ts index 26d64d5adc8a33..992d82795302b9 100644 --- a/src/plugins/discover/public/application/angular/get_inner_angular.ts +++ b/src/plugins/discover/public/application/angular/get_inner_angular.ts @@ -33,13 +33,12 @@ import { createDocViewerDirective } from './doc_viewer'; import { createDiscoverGridDirective } from './create_discover_grid_directive'; import { createRenderCompleteDirective } from './directives/render_complete'; import { - initAngularBootstrap, configureAppAngularModule, PrivateProvider, - PromiseServiceCreator, registerListenEventListener, watchMultiDecorator, } from '../../../../kibana_legacy/public'; +import { PromiseServiceCreator } from './helpers'; import { DiscoverStartPlugins } from '../../plugin'; import { getScopedHistory } from '../../kibana_services'; import { createDiscoverDirective } from './create_discover_directive'; @@ -54,7 +53,6 @@ export function getInnerAngularModule( deps: DiscoverStartPlugins, context: PluginInitializerContext ) { - initAngularBootstrap(); const module = initializeInnerAngularModule(name, core, deps.navigation, deps.data); configureAppAngularModule(module, { core, env: context.env }, true, getScopedHistory); return module; diff --git a/src/plugins/discover/public/application/angular/helpers/index.ts b/src/plugins/discover/public/application/angular/helpers/index.ts index 3d2c0b1c63b332..6a7f75b7e81a20 100644 --- a/src/plugins/discover/public/application/angular/helpers/index.ts +++ b/src/plugins/discover/public/application/angular/helpers/index.ts @@ -8,3 +8,4 @@ export { formatRow, formatTopLevelObject } from './row_formatter'; export { handleSourceColumnState } from './state_helpers'; +export { PromiseServiceCreator } from './promises'; diff --git a/src/plugins/kibana_legacy/public/angular/promises.d.ts b/src/plugins/discover/public/application/angular/helpers/promises.d.ts similarity index 100% rename from src/plugins/kibana_legacy/public/angular/promises.d.ts rename to src/plugins/discover/public/application/angular/helpers/promises.d.ts diff --git a/src/plugins/kibana_legacy/public/angular/promises.js b/src/plugins/discover/public/application/angular/helpers/promises.js similarity index 100% rename from src/plugins/kibana_legacy/public/angular/promises.js rename to src/plugins/discover/public/application/angular/helpers/promises.js diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 3e31fe1d46d459..1e8a5cdac95efe 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -403,6 +403,7 @@ export class DiscoverPlugin } // this is used by application mount and tests const { getInnerAngularModule } = await import('./application/angular/get_inner_angular'); + await plugins.kibanaLegacy.loadAngularBootstrap(); const module = getInnerAngularModule( innerAngularName, core, @@ -473,6 +474,7 @@ export class DiscoverPlugin throw Error('Discover plugin getEmbeddableInjector: initializeServices is undefined'); } const { core, plugins } = await this.initializeServices(); + await getServices().kibanaLegacy.loadAngularBootstrap(); getServices().kibanaLegacy.loadFontAwesome(); const { getInnerAngularModuleEmbeddable } = await import( './application/angular/get_inner_angular' diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.test.tsx new file mode 100644 index 00000000000000..0e5dd7eb82e720 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.test.tsx @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { act } from 'react-dom/test-utils'; +import { registerTestBed } from '../shared_imports'; + +import { Form, UseField } from '../components'; +import React from 'react'; +import { useForm } from '.'; +import { emptyField } from '../../helpers/field_validators'; +import { FieldHook, FieldValidateResponse, VALIDATION_TYPES } from '..'; + +describe('useField() hook', () => { + describe('field.validate()', () => { + const EMPTY_VALUE = ' '; + + test('It should not invalidate a field with arrayItem validation when isBlocking is false', async () => { + let fieldHook: FieldHook; + + const TestField = ({ field }: { field: FieldHook }) => { + fieldHook = field; + return null; + }; + + const TestForm = () => { + const { form } = useForm(); + + return ( +
+ + + ); + }; + + registerTestBed(TestForm)(); + + let validateResponse: FieldValidateResponse; + + await act(async () => { + validateResponse = await fieldHook!.validate({ + value: EMPTY_VALUE, + validationType: VALIDATION_TYPES.ARRAY_ITEM, + }); + }); + + // validation fails for ARRAY_ITEM with a non-blocking validation error + expect(validateResponse!).toEqual({ + isValid: false, + errors: [ + { + code: 'ERR_FIELD_MISSING', + path: 'test-path', + message: 'error-message', + __isBlocking__: false, + validationType: 'arrayItem', + }, + ], + }); + + // expect the field to be valid because the validation error is non-blocking + expect(fieldHook!.isValid).toBe(true); + }); + + test('It should invalidate an arrayItem field when isBlocking is true', async () => { + let fieldHook: FieldHook; + + const TestField = ({ field }: { field: FieldHook }) => { + fieldHook = field; + return null; + }; + + const TestForm = () => { + const { form } = useForm(); + + return ( +
+ + + ); + }; + + registerTestBed(TestForm)(); + + let validateResponse: FieldValidateResponse; + + await act(async () => { + validateResponse = await fieldHook!.validate({ + value: EMPTY_VALUE, + validationType: VALIDATION_TYPES.ARRAY_ITEM, + }); + }); + + // validation fails for ARRAY_ITEM with a blocking validation error + expect(validateResponse!).toEqual({ + isValid: false, + errors: [ + { + code: 'ERR_FIELD_MISSING', + path: 'test-path', + message: 'error-message', + __isBlocking__: true, + validationType: 'arrayItem', + }, + ], + }); + + // expect the field to be invalid because the validation error is blocking + expect(fieldHook!.isValid).toBe(false); + }); + }); +}); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index 0cf1bb36016671..77bb17d7b9e601 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -15,6 +15,7 @@ import { FieldValidateResponse, ValidationError, FormData, + ValidationConfig, } from '../types'; import { FIELD_TYPES, VALIDATION_TYPES } from '../constants'; @@ -189,10 +190,12 @@ export const useField = ( { formData, value: valueToValidate, + onlyBlocking: runOnlyBlockingValidations, validationTypeToValidate, }: { formData: any; value: I; + onlyBlocking: boolean; validationTypeToValidate?: string; }, clearFieldErrors: FieldHook['clearErrors'] @@ -203,10 +206,31 @@ export const useField = ( // By default, for fields that have an asynchronous validation // we will clear the errors as soon as the field value changes. - clearFieldErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]); + clearFieldErrors([ + validationTypeToValidate ?? VALIDATION_TYPES.FIELD, + VALIDATION_TYPES.ASYNC, + ]); cancelInflightValidation(); + const doByPassValidation = ({ + type: validationType, + isBlocking, + }: ValidationConfig) => { + if ( + typeof validationTypeToValidate !== 'undefined' && + validationType !== validationTypeToValidate + ) { + return true; + } + + if (runOnlyBlockingValidations && isBlocking === false) { + return true; + } + + return false; + }; + const runAsync = async () => { const validationErrors: ValidationError[] = []; @@ -219,10 +243,7 @@ export const useField = ( type: validationType = VALIDATION_TYPES.FIELD, } = validation; - if ( - typeof validationTypeToValidate !== 'undefined' && - validationType !== validationTypeToValidate - ) { + if (doByPassValidation(validation)) { continue; } @@ -265,10 +286,7 @@ export const useField = ( type: validationType = VALIDATION_TYPES.FIELD, } = validation; - if ( - typeof validationTypeToValidate !== 'undefined' && - validationType !== validationTypeToValidate - ) { + if (doByPassValidation(validation)) { continue; } @@ -344,6 +362,7 @@ export const useField = ( formData = __getFormData$().value, value: valueToValidate = value, validationType, + onlyBlocking = false, } = validationData; setIsValidated(true); @@ -377,6 +396,7 @@ export const useField = ( formData, value: valueToValidate, validationTypeToValidate: validationType, + onlyBlocking, }, clearErrors ); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx index 40fc179c73c3bb..92a9876f1cd301 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx @@ -10,7 +10,7 @@ import React, { useEffect } from 'react'; import { act } from 'react-dom/test-utils'; import { registerTestBed, getRandomString, TestBed } from '../shared_imports'; - +import { emptyField } from '../../helpers/field_validators'; import { Form, UseField } from '../components'; import { FormSubmitHandler, @@ -18,7 +18,8 @@ import { FormHook, ValidationFunc, FieldConfig, -} from '../types'; + VALIDATION_TYPES, +} from '..'; import { useForm } from './use_form'; interface MyForm { @@ -501,4 +502,74 @@ describe('useForm() hook', () => { expect(isValid).toBeUndefined(); // Make sure it is "undefined" and not "false". }); }); + + describe('form.validate()', () => { + test('should not invalidate a field with arrayItem validation when validating a form', async () => { + const TestComp = () => { + const { form } = useForm(); + formHook = form; + + return ( +
+ + + ); + }; + + registerTestBed(TestComp)(); + + let isValid: boolean = false; + + await act(async () => { + isValid = await formHook!.validate(); + }); + + expect(isValid).toBe(true); + }); + + test('should invalidate a field with a blocking arrayItem validation when validating a form', async () => { + const TestComp = () => { + const { form } = useForm(); + formHook = form; + + return ( +
+ + + ); + }; + + registerTestBed(TestComp)(); + + let isValid: boolean = false; + + await act(async () => { + isValid = await formHook!.validate(); + }); + + expect(isValid).toBe(false); + }); + }); }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index fb334afb22b137..557da1dc767e49 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -151,14 +151,14 @@ export function useForm( }, [fieldsToArray]); const validateFields: FormHook['__validateFields'] = useCallback( - async (fieldNames) => { + async (fieldNames, onlyBlocking = false) => { const fieldsToValidate = fieldNames .map((name) => fieldsRefs.current[name]) .filter((field) => field !== undefined); const formData = getFormData$().value; const validationResult = await Promise.all( - fieldsToValidate.map((field) => field.validate({ formData })) + fieldsToValidate.map((field) => field.validate({ formData, onlyBlocking })) ); if (isMounted.current === false) { @@ -315,7 +315,8 @@ export function useForm( if (fieldsToValidate.length === 0) { isFormValid = fieldsArray.every(isFieldValid); } else { - ({ isFormValid } = await validateFields(fieldsToValidate.map((field) => field.path))); + const fieldPathsToValidate = fieldsToValidate.map((field) => field.path); + ({ isFormValid } = await validateFields(fieldPathsToValidate, true)); } setIsValid(isFormValid); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index 75c918d4340f2f..61b3bd63fb2233 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -53,7 +53,9 @@ export interface FormHook __addField: (field: FieldHook) => void; __removeField: (fieldNames: string | string[]) => void; __validateFields: ( - fieldNames: string[] + fieldNames: string[], + /** Run only blocking validations */ + onlyBlocking?: boolean ) => Promise<{ areFieldsValid: boolean; isFormValid: boolean | undefined }>; __updateFormDataAt: (field: string, value: unknown) => void; __updateDefaultValueAt: (field: string, value: unknown) => void; @@ -137,6 +139,7 @@ export interface FieldHook { formData?: any; value?: I; validationType?: string; + onlyBlocking?: boolean; }) => FieldValidateResponse | Promise; reset: (options?: { resetValue?: boolean; defaultValue?: T }) => unknown | undefined; // Flag to indicate if the field value will be included in the form data outputted 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 6bd06528084ce9..6405a81282471e 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 @@ -132,7 +132,7 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { } ) => ( <> - + {name}   diff --git a/src/plugins/kibana_legacy/public/angular/angular_config.tsx b/src/plugins/kibana_legacy/public/angular/angular_config.tsx index daecfbc57ea991..48ee6d2db269e6 100644 --- a/src/plugins/kibana_legacy/public/angular/angular_config.tsx +++ b/src/plugins/kibana_legacy/public/angular/angular_config.tsx @@ -13,6 +13,7 @@ import { ILocationProvider, IModule, IRootScopeService, + IRequestConfig, } from 'angular'; import $ from 'jquery'; import { set } from '@elastic/safer-lodash-set'; @@ -22,7 +23,6 @@ import { ChromeBreadcrumb, EnvironmentMode, PackageInfo } from 'kibana/public'; import { History } from 'history'; import { CoreStart } from 'kibana/public'; -import { isSystemApiRequest } from '../utils'; import { formatAngularHttpError, isAngularHttpError } from '../notify/lib'; export interface RouteConfiguration { @@ -38,6 +38,11 @@ export interface RouteConfiguration { requireUICapability?: string; } +function isSystemApiRequest(request: IRequestConfig) { + const { headers } = request; + return headers && !!headers['kbn-system-request']; +} + /** * Detects whether a given angular route is a dummy route that doesn't * require any action. There are two ways this can happen: diff --git a/src/plugins/kibana_legacy/public/angular/index.ts b/src/plugins/kibana_legacy/public/angular/index.ts index d9d8c0c19eb7b1..369495698591d0 100644 --- a/src/plugins/kibana_legacy/public/angular/index.ts +++ b/src/plugins/kibana_legacy/public/angular/index.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -// @ts-ignore -export { PromiseServiceCreator } from './promises'; // @ts-ignore export { watchMultiDecorator } from './watch_multi'; export * from './angular_config'; diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts index 03adb768cde208..ea5172f78a68f3 100644 --- a/src/plugins/kibana_legacy/public/index.ts +++ b/src/plugins/kibana_legacy/public/index.ts @@ -14,7 +14,6 @@ export const plugin = (initializerContext: PluginInitializerContext) => export * from './plugin'; -export { initAngularBootstrap } from './angular_bootstrap'; export { PaginateDirectiveProvider, PaginateControlsDirectiveProvider } from './paginate/paginate'; export * from './angular'; export * from './notify'; diff --git a/src/plugins/kibana_legacy/public/mocks.ts b/src/plugins/kibana_legacy/public/mocks.ts index 40834635cc5704..6116c0682cb3bb 100644 --- a/src/plugins/kibana_legacy/public/mocks.ts +++ b/src/plugins/kibana_legacy/public/mocks.ts @@ -22,6 +22,7 @@ const createStartContract = (): Start => ({ getHideWriteControls: jest.fn(), }, loadFontAwesome: jest.fn(), + loadAngularBootstrap: jest.fn(), }); export const kibanaLegacyPluginMock = { diff --git a/src/plugins/kibana_legacy/public/notify/index.ts b/src/plugins/kibana_legacy/public/notify/index.ts index a243059cb19183..d4dcaa77cc47ad 100644 --- a/src/plugins/kibana_legacy/public/notify/index.ts +++ b/src/plugins/kibana_legacy/public/notify/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -export * from './toasts'; export * from './lib'; diff --git a/src/plugins/kibana_legacy/public/notify/toasts/TOAST_NOTIFICATIONS.md b/src/plugins/kibana_legacy/public/notify/toasts/TOAST_NOTIFICATIONS.md deleted file mode 100644 index de6a51f3927d17..00000000000000 --- a/src/plugins/kibana_legacy/public/notify/toasts/TOAST_NOTIFICATIONS.md +++ /dev/null @@ -1,100 +0,0 @@ -# Toast notifications - -Use this service to surface toasts in the bottom-right corner of the screen. After a brief delay, they'll disappear. They're useful for notifying the user of state changes. See [the EUI docs](https://elastic.github.io/eui/) for more information on toasts and their role within the UI. - -## Importing the module - -```js -import { toastNotifications } from 'ui/notify'; -``` - -## Interface - -### Adding toasts - -For convenience, there are several methods which predefine the appearance of different types of toasts. Use these methods so that the same types of toasts look similar to the user. - -#### Default - -Neutral toast. Tell the user a change in state has occurred, which is not necessarily good or bad. - -```js -toastNotifications.add('Copied to clipboard'); -``` - -#### Success - -Let the user know that an action was successful, such as saving or deleting an object. - -```js -toastNotifications.addSuccess('Your document was saved'); -``` - -#### Warning - -If something OK or good happened, but perhaps wasn't perfect, show a warning toast. - -```js -toastNotifications.addWarning('Your document was saved, but not its edit history'); -``` - -#### Danger - -When the user initiated an action but the action failed, show them a danger toast. - -```js -toastNotifications.addDanger('An error caused your document to be lost'); -``` - -### Removing a toast - -Toasts will automatically be dismissed after a brief delay, but if for some reason you want to dismiss a toast, you can use the returned toast from one of the `add` methods and then pass it to `remove`. - -```js -const toast = toastNotifications.add('Your document was saved'); -toastNotifications.remove(toast); -``` - -### Configuration options - -If you want to configure the toast further you can provide an object instead of a string. The properties of this object correspond to the `propTypes` accepted by the `EuiToast` component. Refer to [the EUI docs](https://elastic.github.io/eui/) for info on these `propTypes`. - -```js -toastNotifications.add({ - title: 'Your document was saved', - text: 'Only you have access to this document', - color: 'success', - iconType: 'check', - 'data-test-subj': 'saveDocumentSuccess', -}); -``` - -Because the underlying components are React, you can use JSX to pass in React elements to the `text` prop. This gives you total flexibility over the content displayed within the toast. - -```js -toastNotifications.add({ - title: 'Your document was saved', - text: ( -
-

- Only you have access to this document. Edit permissions. -

- - -
- ), -}); -``` - -## Use in functional tests - -Functional tests are commonly used to verify that a user action yielded a successful outcome. If you surface a toast to notify the user of this successful outcome, you can place a `data-test-subj` attribute on the toast and use it to check if the toast exists inside of your functional test. This acts as a proxy for verifying the successful outcome. - -```js -toastNotifications.addSuccess({ - title: 'Your document was saved', - 'data-test-subj': 'saveDocumentSuccess', -}); -``` diff --git a/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.test.ts b/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.test.ts deleted file mode 100644 index c2c5d9a4fc014a..00000000000000 --- a/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.test.ts +++ /dev/null @@ -1,76 +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 { notificationServiceMock } from '../../../../../core/public/mocks'; -import { ToastNotifications } from './toast_notifications'; -import { Toast } from 'kibana/public'; -import { BehaviorSubject } from 'rxjs'; - -describe('ToastNotifications', () => { - describe('interface', () => { - function setup() { - const toastsMock = notificationServiceMock.createStartContract().toasts; - return { toastNotifications: new ToastNotifications(toastsMock), toastsMock }; - } - - describe('add method', () => { - test('adds a toast', () => { - const { toastNotifications, toastsMock } = setup(); - toastNotifications.add({}); - expect(toastsMock.add).toHaveBeenCalled(); - }); - }); - - describe('remove method', () => { - test('removes a toast', () => { - const { toastNotifications, toastsMock } = setup(); - const fakeToast = {} as Toast; - toastNotifications.remove(fakeToast); - expect(toastsMock.remove).toHaveBeenCalledWith(fakeToast); - }); - }); - - describe('onChange method', () => { - test('callback is called when observable changes', () => { - const toastsMock = notificationServiceMock.createStartContract().toasts; - const toasts$ = new BehaviorSubject([]); - toastsMock.get$.mockReturnValue(toasts$); - const toastNotifications = new ToastNotifications(toastsMock); - const onChangeSpy = jest.fn(); - toastNotifications.onChange(onChangeSpy); - toasts$.next([{ id: 'toast1' }]); - toasts$.next([]); - expect(onChangeSpy).toHaveBeenCalledTimes(2); - }); - }); - - describe('addSuccess method', () => { - test('adds a success toast', () => { - const { toastNotifications, toastsMock } = setup(); - toastNotifications.addSuccess({}); - expect(toastsMock.addSuccess).toHaveBeenCalled(); - }); - }); - - describe('addWarning method', () => { - test('adds a warning toast', () => { - const { toastNotifications, toastsMock } = setup(); - toastNotifications.addWarning({}); - expect(toastsMock.addWarning).toHaveBeenCalled(); - }); - }); - - describe('addDanger method', () => { - test('adds a danger toast', () => { - const { toastNotifications, toastsMock } = setup(); - toastNotifications.addWarning({}); - expect(toastsMock.addWarning).toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.ts b/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.ts deleted file mode 100644 index e7ccbbca07b734..00000000000000 --- a/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { NotificationsSetup, Toast, ToastInput, ErrorToastOptions } from 'kibana/public'; - -export class ToastNotifications { - public list: Toast[] = []; - - private onChangeCallback?: () => void; - - constructor(private readonly toasts: NotificationsSetup['toasts']) { - toasts.get$().subscribe((list) => { - this.list = list; - - if (this.onChangeCallback) { - this.onChangeCallback(); - } - }); - } - - public onChange = (callback: () => void) => { - this.onChangeCallback = callback; - }; - - public add = (toastOrTitle: ToastInput) => this.toasts.add(toastOrTitle); - public remove = (toast: Toast) => this.toasts.remove(toast); - public addSuccess = (toastOrTitle: ToastInput) => this.toasts.addSuccess(toastOrTitle); - public addWarning = (toastOrTitle: ToastInput) => this.toasts.addWarning(toastOrTitle); - public addDanger = (toastOrTitle: ToastInput) => this.toasts.addDanger(toastOrTitle); - public addError = (error: Error, options: ErrorToastOptions) => - this.toasts.addError(error, options); -} diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index 337fdb80da7e45..f60130d367b584 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -33,6 +33,14 @@ export class KibanaLegacyPlugin { loadFontAwesome: async () => { await import('./font_awesome'); }, + /** + * Loads angular bootstrap modules. Should be removed once the last consumer has migrated to EUI + * @deprecated + */ + loadAngularBootstrap: async () => { + const { initAngularBootstrap } = await import('./angular_bootstrap'); + initAngularBootstrap(); + }, /** * @deprecated * Just exported for wiring up with dashboard mode, should not be used. diff --git a/src/plugins/kibana_legacy/public/utils/index.ts b/src/plugins/kibana_legacy/public/utils/index.ts index db3c0af6c8cb94..94233558b4627c 100644 --- a/src/plugins/kibana_legacy/public/utils/index.ts +++ b/src/plugins/kibana_legacy/public/utils/index.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -export * from './system_api'; // @ts-ignore export { KbnAccessibleClickProvider } from './kbn_accessible_click'; // @ts-ignore diff --git a/src/plugins/kibana_legacy/public/utils/system_api.ts b/src/plugins/kibana_legacy/public/utils/system_api.ts deleted file mode 100644 index d0fe221935ba51..00000000000000 --- a/src/plugins/kibana_legacy/public/utils/system_api.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { IRequestConfig } from 'angular'; - -const SYSTEM_REQUEST_HEADER_NAME = 'kbn-system-request'; -const LEGACY_SYSTEM_API_HEADER_NAME = 'kbn-system-api'; - -/** - * Adds a custom header designating request as system API - * @param originalHeaders Object representing set of headers - * @return Object representing set of headers, with system API header added in - */ -export function addSystemApiHeader(originalHeaders: Record) { - const systemApiHeaders = { - [SYSTEM_REQUEST_HEADER_NAME]: true, - }; - return { - ...originalHeaders, - ...systemApiHeaders, - }; -} - -/** - * Returns true if request is a system API request; false otherwise - * - * @param request Object Request object created by $http service - * @return true if request is a system API request; false otherwise - */ -export function isSystemApiRequest(request: IRequestConfig) { - const { headers } = request; - return ( - headers && (!!headers[SYSTEM_REQUEST_HEADER_NAME] || !!headers[LEGACY_SYSTEM_API_HEADER_NAME]) - ); -} diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index 523bf07f828c95..f4ca53a9a974e4 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -42,7 +42,7 @@ export function TopNavMenuItem(props: TopNavMenuData) { {upperFirst(props.label || props.id!)} ) : ( - + {upperFirst(props.label || props.id!)} ); diff --git a/src/plugins/timelion/public/plugin.ts b/src/plugins/timelion/public/plugin.ts index 6f8dbfdcc67041..63ea9a38e2795d 100644 --- a/src/plugins/timelion/public/plugin.ts +++ b/src/plugins/timelion/public/plugin.ts @@ -19,7 +19,7 @@ import { AppNavLinkStatus, } from '../../../core/public'; import { Panel } from './panels/panel'; -import { initAngularBootstrap } from '../../kibana_legacy/public'; +import { KibanaLegacyStart } from '../../kibana_legacy/public'; import { createKbnUrlTracker } from '../../kibana_utils/public'; import { DataPublicPluginStart, esFilters, DataPublicPluginSetup } from '../../data/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; @@ -41,6 +41,7 @@ export interface TimelionPluginStartDependencies { visualizations: VisualizationsStart; visTypeTimelion: VisTypeTimelionPluginStart; savedObjects: SavedObjectsStart; + kibanaLegacy: KibanaLegacyStart; } /** @internal */ @@ -91,7 +92,6 @@ export class TimelionPlugin stopUrlTracker(); }; - initAngularBootstrap(); core.application.register({ id: 'timelion', title: 'Timelion', @@ -103,6 +103,7 @@ export class TimelionPlugin visTypeTimelion.isUiEnabled === false ? AppNavLinkStatus.hidden : AppNavLinkStatus.default, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); + await pluginsStart.kibanaLegacy.loadAngularBootstrap(); this.currentHistory = params.history; appMounted(); diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js index 65e26ddf6e03fd..cbc3db6585a7da 100644 --- a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js +++ b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js @@ -15,7 +15,7 @@ import { round } from 'lodash'; import { getFieldFormatsRegistry } from '../../../../data/public/test_utils'; import { coreMock } from '../../../../../core/public/mocks'; -import { initAngularBootstrap } from '../../../../kibana_legacy/public'; +import { initAngularBootstrap } from '../../../../kibana_legacy/public/angular_bootstrap'; import { setUiSettings } from '../../../../data/public/services'; import { UI_SETTINGS } from '../../../../data/public/'; import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../../share/public'; @@ -60,10 +60,12 @@ describe('Table Vis - AggTable Directive', function () { initTableVisLegacyModule(tableVisModule); }; + beforeAll(async () => { + await initAngularBootstrap(); + }); beforeEach(() => { setUiSettings(core.uiSettings); setFormatService(getFieldFormatsRegistry(core)); - initAngularBootstrap(); initLocalAngular(); angular.mock.module('kibana/table_vis'); angular.mock.inject(($injector, config) => { diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js index 1c6630e30e5f73..ba04b2f449f6dd 100644 --- a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js +++ b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js @@ -13,11 +13,11 @@ import expect from '@kbn/expect'; import { getFieldFormatsRegistry } from '../../../../data/public/test_utils'; import { coreMock } from '../../../../../core/public/mocks'; -import { initAngularBootstrap } from '../../../../kibana_legacy/public'; import { setUiSettings } from '../../../../data/public/services'; import { setFormatService } from '../../services'; import { getInnerAngular } from '../get_inner_angular'; import { initTableVisLegacyModule } from '../table_vis_legacy_module'; +import { initAngularBootstrap } from '../../../../kibana_legacy/public/angular_bootstrap'; import { tabifiedData } from './tabified_data'; const uiSettings = new Map(); @@ -40,10 +40,12 @@ describe('Table Vis - AggTableGroup Directive', function () { initTableVisLegacyModule(tableVisModule); }; + beforeAll(async () => { + await initAngularBootstrap(); + }); beforeEach(() => { setUiSettings(core.uiSettings); setFormatService(getFieldFormatsRegistry(core)); - initAngularBootstrap(); initLocalAngular(); angular.mock.module('kibana/table_vis'); angular.mock.inject(($injector) => { diff --git a/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts b/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts index 09fde318ee4dff..412dd904a5e872 100644 --- a/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts +++ b/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts @@ -16,7 +16,6 @@ import 'angular-recursion'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { CoreStart, IUiSettingsClient, PluginInitializerContext } from 'kibana/public'; import { - initAngularBootstrap, PaginateDirectiveProvider, PaginateControlsDirectiveProvider, PrivateProvider, @@ -24,8 +23,6 @@ import { KbnAccessibleClickProvider, } from '../../../kibana_legacy/public'; -initAngularBootstrap(); - const thirdPartyAngularDependencies = ['ngSanitize', 'ui.bootstrap', 'RecursionHelper']; export function getAngularModule(name: string, core: CoreStart, context: PluginInitializerContext) { diff --git a/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts index 77148803e7978b..3feff52f86792b 100644 --- a/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts +++ b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts @@ -12,6 +12,7 @@ import $ from 'jquery'; import 'angular-sanitize'; import 'angular-mocks'; +import { initAngularBootstrap } from '../../../../kibana_legacy/public/angular_bootstrap'; import { getAngularModule } from '../get_inner_angular'; import { initTableVisLegacyModule } from '../table_vis_legacy_module'; import { coreMock } from '../../../../../core/public/mocks'; @@ -56,6 +57,10 @@ describe('Table Vis - Paginated table', () => { const defaultPerPage = 10; let paginatedTable: any; + beforeAll(async () => { + await initAngularBootstrap(); + }); + const initLocalAngular = () => { const tableVisModule = getAngularModule( 'kibana/table_vis', diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts b/src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts index 36a9cc9cce77fe..f4a742ea16cb4f 100644 --- a/src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts +++ b/src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts @@ -13,6 +13,7 @@ import $ from 'jquery'; import { getAngularModule } from './get_inner_angular'; import { initTableVisLegacyModule } from './table_vis_legacy_module'; +import { initAngularBootstrap } from '../../../kibana_legacy/public/angular_bootstrap'; import { tableVisLegacyTypeDefinition } from './table_vis_legacy_type'; import { Vis } from '../../../visualizations/public'; import { stubFields } from '../../../data/public/stubs'; @@ -76,6 +77,9 @@ describe('Table Vis - Controller', () => { initTableVisLegacyModule(tableVisModule); }; + beforeAll(async () => { + await initAngularBootstrap(); + }); beforeEach(initLocalAngular); beforeEach(angular.mock.module('kibana/table_vis')); diff --git a/src/plugins/vis_type_table/public/legacy/vis_controller.ts b/src/plugins/vis_type_table/public/legacy/vis_controller.ts index ee446c58c00139..ec198aa96f1f96 100644 --- a/src/plugins/vis_type_table/public/legacy/vis_controller.ts +++ b/src/plugins/vis_type_table/public/legacy/vis_controller.ts @@ -56,6 +56,7 @@ export function getTableVisualizationControllerClass( async initLocalAngular() { if (!this.tableVisModule) { const [coreStart, { kibanaLegacy }] = await core.getStartServices(); + await kibanaLegacy.loadAngularBootstrap(); this.tableVisModule = getAngularModule(innerAngularName, coreStart, context); initTableVisLegacyModule(this.tableVisModule); kibanaLegacy.loadFontAwesome(); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js index 546c09cdf34fd0..b9ef2d89135741 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useMemo, useEffect } from 'react'; import { AggRow } from './agg_row'; import { AggSelect } from './agg_select'; import { FieldSelect } from './field_select'; @@ -62,6 +62,7 @@ const getAggWithOptions = (field = {}, fieldTypesRestriction) => { }, ]; case KBN_FIELD_TYPES.STRING: + case KBN_FIELD_TYPES.DATE: return [ { label: i18n.translate('visTypeTimeseries.topHit.aggWithOptions.concatenate', { @@ -91,16 +92,18 @@ const getOrderOptions = () => [ }, ]; +const AGG_WITH_KEY = 'agg_with'; const ORDER_DATE_RESTRICT_FIELDS = [KBN_FIELD_TYPES.DATE]; +const getModelDefaults = () => ({ + size: 1, + order: 'desc', + [AGG_WITH_KEY]: 'noop', +}); + const TopHitAggUi = (props) => { const { fields, series, panel } = props; - const defaults = { - size: 1, - agg_with: 'noop', - order: 'desc', - }; - const model = { ...defaults, ...props.model }; + const model = useMemo(() => ({ ...getModelDefaults(), ...props.model }), [props.model]); const indexPattern = series.override_index_pattern ? series.series_index_pattern : panel.index_pattern; @@ -110,7 +113,7 @@ const TopHitAggUi = (props) => { PANEL_TYPES.METRIC, PANEL_TYPES.MARKDOWN, ].includes(panel.type) - ? [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.STRING] + ? [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.STRING, KBN_FIELD_TYPES.DATE] : [KBN_FIELD_TYPES.NUMBER]; const handleChange = createChangeHandler(props.onChange, model); @@ -124,13 +127,23 @@ const TopHitAggUi = (props) => { const htmlId = htmlIdGenerator(); const selectedAggWithOption = aggWithOptions.find((option) => { - return model.agg_with === option.value; + return model[AGG_WITH_KEY] === option.value; }); const selectedOrderOption = orderOptions.find((option) => { return model.order === option.value; }); + useEffect(() => { + const defaultFn = aggWithOptions?.[0]?.value; + const aggWith = model[AGG_WITH_KEY]; + if (aggWith && defaultFn && aggWith !== defaultFn && !selectedAggWithOption) { + handleChange({ + [AGG_WITH_KEY]: defaultFn, + }); + } + }, [model, selectedAggWithOption, aggWithOptions, handleChange]); + return ( { { )} options={aggWithOptions} selectedOptions={selectedAggWithOption ? [selectedAggWithOption] : []} - onChange={handleSelectChange('agg_with')} + onChange={handleSelectChange(AGG_WITH_KEY)} singleSelection={{ asPlainText: true }} + data-test-subj="topHitAggregateWithComboBox" /> @@ -231,6 +245,7 @@ const TopHitAggUi = (props) => { onChange={handleSelectChange('order_by')} indexPattern={indexPattern} fields={fields} + data-test-subj="topHitOrderByFieldSelect" /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js index c1d82a182e509b..9bccc13d192692 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js @@ -16,7 +16,7 @@ export const createTickFormatter = (format = '0,0.[00]', template, getConfig = n const fieldFormats = getFieldFormats(); if (!template) template = '{{value}}'; - const render = handlebars.compile(template, { knownHelpersOnly: true }); + const render = handlebars.compile(template, { noEscape: true, knownHelpersOnly: true }); let formatter; if (isDuration(format)) { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js index 16e7b9d6072cba..13b890189325ce 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js @@ -111,7 +111,7 @@ export const bucketTransform = { docs: { top_hits: { size: bucket.size, - _source: { includes: [bucket.field] }, + fields: [bucket.field], }, }, }, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.js index 32d17ef6d6cb73..90df3f26759599 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.js @@ -45,10 +45,10 @@ export const getAggValue = (row, metric) => { } const hits = get(row, [metric.id, 'docs', 'hits', 'hits'], []); - const values = hits.map((doc) => get(doc, `_source.${metric.field}`)); + const values = hits.map((doc) => doc.fields[metric.field]); const aggWith = (metric.agg_with && aggFns[metric.agg_with]) || aggFns.noop; - return aggWith(values); + return aggWith(values.flat()); case METRIC_TYPES.COUNT: return get(row, 'doc_count', null); default: diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.test.js index a23c57f5675633..ecbdd1563c3043 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.test.js @@ -67,11 +67,7 @@ describe('getAggValue', () => { doc_count: 1, docs: { hits: { - hits: [ - { _source: { example: { value: 25 } } }, - { _source: { example: { value: 25 } } }, - { _source: { example: { value: 25 } } }, - ], + hits: [{ fields: { 'example.value': [25, 25, 25] } }], }, }, }, diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index ca310493960f53..49b2ad8f9646a3 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'timePicker', 'visChart', 'common', + 'settings', ]); describe('visual builder', function describeIndexTests() { @@ -44,14 +45,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); describe('metric', () => { + const { visualBuilder } = PageObjects; + beforeEach(async () => { - await PageObjects.visualBuilder.resetPage(); - await PageObjects.visualBuilder.clickMetric(); - await PageObjects.visualBuilder.checkMetricTabIsPresent(); - await PageObjects.visualBuilder.clickPanelOptions('metric'); - await PageObjects.visualBuilder.setMetricsDataTimerangeMode('Last value'); - await PageObjects.visualBuilder.setDropLastBucket(true); - await PageObjects.visualBuilder.clickDataTab('metric'); + await visualBuilder.resetPage(); + await visualBuilder.clickMetric(); + await visualBuilder.checkMetricTabIsPresent(); + await visualBuilder.clickPanelOptions('metric'); + await visualBuilder.setMetricsDataTimerangeMode('Last value'); + await visualBuilder.setDropLastBucket(true); + await visualBuilder.clickDataTab('metric'); }); it('should not have inspector enabled', async () => { @@ -59,28 +62,98 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should show correct data', async () => { - const value = await PageObjects.visualBuilder.getMetricValue(); + const value = await visualBuilder.getMetricValue(); expect(value).to.eql('156'); }); it('should show correct data with Math Aggregation', async () => { - await PageObjects.visualBuilder.createNewAgg(); - await PageObjects.visualBuilder.selectAggType('math', 1); - await PageObjects.visualBuilder.fillInVariable(); - await PageObjects.visualBuilder.fillInExpression('params.test + 1'); - const value = await PageObjects.visualBuilder.getMetricValue(); + await visualBuilder.createNewAgg(); + await visualBuilder.selectAggType('math', 1); + await visualBuilder.fillInVariable(); + await visualBuilder.fillInExpression('params.test + 1'); + const value = await visualBuilder.getMetricValue(); expect(value).to.eql('157'); }); it('should populate fields for basic functions', async () => { - const { visualBuilder } = PageObjects; - await visualBuilder.selectAggType('Average'); await visualBuilder.setFieldForAggregation('machine.ram'); const isFieldForAggregationValid = await visualBuilder.checkFieldForAggregationValidity(); expect(isFieldForAggregationValid).to.be(true); }); + + it('should show correct data for Value Count with Entire time range mode', async () => { + await visualBuilder.selectAggType('Value Count'); + await visualBuilder.setFieldForAggregation('machine.ram'); + + await visualBuilder.clickPanelOptions('metric'); + await visualBuilder.setMetricsDataTimerangeMode('Entire time range'); + + const value = await visualBuilder.getMetricValue(); + expect(value).to.eql('13,492'); + }); + + it('should show same data for kibana and string index pattern modes', async () => { + await visualBuilder.selectAggType('Max'); + await visualBuilder.setFieldForAggregation('machine.ram'); + const kibanaIndexPatternModeValue = await visualBuilder.getMetricValue(); + + await visualBuilder.clickPanelOptions('metric'); + await visualBuilder.switchIndexPatternSelectionMode(false); + const stringIndexPatternModeValue = await visualBuilder.getMetricValue(); + + expect(kibanaIndexPatternModeValue).to.eql(stringIndexPatternModeValue); + expect(kibanaIndexPatternModeValue).to.eql('32,212,254,720'); + }); + + describe('Color rules', () => { + beforeEach(async () => { + await visualBuilder.selectAggType('Min'); + await visualBuilder.setFieldForAggregation('machine.ram'); + + await visualBuilder.clickPanelOptions('metric'); + await visualBuilder.setColorRuleOperator('>= greater than or equal'); + await visualBuilder.setColorRuleValue(0); + }); + + it('should apply color rules to visualization background', async () => { + await visualBuilder.setColorPickerValue('#FFCFDF'); + + const backGroundStyle = await visualBuilder.getBackgroundStyle(); + expect(backGroundStyle).to.eql('background-color: rgb(255, 207, 223);'); + }); + + it('should apply color rules to metric value', async () => { + await visualBuilder.setColorPickerValue('#AD7DE6', 1); + + const backGroundStyle = await visualBuilder.getMetricValueStyle(); + expect(backGroundStyle).to.eql('color: rgb(173, 125, 230);'); + }); + }); + + describe('Top Hit aggregation', () => { + beforeEach(async () => { + await visualBuilder.selectAggType('Top Hit'); + await visualBuilder.setTopHitOrderByField('@timestamp'); + }); + + it('should show correct data for string type field', async () => { + await visualBuilder.setFieldForAggregation('machine.os.raw'); + await visualBuilder.setTopHitAggregateWithOption('Concatenate'); + + const value = await visualBuilder.getMetricValue(); + expect(value).to.eql('win 7'); + }); + + it('should show correct data for runtime field', async () => { + await visualBuilder.setFieldForAggregation('hello_world_runtime_field'); + await visualBuilder.setTopHitAggregateWithOption('Concatenate'); + + const value = await visualBuilder.getMetricValue(); + expect(value).to.eql('hello world'); + }); + }); }); describe('gauge', () => { diff --git a/test/functional/fixtures/kbn_archiver/visualize.json b/test/functional/fixtures/kbn_archiver/visualize.json index 660da856964b44..225dc0592e87d5 100644 --- a/test/functional/fixtures/kbn_archiver/visualize.json +++ b/test/functional/fixtures/kbn_archiver/visualize.json @@ -3,6 +3,7 @@ "fieldAttrs": "{\"utc_time\":{\"customLabel\":\"UTC time\"}}", "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "runtimeFieldMap":"{\"hello_world_runtime_field\":{\"type\":\"keyword\",\"script\":{\"source\":\"emit('hello world')\"}}}", "timeFieldName": "@timestamp", "title": "logstash-*" }, @@ -301,4 +302,4 @@ "references": [], "type": "index-pattern", "version": "WzE1LDFd" -} \ No newline at end of file +} diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 8e28ffab6c9c3d..fd89a88658b3a6 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -575,6 +575,42 @@ export class VisualBuilderPageObject extends FtrService { await this.testSubjects.existOrFail('euiColorPickerPopover', { timeout: 5000 }); } + public async setColorPickerValue(colorHex: string, nth: number = 0): Promise { + const picker = await this.find.allByCssSelector('.tvbColorPicker button'); + await picker[nth].clickMouseButton(); + await this.checkColorPickerPopUpIsPresent(); + await this.find.setValue('.euiColorPicker input', colorHex); + await this.visChart.waitForVisualizationRenderingStabilized(); + } + + public async setColorRuleOperator(condition: string): Promise { + await this.retry.try(async () => { + await this.comboBox.clearInputField('colorRuleOperator'); + await this.comboBox.set('colorRuleOperator', condition); + }); + } + + public async setColorRuleValue(value: number): Promise { + await this.retry.try(async () => { + const colorRuleValueInput = await this.find.byCssSelector( + '[data-test-subj="colorRuleValue"]' + ); + await colorRuleValueInput.type(value.toString()); + }); + } + + public async getBackgroundStyle(): Promise { + await this.visChart.waitForVisualizationRenderingStabilized(); + const visualization = await this.find.byClassName('tvbVis'); + return await visualization.getAttribute('style'); + } + + public async getMetricValueStyle(): Promise { + await this.visChart.waitForVisualizationRenderingStabilized(); + const metricValue = await this.find.byCssSelector('[data-test-subj="tsvbMetricValue"]'); + return await metricValue.getAttribute('style'); + } + public async changePanelPreview(nth: number = 0): Promise { const prevRenderingCount = await this.visChart.getVisualizationRenderingCount(); const changePreviewBtnArray = await this.testSubjects.findAll('AddActivatePanelBtn'); @@ -680,4 +716,15 @@ export class VisualBuilderPageObject extends FtrService { const dataTimeRangeMode = await this.testSubjects.find('dataTimeRangeMode'); return await this.comboBox.isOptionSelected(dataTimeRangeMode, value); } + + public async setTopHitAggregateWithOption(option: string): Promise { + await this.comboBox.set('topHitAggregateWithComboBox', option); + } + + public async setTopHitOrderByField(timeField: string) { + await this.retry.try(async () => { + await this.comboBox.clearInputField('topHitOrderByFieldSelect'); + await this.comboBox.set('topHitOrderByFieldSelect', timeField); + }); + } } diff --git a/test/scripts/jenkins_build_load_testing.sh b/test/scripts/jenkins_build_load_testing.sh index d7c7bda83c9ef2..667540515fc835 100755 --- a/test/scripts/jenkins_build_load_testing.sh +++ b/test/scripts/jenkins_build_load_testing.sh @@ -53,6 +53,9 @@ echo "cloud.auth: ${USER_FROM_VAULT}:${PASS_FROM_VAULT}" >> cfg/metricbeat/metri cp cfg/metricbeat/metricbeat.yml $KIBANA_DIR/metricbeat-install/metricbeat.yml # Disable system monitoring: enabled for now to have more data #mv $KIBANA_DIR/metricbeat-install/modules.d/system.yml $KIBANA_DIR/metricbeat-install/modules.d/system.yml.disabled +echo " -> Building puppeteer project" +cd puppeteer +yarn install && yarn build popd # doesn't persist, also set in kibanaPipeline.groovy diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx index 63ba7047696cac..4e6544a20f3015 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx @@ -6,12 +6,7 @@ */ import React from 'react'; -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiToolTip, -} from '@elastic/eui'; +import { EuiHeaderLinks, EuiHeaderLink, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { createExploratoryViewUrl, @@ -62,38 +57,29 @@ export function UXActionMenu({ - - - {ANALYZE_MESSAGE}

}> - - {ANALYZE_DATA} - -
-
- - + {ANALYZE_MESSAGE}

}> + - {i18n.translate('xpack.apm.addDataButtonLabel', { - defaultMessage: 'Add data', - })} -
-
-
+ {ANALYZE_DATA} +
+ + + {i18n.translate('xpack.apm.addDataButtonLabel', { + defaultMessage: 'Add data', + })} + + ); } diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx index ca73f6ddd05b34..4abd36a2773119 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx @@ -66,7 +66,6 @@ export function AlertingPopoverAndFlyout({ const button = ( - + {i18n.translate('xpack.apm.settingsLinkLabel', { defaultMessage: 'Settings', })} diff --git a/x-pack/plugins/canvas/public/components/function_reference_generator/function_reference_generator.tsx b/x-pack/plugins/canvas/public/components/function_reference_generator/function_reference_generator.tsx index 81532816d9c830..eb394801f549c4 100644 --- a/x-pack/plugins/canvas/public/components/function_reference_generator/function_reference_generator.tsx +++ b/x-pack/plugins/canvas/public/components/function_reference_generator/function_reference_generator.tsx @@ -29,7 +29,7 @@ export const FunctionReferenceGenerator: FC = ({ functionRegistry }) => { }; return ( - + Generate function reference ); diff --git a/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx b/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx index 2877ccf41056df..af1850beb5290f 100644 --- a/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx +++ b/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx @@ -46,13 +46,13 @@ export const HelpMenu: FC = ({ functionRegistry }) => { return ( <> - + {strings.getKeyboardShortcutsLinkLabel()} {FunctionReferenceGenerator ? ( - + ) : null} diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/__snapshots__/edit_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/__snapshots__/edit_menu.stories.storyshot index cc33ae3526c0c3..f2bc9c57cbcc66 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/__snapshots__/edit_menu.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/__snapshots__/edit_menu.stories.storyshot @@ -9,7 +9,7 @@ exports[`Storyshots components/WorkpadHeader/EditMenu 2 elements selected 1`] = >