diff --git a/Jenkinsfile b/Jenkinsfile
index 763ee95ddde99..db9d218db15ba 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -41,7 +41,7 @@ kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true) {
'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9),
'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10),
'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'),
- 'xpack-pageLoadMetrics': kibanaPipeline.functionalTestProcess('xpack-pageLoadMetrics', './test/scripts/jenkins_xpack_page_load_metrics.sh'),
+ // 'xpack-pageLoadMetrics': kibanaPipeline.functionalTestProcess('xpack-pageLoadMetrics', './test/scripts/jenkins_xpack_page_load_metrics.sh'),
'xpack-securitySolutionCypress': { processNumber ->
whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/']) {
kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')(processNumber)
diff --git a/NOTICE.txt b/NOTICE.txt
index 946b328b8766c..94312d46c35ec 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -29,6 +29,68 @@ Author Tobias Koppers @sokra
---
This product has relied on ASTExplorer that is licensed under MIT.
+---
+This product includes code that is based on Ace editor, which was available
+under a "BSD" license.
+
+Distributed under the BSD license:
+
+Copyright (c) 2010, Ajax.org B.V.
+All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of Ajax.org B.V. nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+---
+This product includes code that is based on Ace editor, which was available
+under a "BSD" license.
+
+Distributed under the BSD license:
+
+Copyright (c) 2010, Ajax.org B.V.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of Ajax.org B.V. nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
---
This product includes code that is based on flot-charts, which was available
under a "MIT" license.
diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.http.md b/docs/development/core/server/kibana-plugin-core-server.corestart.http.md
new file mode 100644
index 0000000000000..d81049dfbd340
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.corestart.http.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreStart](./kibana-plugin-core-server.corestart.md) > [http](./kibana-plugin-core-server.corestart.http.md)
+
+## CoreStart.http property
+
+[HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md)
+
+Signature:
+
+```typescript
+http: HttpServiceStart;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.md b/docs/development/core/server/kibana-plugin-core-server.corestart.md
index c50e8924c9dd4..6a6bacf1eef40 100644
--- a/docs/development/core/server/kibana-plugin-core-server.corestart.md
+++ b/docs/development/core/server/kibana-plugin-core-server.corestart.md
@@ -18,6 +18,7 @@ export interface CoreStart
| --- | --- | --- |
| [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | CapabilitiesStart | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) |
| [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | ElasticsearchServiceStart | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) |
+| [http](./kibana-plugin-core-server.corestart.http.md) | HttpServiceStart | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) |
| [savedObjects](./kibana-plugin-core-server.corestart.savedobjects.md) | SavedObjectsServiceStart | [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) |
| [uiSettings](./kibana-plugin-core-server.corestart.uisettings.md) | UiSettingsServiceStart | [UiSettingsServiceStart](./kibana-plugin-core-server.uisettingsservicestart.md) |
diff --git a/docs/development/core/server/kibana-plugin-core-server.httpauth.get.md b/docs/development/core/server/kibana-plugin-core-server.httpauth.get.md
new file mode 100644
index 0000000000000..4ea67cf895a27
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.httpauth.get.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpAuth](./kibana-plugin-core-server.httpauth.md) > [get](./kibana-plugin-core-server.httpauth.get.md)
+
+## HttpAuth.get property
+
+Gets authentication state for a request. Returned by `auth` interceptor. [GetAuthState](./kibana-plugin-core-server.getauthstate.md)
+
+Signature:
+
+```typescript
+get: GetAuthState;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.httpauth.isauthenticated.md b/docs/development/core/server/kibana-plugin-core-server.httpauth.isauthenticated.md
new file mode 100644
index 0000000000000..54db6bce5f161
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.httpauth.isauthenticated.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpAuth](./kibana-plugin-core-server.httpauth.md) > [isAuthenticated](./kibana-plugin-core-server.httpauth.isauthenticated.md)
+
+## HttpAuth.isAuthenticated property
+
+Returns authentication status for a request. [IsAuthenticated](./kibana-plugin-core-server.isauthenticated.md)
+
+Signature:
+
+```typescript
+isAuthenticated: IsAuthenticated;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.httpauth.md b/docs/development/core/server/kibana-plugin-core-server.httpauth.md
new file mode 100644
index 0000000000000..d9d77809570ab
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.httpauth.md
@@ -0,0 +1,20 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpAuth](./kibana-plugin-core-server.httpauth.md)
+
+## HttpAuth interface
+
+
+Signature:
+
+```typescript
+export interface HttpAuth
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [get](./kibana-plugin-core-server.httpauth.get.md) | GetAuthState | Gets authentication state for a request. Returned by auth interceptor. [GetAuthState](./kibana-plugin-core-server.getauthstate.md) |
+| [isAuthenticated](./kibana-plugin-core-server.httpauth.isauthenticated.md) | IsAuthenticated | Returns authentication status for a request. [IsAuthenticated](./kibana-plugin-core-server.isauthenticated.md) |
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.auth.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.auth.md
index 6667779c1c7ae..da348a2282b1a 100644
--- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.auth.md
+++ b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.auth.md
@@ -4,11 +4,15 @@
## HttpServiceSetup.auth property
+> Warning: This API is now obsolete.
+>
+> use [the start contract](./kibana-plugin-core-server.httpservicestart.auth.md) instead.
+>
+
+Auth status. See [HttpAuth](./kibana-plugin-core-server.httpauth.md)
+
Signature:
```typescript
-auth: {
- get: GetAuthState;
- isAuthenticated: IsAuthenticated;
- };
+auth: HttpAuth;
```
diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.istlsenabled.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.istlsenabled.md
deleted file mode 100644
index fa86da18393f5..0000000000000
--- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.istlsenabled.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) > [isTlsEnabled](./kibana-plugin-core-server.httpservicesetup.istlsenabled.md)
-
-## HttpServiceSetup.isTlsEnabled property
-
-Flag showing whether a server was configured to use TLS connection.
-
-Signature:
-
-```typescript
-isTlsEnabled: boolean;
-```
diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md
index 2dd832813afb8..b12983836d9e5 100644
--- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md
+++ b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md
@@ -81,13 +81,12 @@ async (context, request, response) => {
| Property | Type | Description |
| --- | --- | --- |
-| [auth](./kibana-plugin-core-server.httpservicesetup.auth.md) | { get: GetAuthState; isAuthenticated: IsAuthenticated; } | |
+| [auth](./kibana-plugin-core-server.httpservicesetup.auth.md) | HttpAuth | Auth status. See [HttpAuth](./kibana-plugin-core-server.httpauth.md) |
| [basePath](./kibana-plugin-core-server.httpservicesetup.basepath.md) | IBasePath | Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-core-server.ibasepath.md). |
| [createCookieSessionStorageFactory](./kibana-plugin-core-server.httpservicesetup.createcookiesessionstoragefactory.md) | <T>(cookieOptions: SessionStorageCookieOptions<T>) => Promise<SessionStorageFactory<T>> | Creates cookie based session storage factory [SessionStorageFactory](./kibana-plugin-core-server.sessionstoragefactory.md) |
| [createRouter](./kibana-plugin-core-server.httpservicesetup.createrouter.md) | () => IRouter | Provides ability to declare a handler function for a particular path and HTTP request method. |
| [csp](./kibana-plugin-core-server.httpservicesetup.csp.md) | ICspConfig | The CSP config used for Kibana. |
| [getServerInfo](./kibana-plugin-core-server.httpservicesetup.getserverinfo.md) | () => HttpServerInfo | Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running http server. |
-| [isTlsEnabled](./kibana-plugin-core-server.httpservicesetup.istlsenabled.md) | boolean | Flag showing whether a server was configured to use TLS connection. |
| [registerAuth](./kibana-plugin-core-server.httpservicesetup.registerauth.md) | (handler: AuthenticationHandler) => void | To define custom authentication and/or authorization mechanism for incoming requests. |
| [registerOnPostAuth](./kibana-plugin-core-server.httpservicesetup.registeronpostauth.md) | (handler: OnPostAuthHandler) => void | To define custom logic to perform for incoming requests. |
| [registerOnPreAuth](./kibana-plugin-core-server.httpservicesetup.registeronpreauth.md) | (handler: OnPreAuthHandler) => void | To define custom logic to perform for incoming requests. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicestart.islistening.md b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.auth.md
similarity index 50%
rename from docs/development/core/server/kibana-plugin-core-server.httpservicestart.islistening.md
rename to docs/development/core/server/kibana-plugin-core-server.httpservicestart.auth.md
index bf2922c62c15f..f7dffee2e125c 100644
--- a/docs/development/core/server/kibana-plugin-core-server.httpservicestart.islistening.md
+++ b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.auth.md
@@ -1,13 +1,13 @@
-[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) > [isListening](./kibana-plugin-core-server.httpservicestart.islistening.md)
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) > [auth](./kibana-plugin-core-server.httpservicestart.auth.md)
-## HttpServiceStart.isListening property
+## HttpServiceStart.auth property
-Indicates if http server is listening on a given port
+Auth status. See [HttpAuth](./kibana-plugin-core-server.httpauth.md)
Signature:
```typescript
-isListening: (port: number) => boolean;
+auth: HttpAuth;
```
diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicestart.basepath.md b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.basepath.md
new file mode 100644
index 0000000000000..e8b2a0fc2cbaa
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.basepath.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) > [basePath](./kibana-plugin-core-server.httpservicestart.basepath.md)
+
+## HttpServiceStart.basePath property
+
+Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-core-server.ibasepath.md).
+
+Signature:
+
+```typescript
+basePath: IBasePath;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicestart.getserverinfo.md b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.getserverinfo.md
new file mode 100644
index 0000000000000..a95c8da64fdb0
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.getserverinfo.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) > [getServerInfo](./kibana-plugin-core-server.httpservicestart.getserverinfo.md)
+
+## HttpServiceStart.getServerInfo property
+
+Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running http server.
+
+Signature:
+
+```typescript
+getServerInfo: () => HttpServerInfo;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicestart.md b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.md
index 53239da516b25..bc99c1217f72b 100644
--- a/docs/development/core/server/kibana-plugin-core-server.httpservicestart.md
+++ b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.md
@@ -15,5 +15,7 @@ export interface HttpServiceStart
| Property | Type | Description |
| --- | --- | --- |
-| [isListening](./kibana-plugin-core-server.httpservicestart.islistening.md) | (port: number) => boolean | Indicates if http server is listening on a given port |
+| [auth](./kibana-plugin-core-server.httpservicestart.auth.md) | HttpAuth | Auth status. See [HttpAuth](./kibana-plugin-core-server.httpauth.md) |
+| [basePath](./kibana-plugin-core-server.httpservicestart.basepath.md) | IBasePath | Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-core-server.ibasepath.md). |
+| [getServerInfo](./kibana-plugin-core-server.httpservicestart.getserverinfo.md) | () => HttpServerInfo | Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running http server. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md
index a45bd3d44b28a..0f1bbbe7176e5 100644
--- a/docs/development/core/server/kibana-plugin-core-server.md
+++ b/docs/development/core/server/kibana-plugin-core-server.md
@@ -85,6 +85,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [EnvironmentMode](./kibana-plugin-core-server.environmentmode.md) | |
| [ErrorHttpResponseOptions](./kibana-plugin-core-server.errorhttpresponseoptions.md) | HTTP response parameters |
| [FakeRequest](./kibana-plugin-core-server.fakerequest.md) | Fake request object created manually by Kibana plugins. |
+| [HttpAuth](./kibana-plugin-core-server.httpauth.md) | |
| [HttpResources](./kibana-plugin-core-server.httpresources.md) | HttpResources service is responsible for serving static & dynamic assets for Kibana application via HTTP. Provides API allowing plug-ins to respond with: - a pre-configured HTML page bootstrapping Kibana client app - custom HTML page - custom JS script file. |
| [HttpResourcesRenderOptions](./kibana-plugin-core-server.httpresourcesrenderoptions.md) | Allows to configure HTTP response parameters |
| [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) | Extended set of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) helpers used to respond with HTML or JS resource. |
diff --git a/docs/settings/ingest-manager-settings.asciidoc b/docs/settings/ingest-manager-settings.asciidoc
index 0c3427b034ae2..f46c769079040 100644
--- a/docs/settings/ingest-manager-settings.asciidoc
+++ b/docs/settings/ingest-manager-settings.asciidoc
@@ -8,8 +8,10 @@
experimental[]
You can configure `xpack.ingestManager` settings in your `kibana.yml`.
-By default, {ingest-manager} is not enabled. You need to enable it. To use
-{fleet}, you also need to configure {kib} and {es} hosts.
+By default, {ingest-manager} is not enabled. You need to
+enable it. To use {fleet}, you also need to configure {kib} and {es} hosts.
+
+See the {ingest-guide}/index.html[Ingest Management] docs for more information.
[[general-ingest-manager-settings-kb]]
==== General {ingest-manager} settings
diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc
index 058d53118b076..18d851ba78f33 100644
--- a/docs/settings/security-settings.asciidoc
+++ b/docs/settings/security-settings.asciidoc
@@ -30,6 +30,150 @@ You do not need to configure any additional settings to use the
|===
+[float]
+[[authentication-security-settings]]
+==== Authentication security settings
+
+You configure authentication settings in the `xpack.security.authc` namespace in `kibana.yml`.
+
+For example:
+
+[source,yaml]
+----------------------------------------
+xpack.security.authc:
+ providers:
+ basic.basic1: <1>
+ order: 0 <2>
+ ...
+
+ saml.saml1: <3>
+ order: 1
+ ...
+
+ saml.saml2: <4>
+ order: 2
+ ...
+
+ pki.realm3:
+ order: 3
+ ...
+ ...
+----------------------------------------
+<1> Specifies the type of authentication provider (for example, `basic`, `token`, `saml`, `oidc`, `kerberos`, `pki`) and the provider name. This setting is mandatory.
+<2> Specifies the order of the provider in the authentication chain and on the Login Selector UI. This setting is mandatory.
+<3> Specifies the settings for the SAML authentication provider with a `saml1` name.
+<4> Specifies the settings for the SAML authentication provider with a `saml2` name.
+
+The valid settings in the `xpack.security.authc.providers` namespace vary depending on the authentication provider type. For more information, refer to <>.
+
+[float]
+[[authentication-provider-settings]]
+===== Valid settings for all authentication providers
+
+[cols="2*<"]
+|===
+| `xpack.security.authc.providers.`
+`..enabled`
+| Determines if the authentication provider should be enabled. By default, {kib} enables the provider as soon as you configure any of its properties.
+
+| `xpack.security.authc.providers.`
+`..order`
+| Order of the provider in the authentication chain and on the Login Selector UI.
+
+| `xpack.security.authc.providers.`
+`..description`
+| Custom description of the provider entry displayed on the Login Selector UI.
+
+| `xpack.security.authc.providers.`
+`..hint`
+| Custom hint for the provider entry displayed on the Login Selector UI.
+
+| `xpack.security.authc.providers.`
+`..icon`
+| Custom icon for the provider entry displayed on the Login Selector UI.
+
+| `xpack.security.authc.providers.`
+`..showInSelector`
+| Flag that indicates if the provider should have an entry on the Login Selector UI. Setting this to `false` doesn't remove the provider from the authentication chain.
+
+|===
+
+[NOTE]
+============
+You are unable to set this setting to `false` for `basic` and `token` authentication providers.
+============
+
+[cols="2*<"]
+|===
+
+| `xpack.security.authc.providers.`
+`..accessAgreement.message`
+| Access agreement text in Markdown format. For more information, refer to <>.
+
+|===
+
+[float]
+[[saml-authentication-provider-settings]]
+===== SAML authentication provider settings
+
+In addition to <>, you can specify the following settings:
+
+[cols="2*<"]
+|===
+| `xpack.security.authc.providers.`
+`saml..realm`
+| SAML realm in {es} that provider should use.
+
+| `xpack.security.authc.providers.`
+`saml..maxRedirectURLSize`
+| The maximum size of the URL that {kib} is allowed to store during the authentication SAML handshake. For more information, refer to <>.
+
+|===
+
+[float]
+[[oidc-authentication-provider-settings]]
+===== OpenID Connect authentication provider settings
+
+In addition to <>, you can specify the following settings:
+
+[cols="2*<"]
+|===
+| `xpack.security.authc.providers.`
+`oidc..realm`
+| OpenID Connect realm in {es} that the provider should use.
+
+|===
+
+[float]
+[[http-authentication-settings]]
+===== HTTP authentication settings
+
+There is a very limited set of cases when you'd want to change these settings. For more information, refer to <>.
+
+[cols="2*<"]
+|===
+| `xpack.security.authc.http.enabled`
+| Determines if HTTP authentication should be enabled. By default, this setting is set to `true`.
+
+| `xpack.security.authc.http.autoSchemesEnabled`
+| Determines if HTTP authentication schemes used by the enabled authentication providers should be automatically supported during HTTP authentication. By default, this setting is set to `true`.
+
+| `xpack.security.authc.http.schemes`
+| List of HTTP authentication schemes that {kib} HTTP authentication should support. By default, this setting is set to `['apikey']` to support HTTP authentication with <> scheme.
+
+|===
+
+[float]
+[[login-selector-settings]]
+===== Login Selector UI settings
+
+[cols="2*<"]
+|===
+| `xpack.security.authc.selector.enabled`
+| Determines if the Login Selector UI should be enabled. By default, this setting is set to `true` if more than one authentication provider is configured.
+
+|===
+
[float]
[[security-ui-settings]]
==== User interface security settings
@@ -96,4 +240,7 @@ string of `[ms|s|m|h|d|w|M|Y]` (e.g. '70ms', '5s', '3d', '1Y').
| `xpack.security.loginAssistanceMessage`
| Adds a message to the login screen. Useful for displaying information about maintenance windows, links to corporate sign up pages etc.
+| `xpack.security.loginHelp`
+ | Adds a message accessible at the Login Selector UI with additional help information for the login process.
+
|===
diff --git a/docs/user/security/access-agreement.asciidoc b/docs/user/security/access-agreement.asciidoc
new file mode 100644
index 0000000000000..362eb80501210
--- /dev/null
+++ b/docs/user/security/access-agreement.asciidoc
@@ -0,0 +1,27 @@
+[role="xpack"]
+[[xpack-security-access-agreement]]
+=== Access agreement
+
+Some work environments require you to acknowledge and accept an agreement before you can access {kib}, which can contain sensitive information. The agreement text supports Markdown format and can be specified using the `xpack.security.authc.providers...accessAgreement.message` setting.
+
+[NOTE]
+============================================================================
+You need to acknowledge the access agreement only once per session, and {kib} reports the acknowledgement in the audit logs.
+============================================================================
+
+Here is how your `kibana.yml` can look like if you define an access agreement:
+
+[source,yaml]
+--------------------------------------------------------------------------------
+xpack.security.authc.providers:
+ basic.basic1:
+ order: 0
+ accessAgreement:
+ message: "**You are accessing a system with a sensitive information** \n\n
+ By logging in, you acknowledge that (shortened ...)"
+--------------------------------------------------------------------------------
+
+When you authenticate using `basic.basic1`, you'll see the following agreement that you must acknowledge before you can access {kib}:
+
+[role="screenshot"]
+image::user/security/images/access-agreement.png["Access Agreement UI"]
diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc
index f72ae0dcf9c93..a7359af38c1cb 100644
--- a/docs/user/security/audit-logging.asciidoc
+++ b/docs/user/security/audit-logging.asciidoc
@@ -1,6 +1,6 @@
[role="xpack"]
[[xpack-security-audit-logging]]
-=== Audit Logging
+=== Audit logs
You can enable auditing to keep track of security-related events such as
authorization success and failures. Logging these events enables you
diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc
index 4c0e863b05d31..fe93e38151b82 100644
--- a/docs/user/security/authentication/index.asciidoc
+++ b/docs/user/security/authentication/index.asciidoc
@@ -17,22 +17,29 @@
Enable multiple authentication mechanisms at the same time specifying a prioritized list of the authentication _providers_ (typically of various types) in the configuration. Providers are consulted in ascending order. Make sure each configured provider has a unique name (e.g. `basic1` or `saml1` in the configuration example) and `order` setting. In the event that two or more providers have the same name or `order`, {kib} will fail to start.
-When two or more providers are configured, you can choose the provider you want to use on the Login Selector UI. The order the providers appear is determined by the order setting. The appearance of the specific provider entry can be customized with the `description` setting.
+When two or more providers are configured, you can choose the provider you want to use on the Login Selector UI. The order the providers appear is determined by the `order` setting. The appearance of the specific provider entry can be customized with the `description`, `hint`, and `icon` settings.
+
+TIP: To provide login instructions to users, use the `xpack.security.loginHelp` setting, which supports Markdown format. When you specify the `xpack.security.loginHelp` setting, the Login Selector UI displays a `Need help?` link that lets users access login help information.
If you don't want a specific provider to show up at the Login Selector UI (e.g. to only support third-party initiated login) you can hide it with `showInSelector` setting set to `false`. However, in this case, the provider is presented in the provider chain and may be consulted during authentication based on its `order`. To disable the provider, use the `enabled` setting.
TIP: The Login Selector UI can also be disabled or enabled with `xpack.security.authc.selector.enabled` setting.
-Here is how your `kibana.yml` can look like if you deal with multiple authentication providers:
+Here is how your `kibana.yml` and Login Selector UI can look like if you deal with multiple authentication providers:
+[source,yaml]
--------------------------------------------------------------------------------
+xpack.security.loginHelp: "**Help** info with a [link](...)"
xpack.security.authc.providers:
basic.basic1:
order: 0
+ icon: "logoElasticsearch"
+ hint: "Typically for administrators"
saml.saml1:
order: 1
realm: saml1
description: "Log in with SSO"
+ icon: "https://my-company.xyz/saml-logo.svg"
saml.saml2:
order: 2
realm: saml2
@@ -42,6 +49,11 @@ xpack.security.authc.providers:
enabled: false
--------------------------------------------------------------------------------
+[role="screenshot"]
+image::user/security/images/kibana-login.png["Login Selector UI"]
+
+For more information, refer to <>.
+
[[basic-authentication]]
==== Basic authentication
@@ -170,6 +182,7 @@ Basic authentication is supported _only_ if the `basic` authentication provider
To support basic authentication for the applications like `curl` or when the `Authorization: Basic base64(username:password)` HTTP header is included in the request (for example, by reverse proxy), add `Basic` scheme to the list of supported schemes for the <>.
[float]
+[[security-saml-and-long-urls]]
===== SAML and long URLs
At the beginning of the SAML handshake, {kib} stores the initial URL in the session cookie, so it can redirect the user back to that URL after successful SAML authentication.
@@ -325,4 +338,4 @@ NOTE: Don't forget to explicitly specify default `apikey` scheme when you just w
xpack.security.authc.http.schemes: [apikey, basic, something-custom]
--------------------------------------------------------------------------------
-With this configuration, you can send requests to {kib} with the `Authorization` header using `ApiKey`, `Basic` or `Something-Custom` HTTP schemes (case insensitive). Under the hood, {kib} relays this header to {es}, then {es} authenticates the request using the credentials in the header.
\ No newline at end of file
+With this configuration, you can send requests to {kib} with the `Authorization` header using `ApiKey`, `Basic` or `Something-Custom` HTTP schemes (case insensitive). Under the hood, {kib} relays this header to {es}, then {es} authenticates the request using the credentials in the header.
diff --git a/docs/user/security/images/access-agreement.png b/docs/user/security/images/access-agreement.png
new file mode 100644
index 0000000000000..ecb6122875cb8
Binary files /dev/null and b/docs/user/security/images/access-agreement.png differ
diff --git a/docs/user/security/images/kibana-login.jpg b/docs/user/security/images/kibana-login.jpg
deleted file mode 100644
index 9a1916d5217dc..0000000000000
Binary files a/docs/user/security/images/kibana-login.jpg and /dev/null differ
diff --git a/docs/user/security/images/kibana-login.png b/docs/user/security/images/kibana-login.png
new file mode 100644
index 0000000000000..813f2c703908d
Binary files /dev/null and b/docs/user/security/images/kibana-login.png differ
diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc
index 6fc2b0af318a4..b30acd0ed2e53 100644
--- a/docs/user/security/securing-kibana.asciidoc
+++ b/docs/user/security/securing-kibana.asciidoc
@@ -145,3 +145,4 @@ include::authentication/index.asciidoc[]
include::securing-communications/index.asciidoc[]
include::securing-communications/elasticsearch-mutual-tls.asciidoc[]
include::audit-logging.asciidoc[]
+include::access-agreement.asciidoc[]
diff --git a/package.json b/package.json
index cddeb28fcdfad..5d4c165c55899 100644
--- a/package.json
+++ b/package.json
@@ -256,7 +256,7 @@
"reselect": "^4.0.0",
"resize-observer-polyfill": "^1.5.0",
"rison-node": "1.0.2",
- "rxjs": "^6.5.3",
+ "rxjs": "^6.5.5",
"script-loader": "0.7.2",
"seedrandom": "^3.0.5",
"semver": "^5.5.0",
@@ -341,7 +341,7 @@
"@types/history": "^4.7.3",
"@types/hoek": "^4.1.3",
"@types/inert": "^5.1.2",
- "@types/jest": "24.0.19",
+ "@types/jest": "^25.2.3",
"@types/joi": "^13.4.2",
"@types/jquery": "^3.3.31",
"@types/js-yaml": "^3.11.1",
@@ -386,6 +386,7 @@
"@types/supertest-as-promised": "^2.0.38",
"@types/tar": "^4.0.3",
"@types/testing-library__dom": "^6.10.0",
+ "@types/testing-library__jest-dom": "^5.7.0",
"@types/testing-library__react": "^9.1.2",
"@types/testing-library__react-hooks": "^3.1.0",
"@types/type-detect": "^4.0.1",
@@ -398,8 +399,9 @@
"archiver": "^3.1.1",
"axe-core": "^3.4.1",
"babel-eslint": "^10.0.3",
- "babel-jest": "^24.9.0",
- "backport": "4.9.0",
+ "babel-jest": "^25.5.1",
+ "babel-plugin-istanbul": "^6.0.0",
+ "backport": "5.4.1",
"chai": "3.5.0",
"chance": "1.0.18",
"cheerio": "0.22.0",
@@ -417,7 +419,7 @@
"eslint-plugin-ban": "^1.4.0",
"eslint-plugin-cypress": "^2.8.1",
"eslint-plugin-import": "^2.19.1",
- "eslint-plugin-jest": "^23.3.0",
+ "eslint-plugin-jest": "^23.10.0",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-mocha": "^6.2.2",
"eslint-plugin-no-unsanitized": "^3.0.2",
@@ -446,8 +448,10 @@
"intl-messageformat-parser": "^1.4.0",
"is-path-inside": "^2.1.0",
"istanbul-instrumenter-loader": "3.0.1",
- "jest": "^24.9.0",
- "jest-cli": "^24.9.0",
+ "jest": "^25.5.4",
+ "jest-environment-jsdom-thirteen": "^1.0.1",
+ "jest-circus": "^25.5.4",
+ "jest-cli": "^25.5.4",
"jest-raw-loader": "^1.0.1",
"jimp": "^0.9.6",
"json5": "^1.0.1",
diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json
index 9bc8ed3019e54..e14423d681a4e 100644
--- a/packages/eslint-config-kibana/package.json
+++ b/packages/eslint-config-kibana/package.json
@@ -23,7 +23,7 @@
"eslint-plugin-ban": "^1.4.0",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-import": "^2.19.1",
- "eslint-plugin-jest": "^23.3.0",
+ "eslint-plugin-jest": "^23.10.0",
"eslint-plugin-mocha": "^6.2.2",
"eslint-plugin-no-unsanitized": "^3.0.2",
"eslint-plugin-prefer-object-spread": "^1.2.1",
diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json
index dedc2707f5b5b..d95cd1d404a1b 100644
--- a/packages/kbn-dev-utils/package.json
+++ b/packages/kbn-dev-utils/package.json
@@ -18,7 +18,7 @@
"getopts": "^2.2.5",
"load-json-file": "^6.2.0",
"moment": "^2.24.0",
- "rxjs": "^6.5.3",
+ "rxjs": "^6.5.5",
"tree-kill": "^1.2.2",
"tslib": "^2.0.0"
},
diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts
index 40ee72d94729f..fad5f85ab5890 100644
--- a/packages/kbn-dev-utils/src/index.ts
+++ b/packages/kbn-dev-utils/src/index.ts
@@ -37,4 +37,5 @@ export { run, createFailError, createFlagError, combineErrors, isFailError, Flag
export { REPO_ROOT } from './repo_root';
export { KbnClient } from './kbn_client';
export * from './axios';
+export * from './stdio';
export * from './ci_stats_reporter';
diff --git a/packages/kbn-dev-utils/src/proc_runner/proc.ts b/packages/kbn-dev-utils/src/proc_runner/proc.ts
index 59512cbb133b3..bd2368defd7e0 100644
--- a/packages/kbn-dev-utils/src/proc_runner/proc.ts
+++ b/packages/kbn-dev-utils/src/proc_runner/proc.ts
@@ -29,7 +29,7 @@ import { promisify } from 'util';
const treeKillAsync = promisify((...args: [number, string, any]) => treeKill(...args));
import { ToolingLog } from '../tooling_log';
-import { observeLines } from './observe_lines';
+import { observeLines } from '../stdio';
import { createCliError } from './errors';
const SECOND = 1000;
diff --git a/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts
index 9edc63dd7d842..af55622c76198 100644
--- a/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts
+++ b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts
@@ -21,7 +21,7 @@ import { REPO_ROOT } from '../repo_root';
export function createAbsolutePathSerializer(rootPath: string = REPO_ROOT) {
return {
- print: (value: string) => value.replace(rootPath, '').replace(/\\/g, '/'),
+ serialize: (value: string) => value.replace(rootPath, '').replace(/\\/g, '/'),
test: (value: any) => typeof value === 'string' && value.startsWith(rootPath),
};
}
diff --git a/packages/kbn-dev-utils/src/stdio/index.ts b/packages/kbn-dev-utils/src/stdio/index.ts
new file mode 100644
index 0000000000000..3436d971b879a
--- /dev/null
+++ b/packages/kbn-dev-utils/src/stdio/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export * from './observe_lines';
+export * from './observe_readable';
diff --git a/packages/kbn-dev-utils/src/proc_runner/observe_lines.ts b/packages/kbn-dev-utils/src/stdio/observe_lines.ts
similarity index 100%
rename from packages/kbn-dev-utils/src/proc_runner/observe_lines.ts
rename to packages/kbn-dev-utils/src/stdio/observe_lines.ts
diff --git a/packages/kbn-dev-utils/src/proc_runner/observe_readable.ts b/packages/kbn-dev-utils/src/stdio/observe_readable.ts
similarity index 100%
rename from packages/kbn-dev-utils/src/proc_runner/observe_readable.ts
rename to packages/kbn-dev-utils/src/stdio/observe_readable.ts
diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json
index c7bf1dd60985d..c11bd1b646933 100644
--- a/packages/kbn-optimizer/package.json
+++ b/packages/kbn-optimizer/package.json
@@ -31,7 +31,7 @@
"execa": "^4.0.2",
"file-loader": "^4.2.0",
"istanbul-instrumenter-loader": "^3.0.1",
- "jest-diff": "^25.1.0",
+ "jest-diff": "^25.5.0",
"json-stable-stringify": "^1.0.1",
"loader-utils": "^1.2.3",
"node-sass": "^4.13.0",
@@ -39,7 +39,7 @@
"postcss-loader": "^3.0.0",
"raw-loader": "^3.1.0",
"resolve-url-loader": "^3.1.1",
- "rxjs": "^6.5.3",
+ "rxjs": "^6.5.5",
"sass-loader": "^8.0.2",
"style-loader": "^1.1.3",
"terser-webpack-plugin": "^2.1.2",
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 efe3f45d6512f..4410ffe81c0d8 100644
--- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts
+++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts
@@ -33,7 +33,7 @@ const MOCK_REPO_SRC = Path.resolve(__dirname, '../__fixtures__/mock_repo');
const MOCK_REPO_DIR = Path.resolve(TMP_DIR, 'mock_repo');
expect.addSnapshotSerializer({
- print: (value: string) => value.split(REPO_ROOT).join('').replace(/\\/g, '/'),
+ serialize: (value: string) => value.split(REPO_ROOT).join('').replace(/\\/g, '/'),
test: (value: any) => typeof value === 'string' && value.includes(REPO_ROOT),
});
diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js
index 86bd9716c1545..d2b3c1c1ec7c4 100644
--- a/packages/kbn-pm/dist/index.js
+++ b/packages/kbn-pm/dist/index.js
@@ -986,10 +986,10 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var _internal_util_pipe__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(24);
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pipe", function() { return _internal_util_pipe__WEBPACK_IMPORTED_MODULE_17__["pipe"]; });
-/* harmony import */ var _internal_util_noop__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(25);
+/* harmony import */ var _internal_util_noop__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(60);
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "noop", function() { return _internal_util_noop__WEBPACK_IMPORTED_MODULE_18__["noop"]; });
-/* harmony import */ var _internal_util_identity__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(60);
+/* harmony import */ var _internal_util_identity__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(25);
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "identity", function() { return _internal_util_identity__WEBPACK_IMPORTED_MODULE_19__["identity"]; });
/* harmony import */ var _internal_util_isObservable__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(61);
@@ -2163,8 +2163,8 @@ var observable = /*@__PURE__*/ (function () { return typeof Symbol === 'function
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pipe", function() { return pipe; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pipeFromArray", function() { return pipeFromArray; });
-/* harmony import */ var _noop__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(25);
-/** PURE_IMPORTS_START _noop PURE_IMPORTS_END */
+/* harmony import */ var _identity__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(25);
+/** PURE_IMPORTS_START _identity PURE_IMPORTS_END */
function pipe() {
var fns = [];
@@ -2174,8 +2174,8 @@ function pipe() {
return pipeFromArray(fns);
}
function pipeFromArray(fns) {
- if (!fns) {
- return _noop__WEBPACK_IMPORTED_MODULE_0__["noop"];
+ if (fns.length === 0) {
+ return _identity__WEBPACK_IMPORTED_MODULE_0__["identity"];
}
if (fns.length === 1) {
return fns[0];
@@ -2193,10 +2193,12 @@ function pipeFromArray(fns) {
"use strict";
__webpack_require__.r(__webpack_exports__);
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "noop", function() { return noop; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "identity", function() { return identity; });
/** PURE_IMPORTS_START PURE_IMPORTS_END */
-function noop() { }
-//# sourceMappingURL=noop.js.map
+function identity(x) {
+ return x;
+}
+//# sourceMappingURL=identity.js.map
/***/ }),
@@ -3848,26 +3850,34 @@ var AsapAction = /*@__PURE__*/ (function (_super) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Immediate", function() { return Immediate; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TestTools", function() { return TestTools; });
/** PURE_IMPORTS_START PURE_IMPORTS_END */
var nextHandle = 1;
-var tasksByHandle = {};
-function runIfPresent(handle) {
- var cb = tasksByHandle[handle];
- if (cb) {
- cb();
+var RESOLVED = /*@__PURE__*/ (function () { return /*@__PURE__*/ Promise.resolve(); })();
+var activeHandles = {};
+function findAndClearHandle(handle) {
+ if (handle in activeHandles) {
+ delete activeHandles[handle];
+ return true;
}
+ return false;
}
var Immediate = {
setImmediate: function (cb) {
var handle = nextHandle++;
- tasksByHandle[handle] = cb;
- Promise.resolve().then(function () { return runIfPresent(handle); });
+ activeHandles[handle] = true;
+ RESOLVED.then(function () { return findAndClearHandle(handle) && cb(); });
return handle;
},
clearImmediate: function (handle) {
- delete tasksByHandle[handle];
+ findAndClearHandle(handle);
},
};
+var TestTools = {
+ pending: function () {
+ return Object.keys(activeHandles).length;
+ }
+};
//# sourceMappingURL=Immediate.js.map
@@ -4169,12 +4179,10 @@ var VirtualAction = /*@__PURE__*/ (function (_super) {
"use strict";
__webpack_require__.r(__webpack_exports__);
-/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "identity", function() { return identity; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "noop", function() { return noop; });
/** PURE_IMPORTS_START PURE_IMPORTS_END */
-function identity(x) {
- return x;
-}
-//# sourceMappingURL=identity.js.map
+function noop() { }
+//# sourceMappingURL=noop.js.map
/***/ }),
@@ -4728,17 +4736,17 @@ __webpack_require__.r(__webpack_exports__);
-function subscribeToResult(outerSubscriber, result, outerValue, outerIndex, destination) {
- if (destination === void 0) {
- destination = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_0__["InnerSubscriber"](outerSubscriber, outerValue, outerIndex);
+function subscribeToResult(outerSubscriber, result, outerValue, outerIndex, innerSubscriber) {
+ if (innerSubscriber === void 0) {
+ innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_0__["InnerSubscriber"](outerSubscriber, outerValue, outerIndex);
}
- if (destination.closed) {
+ if (innerSubscriber.closed) {
return undefined;
}
if (result instanceof _Observable__WEBPACK_IMPORTED_MODULE_2__["Observable"]) {
- return result.subscribe(destination);
+ return result.subscribe(innerSubscriber);
}
- return Object(_subscribeTo__WEBPACK_IMPORTED_MODULE_1__["subscribeTo"])(result)(destination);
+ return Object(_subscribeTo__WEBPACK_IMPORTED_MODULE_1__["subscribeTo"])(result)(innerSubscriber);
}
//# sourceMappingURL=subscribeToResult.js.map
@@ -5010,7 +5018,7 @@ function concatAll() {
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mergeAll", function() { return mergeAll; });
/* harmony import */ var _mergeMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(82);
-/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(60);
+/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(25);
/** PURE_IMPORTS_START _mergeMap,_util_identity PURE_IMPORTS_END */
@@ -5108,10 +5116,13 @@ var MergeMapSubscriber = /*@__PURE__*/ (function (_super) {
this._innerSub(result, value, index);
};
MergeMapSubscriber.prototype._innerSub = function (ish, value, index) {
- var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_3__["InnerSubscriber"](this, undefined, undefined);
+ var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_3__["InnerSubscriber"](this, value, index);
var destination = this.destination;
destination.add(innerSubscriber);
- Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_1__["subscribeToResult"])(this, ish, value, index, innerSubscriber);
+ var innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_1__["subscribeToResult"])(this, ish, undefined, undefined, innerSubscriber);
+ if (innerSubscription !== innerSubscriber) {
+ destination.add(innerSubscription);
+ }
};
MergeMapSubscriber.prototype._complete = function () {
this.hasCompleted = true;
@@ -5607,7 +5618,7 @@ function fromEventPattern(addHandler, removeHandler, resultSelector) {
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "generate", function() { return generate; });
/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
-/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(60);
+/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(25);
/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(45);
/** PURE_IMPORTS_START _Observable,_util_identity,_util_isScheduler PURE_IMPORTS_END */
@@ -5866,7 +5877,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "NEVER", function() { return NEVER; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "never", function() { return never; });
/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(9);
-/* harmony import */ var _util_noop__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(25);
+/* harmony import */ var _util_noop__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(60);
/** PURE_IMPORTS_START _Observable,_util_noop PURE_IMPORTS_END */
@@ -51259,7 +51270,10 @@ var CatchSubscriber = /*@__PURE__*/ (function (_super) {
this._unsubscribeAndRecycle();
var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__["InnerSubscriber"](this, undefined, undefined);
this.add(innerSubscriber);
- Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, result, undefined, undefined, innerSubscriber);
+ var innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, result, undefined, undefined, innerSubscriber);
+ if (innerSubscription !== innerSubscriber) {
+ this.add(innerSubscription);
+ }
}
};
return CatchSubscriber;
@@ -52483,10 +52497,13 @@ var ExhaustMapSubscriber = /*@__PURE__*/ (function (_super) {
this._innerSub(result, value, index);
};
ExhaustMapSubscriber.prototype._innerSub = function (result, value, index) {
- var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__["InnerSubscriber"](this, undefined, undefined);
+ var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__["InnerSubscriber"](this, value, index);
var destination = this.destination;
destination.add(innerSubscriber);
- Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, result, value, index, innerSubscriber);
+ var innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, result, undefined, undefined, innerSubscriber);
+ if (innerSubscription !== innerSubscriber) {
+ destination.add(innerSubscription);
+ }
};
ExhaustMapSubscriber.prototype._complete = function () {
this.hasCompleted = true;
@@ -52771,7 +52788,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(420);
/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(410);
/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(419);
-/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(60);
+/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25);
/** PURE_IMPORTS_START _util_EmptyError,_filter,_take,_defaultIfEmpty,_throwIfEmpty,_util_identity PURE_IMPORTS_END */
@@ -52879,7 +52896,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(433);
/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(419);
/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(410);
-/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(60);
+/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25);
/** PURE_IMPORTS_START _util_EmptyError,_filter,_takeLast,_throwIfEmpty,_defaultIfEmpty,_util_identity PURE_IMPORTS_END */
@@ -53305,10 +53322,13 @@ var MergeScanSubscriber = /*@__PURE__*/ (function (_super) {
}
};
MergeScanSubscriber.prototype._innerSub = function (ish, value, index) {
- var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_3__["InnerSubscriber"](this, undefined, undefined);
+ var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_3__["InnerSubscriber"](this, value, index);
var destination = this.destination;
destination.add(innerSubscriber);
- Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_1__["subscribeToResult"])(this, ish, value, index, innerSubscriber);
+ var innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_1__["subscribeToResult"])(this, ish, undefined, undefined, innerSubscriber);
+ if (innerSubscription !== innerSubscriber) {
+ destination.add(innerSubscription);
+ }
};
MergeScanSubscriber.prototype._complete = function () {
this.hasCompleted = true;
@@ -53495,7 +53515,10 @@ var OnErrorResumeNextSubscriber = /*@__PURE__*/ (function (_super) {
var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_4__["InnerSubscriber"](this, undefined, undefined);
var destination = this.destination;
destination.add(innerSubscriber);
- Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__["subscribeToResult"])(this, next, undefined, undefined, innerSubscriber);
+ var innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__["subscribeToResult"])(this, next, undefined, undefined, innerSubscriber);
+ if (innerSubscription !== innerSubscriber) {
+ destination.add(innerSubscription);
+ }
}
else {
this.destination.complete();
@@ -54333,6 +54356,7 @@ function shareReplayOperator(_a) {
},
complete: function () {
isComplete = true;
+ subscription = undefined;
subject.complete();
},
});
@@ -54572,7 +54596,11 @@ var SkipUntilSubscriber = /*@__PURE__*/ (function (_super) {
var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__["InnerSubscriber"](_this, undefined, undefined);
_this.add(innerSubscriber);
_this.innerSubscription = innerSubscriber;
- Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(_this, notifier, undefined, undefined, innerSubscriber);
+ var innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(_this, notifier, undefined, undefined, innerSubscriber);
+ if (innerSubscription !== innerSubscriber) {
+ _this.add(innerSubscription);
+ _this.innerSubscription = innerSubscription;
+ }
return _this;
}
SkipUntilSubscriber.prototype._next = function (value) {
@@ -54781,7 +54809,7 @@ var SubscribeOnObservable = /*@__PURE__*/ (function (_super) {
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return switchAll; });
/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(471);
-/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(60);
+/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(25);
/** PURE_IMPORTS_START _switchMap,_util_identity PURE_IMPORTS_END */
@@ -54851,10 +54879,13 @@ var SwitchMapSubscriber = /*@__PURE__*/ (function (_super) {
if (innerSubscription) {
innerSubscription.unsubscribe();
}
- var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__["InnerSubscriber"](this, undefined, undefined);
+ var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__["InnerSubscriber"](this, value, index);
var destination = this.destination;
destination.add(innerSubscriber);
- this.innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, result, value, index, innerSubscriber);
+ this.innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, result, undefined, undefined, innerSubscriber);
+ if (this.innerSubscription !== innerSubscriber) {
+ destination.add(this.innerSubscription);
+ }
};
SwitchMapSubscriber.prototype._complete = function () {
var innerSubscription = this.innerSubscription;
@@ -55025,7 +55056,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "tap", function() { return tap; });
/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12);
/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(11);
-/* harmony import */ var _util_noop__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(25);
+/* harmony import */ var _util_noop__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(60);
/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(13);
/** PURE_IMPORTS_START tslib,_Subscriber,_util_noop,_util_isFunction PURE_IMPORTS_END */
diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json
index d9ff0be55bde5..3e7ed49c61314 100644
--- a/packages/kbn-pm/package.json
+++ b/packages/kbn-pm/package.json
@@ -53,7 +53,7 @@
"ora": "^1.4.0",
"prettier": "^2.0.5",
"read-pkg": "^5.2.0",
- "rxjs": "^6.5.3",
+ "rxjs": "^6.5.5",
"spawn-sync": "^1.0.15",
"string-replace-loader": "^2.2.0",
"strip-ansi": "^4.0.0",
diff --git a/packages/kbn-pm/src/commands/bootstrap.test.ts b/packages/kbn-pm/src/commands/bootstrap.test.ts
index c69427e8b3c74..97505a66a1fff 100644
--- a/packages/kbn-pm/src/commands/bootstrap.test.ts
+++ b/packages/kbn-pm/src/commands/bootstrap.test.ts
@@ -127,13 +127,13 @@ test('handles dependencies of dependencies', async () => {
expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir');
expect(logWriter.messages).toMatchInlineSnapshot(`
Array [
- " info [kibana] running yarn",
+ info [kibana] running yarn,
"",
"",
- " info [bar] running yarn",
+ info [bar] running yarn,
"",
"",
- " info [foo] running yarn",
+ info [foo] running yarn,
"",
"",
]
@@ -174,7 +174,7 @@ test('does not run installer if no deps in package', async () => {
expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir');
expect(logWriter.messages).toMatchInlineSnapshot(`
Array [
- " info [kibana] running yarn",
+ info [kibana] running yarn,
"",
"",
]
diff --git a/packages/kbn-pm/src/test_helpers/strip_ansi_snapshot_serializer.ts b/packages/kbn-pm/src/test_helpers/strip_ansi_snapshot_serializer.ts
index bf8bd129c0bbf..d5b73dfd4d5ee 100644
--- a/packages/kbn-pm/src/test_helpers/strip_ansi_snapshot_serializer.ts
+++ b/packages/kbn-pm/src/test_helpers/strip_ansi_snapshot_serializer.ts
@@ -20,9 +20,9 @@
import hasAnsi from 'has-ansi';
import stripAnsi from 'strip-ansi';
-export const stripAnsiSnapshotSerializer = {
- print(value: string, serialize: (val: string) => string) {
- return serialize(stripAnsi(value));
+export const stripAnsiSnapshotSerializer: jest.SnapshotSerializerPlugin = {
+ serialize(value: string) {
+ return stripAnsi(value);
},
test(value: any) {
diff --git a/packages/kbn-pm/src/utils/__snapshots__/projects_tree.test.ts.snap b/packages/kbn-pm/src/utils/__snapshots__/projects_tree.test.ts.snap
index 12cbc704b4803..9993e2eaf377a 100644
--- a/packages/kbn-pm/src/utils/__snapshots__/projects_tree.test.ts.snap
+++ b/packages/kbn-pm/src/utils/__snapshots__/projects_tree.test.ts.snap
@@ -1,29 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`handles projects outside root folder 1`] = `
-"kibana
+kibana
├── packages
│ ├── bar
│ └── foo
└── ../plugins
├── baz
├── quux
- └── zorge"
+ └── zorge
`;
exports[`handles projects with root folder 1`] = `
-"kibana
+kibana
└── packages
├── bar
- └── foo"
+ └── foo
`;
exports[`handles projects within projects outside root folder 1`] = `
-"kibana
+kibana
├── packages
│ ├── bar
│ └── foo
└── ../kibana-extra/additional_projects (with-additional-projects)
├── packages/baz
- └── plugins/quux"
+ └── plugins/quux
`;
diff --git a/packages/kbn-pm/src/utils/link_project_executables.test.ts b/packages/kbn-pm/src/utils/link_project_executables.test.ts
index f5c19d569f6b8..a98d2caa30343 100644
--- a/packages/kbn-pm/src/utils/link_project_executables.test.ts
+++ b/packages/kbn-pm/src/utils/link_project_executables.test.ts
@@ -115,9 +115,9 @@ describe('bin script points to a file', () => {
expect(getFsMockCalls()).toMatchSnapshot('fs module calls');
expect(logWriter.messages).toMatchInlineSnapshot(`
Array [
- " debg Linking package executables",
- " debg [foo] bar -> ../bar/bin/bar.js",
- " debg [baz] bar -> ../bar/bin/bar.js",
+ debg Linking package executables,
+ debg [foo] bar -> ../bar/bin/bar.js,
+ debg [baz] bar -> ../bar/bin/bar.js,
]
`);
});
diff --git a/packages/kbn-spec-to-console/package.json b/packages/kbn-spec-to-console/package.json
index 6bb9945af8b34..a1e73d985ae20 100644
--- a/packages/kbn-spec-to-console/package.json
+++ b/packages/kbn-spec-to-console/package.json
@@ -17,7 +17,7 @@
},
"homepage": "https://github.com/jbudz/spec-to-console#readme",
"devDependencies": {
- "jest": "^24.9.0",
+ "jest": "^25.5.4",
"prettier": "^2.0.5"
},
"dependencies": {
diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json
index 72308fa31287f..4f035c3334b86 100644
--- a/packages/kbn-storybook/package.json
+++ b/packages/kbn-storybook/package.json
@@ -23,7 +23,7 @@
"mini-css-extract-plugin": "0.7.0",
"normalize-path": "3.0.0",
"react-docgen-typescript-loader": "3.1.0",
- "rxjs": "6.5.2",
+ "rxjs": "6.5.5",
"serve-static": "1.14.1",
"styled-components": "^5.1.0",
"webpack": "^4.41.5"
diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json
index c74dba8a34c9d..042de2617565e 100644
--- a/packages/kbn-test/package.json
+++ b/packages/kbn-test/package.json
@@ -1,9 +1,9 @@
{
"name": "@kbn/test",
- "main": "./target/index.js",
"version": "1.0.0",
- "license": "Apache-2.0",
"private": true,
+ "license": "Apache-2.0",
+ "main": "./target/index.js",
"scripts": {
"build": "babel src --out-dir target --delete-dir-on-start --extensions .ts,.js,.tsx --ignore *.test.js,**/__tests__/** --source-maps=inline",
"kbn:bootstrap": "yarn build",
@@ -13,6 +13,7 @@
"@babel/cli": "^7.10.1",
"@kbn/babel-preset": "1.0.0",
"@kbn/dev-utils": "1.0.0",
+ "@types/joi": "^13.4.2",
"@types/parse-link-header": "^1.0.0",
"@types/puppeteer": "^3.0.0",
"@types/strip-ansi": "^5.2.1",
@@ -23,12 +24,14 @@
"chalk": "^2.4.2",
"dedent": "^0.7.0",
"del": "^5.1.0",
+ "exit-hook": "^2.2.0",
"getopts": "^2.2.4",
"glob": "^7.1.2",
+ "joi": "^13.5.2",
"parse-link-header": "^1.0.1",
"puppeteer": "^3.3.0",
+ "rxjs": "^6.5.5",
"strip-ansi": "^5.2.0",
- "rxjs": "^6.5.3",
"tar-fs": "^1.16.3",
"tmp": "^0.1.0",
"xml2js": "^0.4.22",
diff --git a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts
index 7cbeb18a5ebd4..f8f279151e07f 100644
--- a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts
+++ b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts
@@ -26,7 +26,7 @@ import { createPatch } from 'diff';
// turns out Jest can't encode xml diffs in their JUnit reports...
expect.addSnapshotSerializer({
test: (v) => typeof v === 'string' && (v.includes('<') || v.includes('>')),
- print: (v) => v.replace(//g, '›').replace(/^\s+$/gm, ''),
+ serialize: (v) => v.replace(//g, '›').replace(/^\s+$/gm, ''),
});
jest.mock('fs', () => {
diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts
index c8ccb09903f0d..021562333e59f 100644
--- a/packages/kbn-test/src/functional_test_runner/cli.ts
+++ b/packages/kbn-test/src/functional_test_runner/cli.ts
@@ -19,7 +19,10 @@
import { resolve } from 'path';
import { inspect } from 'util';
+
import { run, createFlagError, Flags } from '@kbn/dev-utils';
+import exitHook from 'exit-hook';
+
import { FunctionalTestRunner } from './functional_test_runner';
const makeAbsolutePath = (v: string) => resolve(process.cwd(), v);
@@ -84,8 +87,7 @@ export function runFtrCli() {
err instanceof Error ? err : new Error(`non-Error type rejection value: ${inspect(err)}`)
)
);
- process.on('SIGTERM', () => teardown());
- process.on('SIGINT', () => teardown());
+ exitHook(teardown);
try {
if (flags['test-stats']) {
diff --git a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts
index 03d4d7643607f..24f5cdceac95b 100644
--- a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts
+++ b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts
@@ -29,6 +29,7 @@ import {
readProviderSpec,
setupMocha,
runTests,
+ DockerServersService,
Config,
SuiteTracker,
} from './lib';
@@ -130,12 +131,19 @@ export class FunctionalTestRunner {
throw new Error('No tests defined.');
}
+ const dockerServers = new DockerServersService(
+ config.get('dockerServers'),
+ this.log,
+ this.lifecycle
+ );
+
// base level services that functional_test_runner exposes
const coreProviders = readProviderSpec('Service', {
lifecycle: () => this.lifecycle,
log: () => this.log,
failureMetadata: () => this.failureMetadata,
config: () => config,
+ dockerServers: () => dockerServers,
});
return await handler(config, coreProviders);
diff --git a/packages/kbn-test/src/functional_test_runner/index.ts b/packages/kbn-test/src/functional_test_runner/index.ts
index c783117a0ba42..cf65ceb51df8e 100644
--- a/packages/kbn-test/src/functional_test_runner/index.ts
+++ b/packages/kbn-test/src/functional_test_runner/index.ts
@@ -20,3 +20,4 @@
export { FunctionalTestRunner } from './functional_test_runner';
export { readConfigFile } from './lib';
export { runFtrCli } from './cli';
+export * from './lib/docker_servers';
diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts
index e9aeee87f1a3b..6cbdc5ec7fc20 100644
--- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts
+++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts
@@ -57,6 +57,27 @@ const appUrlPartsSchema = () =>
})
.default();
+const requiredWhenEnabled = (schema: Joi.Schema) => {
+ return Joi.when('enabled', {
+ is: true,
+ then: schema.required(),
+ otherwise: schema.optional(),
+ });
+};
+
+const dockerServerSchema = () =>
+ Joi.object()
+ .keys({
+ enabled: Joi.boolean().required(),
+ image: requiredWhenEnabled(Joi.string()),
+ port: requiredWhenEnabled(Joi.number()),
+ portInContainer: requiredWhenEnabled(Joi.number()),
+ waitForLogLine: Joi.alternatives(Joi.object().type(RegExp), Joi.string()).optional(),
+ waitFor: Joi.func().optional(),
+ args: Joi.array().items(Joi.string()).optional(),
+ })
+ .default();
+
const defaultRelativeToConfigPath = (path: string) => {
const makeDefault: any = (_: any, options: any) => resolve(dirname(options.context.path), path);
makeDefault.description = `/${path}`;
@@ -259,5 +280,7 @@ export const schema = Joi.object()
disableTestUser: Joi.boolean(),
})
.default(),
+
+ dockerServers: Joi.object().pattern(Joi.string(), dockerServerSchema()).default(),
})
.default();
diff --git a/packages/kbn-test/src/functional_test_runner/lib/docker_servers/README.md b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/README.md
new file mode 100644
index 0000000000000..d75faf4c854aa
--- /dev/null
+++ b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/README.md
@@ -0,0 +1,113 @@
+# Functional Test Runner - Docker Servers
+
+In order to make it simpler to run some services while the functional tests are running, we've added the ability to execute docker containers while the tests execute for the purpose of exposing services to the tests. These containers are expected to expose an application via a single HTTP port and live for the life of the tests. If the application exits for any reason before the tests complete the tests will abort.
+
+To configure docker servers in your FTR config add the `dockerServers` key to your config like so:
+
+```ts
+// import this helper to get TypeScript support for this section of the config
+import { defineDockerServersConfig } from '@kbn/test';
+
+export default function () {
+ return {
+ ...
+
+ dockerServers: defineDockerServersConfig({
+ // unique names are used in logging and to get the details of this server in the tests
+ helloWorld: {
+ /** disable this docker server unless the user sets some flag/env var */
+ enabled: !!process.env.HELLO_WORLD_PORT,
+ /** the docker image to pull and run */
+ image: 'vad1mo/hello-world-rest',
+ /** The port that this application will be accessible via locally */
+ port: process.env.HELLO_WORLD_PORT,
+ /** The port that the container binds to in the container */
+ portInContainer: 5050,
+ /**
+ * OPTIONAL: string/regex to look for in the log, when specified the
+ * tests won't start until a line containing this string, or matching
+ * this expression is found.
+ */
+ waitForLogLine: /hello/,
+ /**
+ * OPTIONAL: function that is called when server is started, when defined
+ * it is called to give the configuration an option to write custom delay
+ * logic. The function is passed a DockerServer object, which is described
+ * below, and an observable of the log lines produced by the application
+ */
+ async waitFor(server, logLine$) {
+ await logLine$.pipe(
+ filter(line => line.includes('...')),
+ tap((line) => {
+ console.log('marking server ready because this line was logged:', line);
+ console.log('server accessible from url', server.url);
+ })
+ ).toPromise()
+ }
+ }
+ })
+ }
+}
+```
+
+To consume the test server, use can use something like supertest to send request. Just make sure that you disable your test suite if the user doesn't choose to enable your docker server:
+
+```ts
+import makeSupertest from 'supertest-as-promised';
+import { FtrProviderContext } from '../ftr_provider_context';
+
+export default function ({ getService }: FtrProviderContext) {
+ const dockerServers = getService('dockerServers');
+ const log = getService('log');
+
+ const server = dockerServers.get('helloWorld');
+ const supertest = makeSupertest(server.url);
+
+ describe('test suite name', function () {
+ if (!server.enabled) {
+ log.warning(
+ 'disabling tests because server is not enabled, set HELLO_WORLD_PORT to run them'
+ );
+ this.pending = true;
+ }
+
+ it('test name', async () => {
+ await supertest.get('/foo/bar').expect(200);
+ });
+ });
+}
+```
+
+## `DockerServersService`
+
+The docker servers service is a core service that is always available in functional test runner tests. When you call `getService('dockerServers')` you will receive an instance of the `DockerServersService` class which has to methods:
+
+### `has(name: string): boolean`
+
+Determine if a name resolves to a known docker server.
+
+### `isEnabled(name: string): boolean`
+
+Determine if a named server is enabled.
+
+### `get(name: string): DockerServer`
+
+Get a `DockerServer` object for the server with the given name.
+
+
+## `DockerServer`
+
+The object passed to the `waitFor()` config function and returned by `DockerServersService#get()`
+
+```ts
+{
+ url: string;
+ name: string;
+
+ portInContainer: number;
+ port: number;
+ image: string;
+ waitForLogLine?: RegExp | string;
+ waitFor?: (server: DockerServer, logLine$: Rx.Observable) => Promise;
+}
+```
diff --git a/packages/kbn-test/src/functional_test_runner/lib/docker_servers/container_logs.ts b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/container_logs.ts
new file mode 100644
index 0000000000000..f8e8137ce40a2
--- /dev/null
+++ b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/container_logs.ts
@@ -0,0 +1,43 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import execa from 'execa';
+import * as Rx from 'rxjs';
+import { tap } from 'rxjs/operators';
+import { ToolingLog, observeLines } from '@kbn/dev-utils';
+
+/**
+ * Observe the logs for a container, reflecting the log lines
+ * to the ToolingLog and the returned Observable
+ */
+export function observeContainerLogs(name: string, containerId: string, log: ToolingLog) {
+ log.debug(`[docker:${name}] streaming logs from container [id=${containerId}]`);
+ const logsProc = execa('docker', ['logs', '--follow', containerId], {
+ stdio: ['ignore', 'pipe', 'pipe'],
+ });
+
+ const logLine$ = new Rx.Subject();
+
+ Rx.merge(
+ observeLines(logsProc.stdout).pipe(tap((line) => log.info(`[docker:${name}] ${line}`))),
+ observeLines(logsProc.stderr).pipe(tap((line) => log.error(`[docker:${name}] ${line}`)))
+ ).subscribe(logLine$);
+
+ return logLine$.asObservable();
+}
diff --git a/packages/kbn-test/src/functional_test_runner/lib/docker_servers/container_running.ts b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/container_running.ts
new file mode 100644
index 0000000000000..3e3247a6ae3bb
--- /dev/null
+++ b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/container_running.ts
@@ -0,0 +1,65 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import execa from 'execa';
+import * as Rx from 'rxjs';
+import { ToolingLog } from '@kbn/dev-utils';
+
+/**
+ * Create an observable that errors if a docker
+ * container exits before being unsubscribed
+ */
+export function observeContainerRunning(name: string, containerId: string, log: ToolingLog) {
+ return new Rx.Observable((subscriber) => {
+ log.debug(`[docker:${name}] watching container for exit status [${containerId}]`);
+
+ const exitCodeProc = execa('docker', ['wait', containerId]);
+
+ let exitCode: Error | number | null = null;
+ exitCodeProc
+ .then(({ stdout }) => {
+ exitCode = Number.parseInt(stdout.trim(), 10);
+
+ if (Number.isFinite(exitCode)) {
+ subscriber.error(
+ new Error(`container [id=${containerId}] unexpectedly exitted with code ${exitCode}`)
+ );
+ } else {
+ subscriber.error(
+ new Error(`unable to parse exit code from output of "docker wait": ${stdout}`)
+ );
+ }
+ })
+ .catch((error) => {
+ if (error?.killed) {
+ // ignore errors thrown because the process was killed
+ subscriber.complete();
+ return;
+ }
+
+ subscriber.error(
+ new Error(`failed to monitor process with "docker wait": ${error.message}`)
+ );
+ });
+
+ return () => {
+ exitCodeProc.kill('SIGKILL');
+ };
+ });
+}
diff --git a/packages/kbn-test/src/functional_test_runner/lib/docker_servers/define_docker_servers_config.ts b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/define_docker_servers_config.ts
new file mode 100644
index 0000000000000..bb91ddbacf039
--- /dev/null
+++ b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/define_docker_servers_config.ts
@@ -0,0 +1,45 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as Rx from 'rxjs';
+
+export interface DockerServerSpec {
+ enabled: boolean;
+ portInContainer: number;
+ port: number;
+ image: string;
+ waitForLogLine?: RegExp | string;
+ /** a function that should return an observable that will allow the tests to execute as soon as it emits anything */
+ waitFor?: (server: DockerServer, logLine$: Rx.Observable) => Rx.Observable;
+ /* additional command line arguments passed to docker run */
+ args?: string[];
+}
+
+export interface DockerServer extends DockerServerSpec {
+ name: string;
+ url: string;
+}
+
+/**
+ * Helper that helps authors use the type definitions for the section of the FTR config
+ * under the `dockerServers` key.
+ */
+export function defineDockerServersConfig(config: { [name: string]: DockerServerSpec } | {}) {
+ return config;
+}
diff --git a/packages/kbn-test/src/functional_test_runner/lib/docker_servers/docker_servers_service.ts b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/docker_servers_service.ts
new file mode 100644
index 0000000000000..606902228e1b7
--- /dev/null
+++ b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/docker_servers_service.ts
@@ -0,0 +1,224 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import Url from 'url';
+import execa from 'execa';
+import * as Rx from 'rxjs';
+import { filter, take, map } from 'rxjs/operators';
+import { ToolingLog } from '@kbn/dev-utils';
+
+import { Lifecycle } from '../lifecycle';
+import { observeContainerRunning } from './container_running';
+import { observeContainerLogs } from './container_logs';
+import { DockerServer, DockerServerSpec } from './define_docker_servers_config';
+
+const SECOND = 1000;
+
+export class DockerServersService {
+ private servers: DockerServer[];
+
+ constructor(
+ configs: {
+ [name: string]: DockerServerSpec;
+ },
+ private log: ToolingLog,
+ private lifecycle: Lifecycle
+ ) {
+ this.servers = Object.entries(configs).map(([name, config]) => ({
+ ...config,
+ name,
+ url: Url.format({
+ protocol: 'http:',
+ hostname: 'localhost',
+ port: config.port,
+ }),
+ }));
+
+ this.lifecycle.beforeTests.add(async () => {
+ await this.startServers();
+ });
+ }
+
+ isEnabled(name: string) {
+ return this.get(name).enabled;
+ }
+
+ has(name: string) {
+ return this.servers.some((s) => s.name === name);
+ }
+
+ get(name: string) {
+ const server = this.servers.find((s) => s.name === name);
+ if (!server) {
+ throw new Error(`no server with name "${name}"`);
+ }
+ return { ...server };
+ }
+
+ private async dockerRun(server: DockerServer) {
+ const { args } = server;
+ try {
+ this.log.info(`[docker:${server.name}] running image "${server.image}"`);
+
+ const dockerArgs = [
+ 'run',
+ '-dit',
+ args || [],
+ '-p',
+ `${server.port}:${server.portInContainer}`,
+ server.image,
+ ].flat();
+ const res = await execa('docker', dockerArgs);
+
+ return res.stdout.trim();
+ } catch (error) {
+ if (error?.exitCode === 125 && error?.message.includes('port is already allocated')) {
+ throw new Error(`
+ [docker:${server.name}] Another process is already listening on port ${server.port}.
+
+ When this happens on CI it is usually because the port number isn't taking into
+ account parallel workers running on the same machine.
+
+ When this happens locally it is usually because the functional test runner didn't
+ have a chance to cleanup the running docker containers before being killed.
+
+ To see if this is the case:
+
+ 1. make sure that there aren't other instances of the functional test runner running
+ 2. run \`docker ps\` to see the containers running
+ 3. if one of them lists that it is using port ${server.port} then kill it with \`docker kill "container ID"\`
+ `);
+ }
+
+ throw error;
+ }
+ }
+
+ private async startServer(server: DockerServer) {
+ const { log, lifecycle } = this;
+ const { image, name, waitFor, waitForLogLine } = server;
+
+ // pull image from registry
+ log.info(`[docker:${name}] pulling docker image "${image}"`);
+ await execa('docker', ['pull', image]);
+
+ // run the image that we just pulled
+ const containerId = await this.dockerRun(server);
+
+ lifecycle.cleanup.add(() => {
+ try {
+ execa.sync('docker', ['kill', containerId]);
+ execa.sync('docker', ['rm', containerId]);
+ } catch (error) {
+ if (
+ error.message.includes(`Container ${containerId} is not running`) ||
+ error.message.includes(`No such container: ${containerId}`)
+ ) {
+ return;
+ }
+
+ throw error;
+ }
+ });
+
+ // push the logs from the container to our logger, and expose an observable of those lines for testing
+ const logLine$ = observeContainerLogs(name, containerId, log);
+ lifecycle.cleanup.add(async () => {
+ await logLine$.toPromise();
+ });
+
+ // ensure container stays running, error if it exits
+ lifecycle.cleanup.addSub(
+ observeContainerRunning(name, containerId, log).subscribe({
+ error: (error) => {
+ lifecycle.cleanup.after$.subscribe(() => {
+ log.error(`[docker:${name}] Error ensuring that the container is running`);
+ log.error(error);
+ process.exit(1);
+ });
+
+ if (!lifecycle.cleanup.triggered) {
+ lifecycle.cleanup.trigger();
+ }
+ },
+ })
+ );
+
+ // wait for conditions before completing
+ if (waitForLogLine instanceof RegExp) {
+ log.info(`[docker:${name}] Waiting for log line matching /${waitForLogLine.source}/`);
+ }
+ if (typeof waitForLogLine === 'string') {
+ log.info(`[docker:${name}] Waiting for log line containing "${waitForLogLine}"`);
+ }
+ if (waitFor !== undefined) {
+ log.info(`[docker:${name}] Waiting for waitFor() promise to resolve`);
+ }
+ if (waitForLogLine === undefined && waitFor === undefined) {
+ log.warning(`
+ [docker:${name}] No "waitFor*" condition defined, you should always
+ define a wait condition to avoid race conditions that are more likely
+ to fail on CI because we're not waiting for the contained server to be ready.
+ `);
+ }
+
+ function firstWithTimeout(source$: Rx.Observable, errorMsg: string, ms = 30 * SECOND) {
+ return Rx.race(
+ source$.pipe(take(1)),
+ Rx.timer(ms).pipe(
+ map(() => {
+ throw new Error(`[docker:${name}] ${errorMsg} within ${ms / SECOND} seconds`);
+ })
+ )
+ );
+ }
+
+ await Rx.merge(
+ waitFor === undefined
+ ? Rx.EMPTY
+ : firstWithTimeout(
+ waitFor(server, logLine$),
+ `didn't find a line containing "${waitForLogLine}"`
+ ),
+
+ waitForLogLine === undefined
+ ? Rx.EMPTY
+ : firstWithTimeout(
+ logLine$.pipe(
+ filter((line) =>
+ waitForLogLine instanceof RegExp
+ ? waitForLogLine.test(line)
+ : line.includes(waitForLogLine)
+ )
+ ),
+ `waitForLogLine didn't emit anything`
+ )
+ ).toPromise();
+ }
+
+ private async startServers() {
+ await Promise.all(
+ this.servers.map(async (server) => {
+ if (server.enabled) {
+ await this.startServer(server);
+ }
+ })
+ );
+ }
+}
diff --git a/packages/kbn-test/src/functional_test_runner/lib/docker_servers/index.ts b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/index.ts
new file mode 100644
index 0000000000000..23df9df0458d7
--- /dev/null
+++ b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export * from './docker_servers_service';
+export * from './define_docker_servers_config';
diff --git a/packages/kbn-test/src/functional_test_runner/lib/index.ts b/packages/kbn-test/src/functional_test_runner/lib/index.ts
index 2e534974e1d76..4c34f334a8c25 100644
--- a/packages/kbn-test/src/functional_test_runner/lib/index.ts
+++ b/packages/kbn-test/src/functional_test_runner/lib/index.ts
@@ -23,4 +23,5 @@ export { readConfigFile, Config } from './config';
export { readProviderSpec, ProviderCollection, Provider } from './providers';
export { runTests, setupMocha } from './mocha';
export { FailureMetadata } from './failure_metadata';
+export * from './docker_servers';
export { SuiteTracker } from './suite_tracker';
diff --git a/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.ts b/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.ts
index 5c7fdb532faa1..70fcd4d217be0 100644
--- a/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.ts
+++ b/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.ts
@@ -28,6 +28,8 @@ export type GetArgsType> = T extends LifecyclePhas
export class LifecyclePhase {
private readonly handlers: Array<(...args: Args) => Promise | void> = [];
+ public triggered = false;
+
private readonly beforeSubj = new Rx.Subject();
public readonly before$ = this.beforeSubj.asObservable();
@@ -46,29 +48,39 @@ export class LifecyclePhase {
this.handlers.push(fn);
}
+ public addSub(sub: Rx.Subscription) {
+ this.handlers.push(() => {
+ sub.unsubscribe();
+ });
+ }
+
public async trigger(...args: Args) {
- if (this.beforeSubj.isStopped) {
+ if (this.options.singular && this.triggered) {
throw new Error(`singular lifecycle event can only be triggered once`);
}
+ this.triggered = true;
+
this.beforeSubj.next(undefined);
if (this.options.singular) {
this.beforeSubj.complete();
}
// catch the first error but still execute all handlers
- let error;
+ let error: Error | undefined;
// shuffle the handlers to prevent relying on their order
- for (const fn of shuffle(this.handlers)) {
- try {
- await fn(...args);
- } catch (_error) {
- if (!error) {
- error = _error;
+ await Promise.all(
+ shuffle(this.handlers).map(async (fn) => {
+ try {
+ await fn(...args);
+ } catch (_error) {
+ if (!error) {
+ error = _error;
+ }
}
- }
- }
+ })
+ );
this.afterSubj.next(undefined);
if (this.options.singular) {
diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts
index e6b180d979f17..bc17b731f0aa5 100644
--- a/packages/kbn-test/src/index.ts
+++ b/packages/kbn-test/src/index.ts
@@ -52,4 +52,5 @@ export { makeJunitReportPath } from './junit_report_path';
export { CI_PARALLEL_PROCESS_PREFIX } from './ci_parallel_process_prefix';
+export * from './functional_test_runner';
export * from './page_load_metrics';
diff --git a/packages/kbn-test/types/ftr.d.ts b/packages/kbn-test/types/ftr.d.ts
index 8beecab88878d..38eb69d3e6811 100644
--- a/packages/kbn-test/types/ftr.d.ts
+++ b/packages/kbn-test/types/ftr.d.ts
@@ -18,7 +18,12 @@
*/
import { ToolingLog } from '@kbn/dev-utils';
-import { Config, Lifecycle, FailureMetadata } from '../src/functional_test_runner/lib';
+import {
+ Config,
+ Lifecycle,
+ FailureMetadata,
+ DockerServersService,
+} from '../src/functional_test_runner/lib';
export { Lifecycle, Config, FailureMetadata };
@@ -61,7 +66,9 @@ export interface GenericFtrProviderContext<
* Determine if a service is avaliable
* @param serviceName
*/
- hasService(serviceName: 'config' | 'log' | 'lifecycle' | 'failureMetadata'): true;
+ hasService(
+ serviceName: 'config' | 'log' | 'lifecycle' | 'failureMetadata' | 'dockerServers'
+ ): true;
hasService(serviceName: K): serviceName is K;
hasService(serviceName: string): serviceName is Extract;
@@ -73,6 +80,7 @@ export interface GenericFtrProviderContext<
getService(serviceName: 'config'): Config;
getService(serviceName: 'log'): ToolingLog;
getService(serviceName: 'lifecycle'): Lifecycle;
+ getService(serviceName: 'dockerServers'): DockerServersService;
getService(serviceName: 'failureMetadata'): FailureMetadata;
getService(serviceName: T): ServiceMap[T];
diff --git a/packages/kbn-ui-framework/src/components/form/text_input/text_input.test.js b/packages/kbn-ui-framework/src/components/form/text_input/text_input.test.js
index 41d3726582fb5..2e614c3f17611 100644
--- a/packages/kbn-ui-framework/src/components/form/text_input/text_input.test.js
+++ b/packages/kbn-ui-framework/src/components/form/text_input/text_input.test.js
@@ -45,18 +45,34 @@ describe('KuiTextInput', () => {
});
describe('autoFocus', () => {
+ /* eslint-disable no-console */
+ // Silence until enzyme fixed https://github.com/enzymejs/enzyme/issues/2337
+ const originalError = console.error;
+ beforeAll(() => {
+ console.error = jest.fn();
+ });
+ afterAll(() => {
+ console.error = originalError;
+ });
+ /* eslint-enable no-console */
+
test('sets focus on the element', () => {
const component = mount(
- {}} data-test-subj="input" />
+ {}} data-test-subj="input" />,
+ { attachTo: document.body }
);
expect(findTestSubject(component, 'input').getDOMNode()).toBe(document.activeElement);
+ component.unmount();
});
test('does not focus the element by default', () => {
- const component = mount( {}} data-test-subj="input" />);
+ const component = mount( {}} data-test-subj="input" />, {
+ attachTo: document.body,
+ });
expect(findTestSubject(component, 'input').getDOMNode()).not.toBe(document.activeElement);
+ component.unmount();
});
});
diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json
index 0cfbec4349127..9ea006cff2841 100644
--- a/packages/kbn-ui-shared-deps/package.json
+++ b/packages/kbn-ui-shared-deps/package.json
@@ -29,7 +29,7 @@
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"regenerator-runtime": "^0.13.3",
- "rxjs": "^6.5.3",
+ "rxjs": "^6.5.5",
"symbol-observable": "^1.2.0",
"whatwg-fetch": "^3.0.0"
},
diff --git a/src/core/public/application/application_service.test.mocks.ts b/src/core/public/application/application_service.test.mocks.ts
index 727295237d741..11c69c1971fd1 100644
--- a/src/core/public/application/application_service.test.mocks.ts
+++ b/src/core/public/application/application_service.test.mocks.ts
@@ -37,7 +37,11 @@ jest.doMock('history', () => ({
}));
export const parseAppUrlMock = jest.fn();
-jest.doMock('./utils', () => ({
- ...jest.requireActual('./utils'),
- parseAppUrl: parseAppUrlMock,
-}));
+jest.doMock('./utils', () => {
+ const original = jest.requireActual('./utils');
+
+ return {
+ ...original,
+ parseAppUrl: parseAppUrlMock,
+ };
+});
diff --git a/src/core/public/application/integration_tests/application_service.test.tsx b/src/core/public/application/integration_tests/application_service.test.tsx
index e3cd0761c3244..b0419d276dfa1 100644
--- a/src/core/public/application/integration_tests/application_service.test.tsx
+++ b/src/core/public/application/integration_tests/application_service.test.tsx
@@ -147,44 +147,52 @@ describe('ApplicationService', () => {
});
});
- it('redirects to full path when navigating to legacy app', async () => {
- const redirectTo = jest.fn();
- const reloadSpy = jest.spyOn(window.location, 'reload').mockImplementation(() => {});
-
- // In the real application, we use a BrowserHistory instance configured with `basename`. However, in tests we must
- // use MemoryHistory which does not support `basename`. In order to emulate this behavior, we will wrap this
- // instance with a ScopedHistory configured with a basepath.
- history.push(setupDeps.http.basePath.get()); // ScopedHistory constructor will fail if underlying history is not currently at basePath.
- const { register, registerLegacyApp } = service.setup({
- ...setupDeps,
- redirectTo,
- history: new ScopedHistory(history, setupDeps.http.basePath.get()),
+ describe('redirects', () => {
+ beforeAll(() => {
+ Object.defineProperty(window, 'location', {
+ value: {
+ reload: jest.fn(),
+ },
+ });
});
- register(Symbol(), {
- id: 'app1',
- title: 'App1',
- mount: ({ onAppLeave }: AppMountParameters) => {
- onAppLeave((actions) => actions.default());
- return () => undefined;
- },
- });
- registerLegacyApp({
- id: 'myLegacyTestApp',
- appUrl: '/app/myLegacyTestApp',
- title: 'My Legacy Test App',
- });
+ it('to full path when navigating to legacy app', async () => {
+ const redirectTo = jest.fn();
+
+ // In the real application, we use a BrowserHistory instance configured with `basename`. However, in tests we must
+ // use MemoryHistory which does not support `basename`. In order to emulate this behavior, we will wrap this
+ // instance with a ScopedHistory configured with a basepath.
+ history.push(setupDeps.http.basePath.get()); // ScopedHistory constructor will fail if underlying history is not currently at basePath.
+ const { register, registerLegacyApp } = service.setup({
+ ...setupDeps,
+ redirectTo,
+ history: new ScopedHistory(history, setupDeps.http.basePath.get()),
+ });
+
+ register(Symbol(), {
+ id: 'app1',
+ title: 'App1',
+ mount: ({ onAppLeave }: AppMountParameters) => {
+ onAppLeave((actions) => actions.default());
+ return () => undefined;
+ },
+ });
+ registerLegacyApp({
+ id: 'myLegacyTestApp',
+ appUrl: '/app/myLegacyTestApp',
+ title: 'My Legacy Test App',
+ });
- const { navigateToApp, getComponent } = await service.start(startDeps);
+ const { navigateToApp, getComponent } = await service.start(startDeps);
- update = createRenderer(getComponent());
+ update = createRenderer(getComponent());
- await navigate('/test/app/app1');
- await act(() => navigateToApp('myLegacyTestApp', { path: '#/some-path' }));
+ await navigate('/test/app/app1');
+ await act(() => navigateToApp('myLegacyTestApp', { path: '#/some-path' }));
- expect(redirectTo).toHaveBeenCalledWith('/test/app/myLegacyTestApp#/some-path');
- expect(reloadSpy).toHaveBeenCalled();
- reloadSpy.mockRestore();
+ expect(redirectTo).toHaveBeenCalledWith('/test/app/myLegacyTestApp#/some-path');
+ expect(window.location.reload).toHaveBeenCalled();
+ });
});
describe('leaving an application that registered an app leave handler', () => {
diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap
index 60963c0acb990..9239811df2065 100644
--- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap
+++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap
@@ -410,6 +410,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
>
) => void;
@@ -948,17 +956,13 @@ export interface HttpServerInfo {
// @public
export interface HttpServiceSetup {
- // (undocumented)
- auth: {
- get: GetAuthState;
- isAuthenticated: IsAuthenticated;
- };
+ // @deprecated
+ auth: HttpAuth;
basePath: IBasePath;
createCookieSessionStorageFactory: (cookieOptions: SessionStorageCookieOptions) => Promise>;
createRouter: () => IRouter;
csp: ICspConfig;
getServerInfo: () => HttpServerInfo;
- isTlsEnabled: boolean;
registerAuth: (handler: AuthenticationHandler) => void;
registerOnPostAuth: (handler: OnPostAuthHandler) => void;
registerOnPreAuth: (handler: OnPreAuthHandler) => void;
@@ -968,7 +972,9 @@ export interface HttpServiceSetup {
// @public (undocumented)
export interface HttpServiceStart {
- isListening: (port: number) => boolean;
+ auth: HttpAuth;
+ basePath: IBasePath;
+ getServerInfo: () => HttpServerInfo;
}
// @public
diff --git a/src/core/server/server.ts b/src/core/server/server.ts
index 6ca580083648f..ae1a02cf71b88 100644
--- a/src/core/server/server.ts
+++ b/src/core/server/server.ts
@@ -202,10 +202,12 @@ export class Server {
});
const capabilitiesStart = this.capabilities.start();
const uiSettingsStart = await this.uiSettings.start();
+ const httpStart = this.http.getStartContract();
this.coreStart = {
capabilities: capabilitiesStart,
elasticsearch: elasticsearchStart,
+ http: httpStart,
savedObjects: savedObjectsStart,
uiSettings: uiSettingsStart,
};
@@ -221,6 +223,7 @@ export class Server {
});
await this.http.start();
+
await this.rendering.start({
legacy: this.legacy,
});
diff --git a/src/core/server/status/test_utils.ts b/src/core/server/status/test_utils.ts
index 765fa8771f375..737b34e2128b2 100644
--- a/src/core/server/status/test_utils.ts
+++ b/src/core/server/status/test_utils.ts
@@ -21,5 +21,5 @@ import { ServiceStatusLevels, ServiceStatusLevel } from './types';
export const ServiceStatusLevelSnapshotSerializer: jest.SnapshotSerializerPlugin = {
test: (val: any) => Object.values(ServiceStatusLevels).includes(val),
- print: (val: ServiceStatusLevel) => val.toString(),
+ serialize: (val: ServiceStatusLevel) => val.toString(),
};
diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts
index 49d8137a65d39..adf36e4491b79 100644
--- a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts
+++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts
@@ -87,7 +87,6 @@ describe('createOrUpgradeSavedConfig()', () => {
}, 30000);
it('upgrades the previous version on each increment', async function () {
- jest.setTimeout(30000);
// ------------------------------------
// upgrade to 5.4.0
await createOrUpgradeSavedConfig({
@@ -211,5 +210,5 @@ describe('createOrUpgradeSavedConfig()', () => {
'5.4.0': true,
'5.4.0-rc1': true,
});
- });
+ }, 30000);
});
diff --git a/src/core/server/ui_settings/ui_settings_service.test.ts b/src/core/server/ui_settings/ui_settings_service.test.ts
index ebcb0cf1d762f..096ca347e6f4b 100644
--- a/src/core/server/ui_settings/ui_settings_service.test.ts
+++ b/src/core/server/ui_settings/ui_settings_service.test.ts
@@ -49,7 +49,7 @@ describe('uiSettings', () => {
beforeEach(() => {
const coreContext = mockCoreContext.create();
coreContext.configService.atPath.mockReturnValue(new BehaviorSubject({ overrides }));
- const httpSetup = httpServiceMock.createSetupContract();
+ const httpSetup = httpServiceMock.createInternalSetupContract();
const savedObjectsSetup = savedObjectsServiceMock.createInternalSetupContract();
setupDeps = { http: httpSetup, savedObjects: savedObjectsSetup };
savedObjectsClient = savedObjectsClientMock.create();
diff --git a/src/core/test_helpers/strip_ansi_snapshot_serializer.ts b/src/core/test_helpers/strip_ansi_snapshot_serializer.ts
index bf8bd129c0bbf..d5b73dfd4d5ee 100644
--- a/src/core/test_helpers/strip_ansi_snapshot_serializer.ts
+++ b/src/core/test_helpers/strip_ansi_snapshot_serializer.ts
@@ -20,9 +20,9 @@
import hasAnsi from 'has-ansi';
import stripAnsi from 'strip-ansi';
-export const stripAnsiSnapshotSerializer = {
- print(value: string, serialize: (val: string) => string) {
- return serialize(stripAnsi(value));
+export const stripAnsiSnapshotSerializer: jest.SnapshotSerializerPlugin = {
+ serialize(value: string) {
+ return stripAnsi(value);
},
test(value: any) {
diff --git a/src/dev/jest/cli.js b/src/dev/jest/cli.js
index 1d63bb143fe16..40627c4bece74 100644
--- a/src/dev/jest/cli.js
+++ b/src/dev/jest/cli.js
@@ -17,6 +17,6 @@
* under the License.
*/
-import jest from 'jest';
+import { run } from 'jest';
-jest.run(process.argv.slice(2));
+run(process.argv.slice(2));
diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js
index ded7606050ba9..3cedc3281be73 100644
--- a/src/dev/jest/config.js
+++ b/src/dev/jest/config.js
@@ -79,6 +79,7 @@ export default {
coverageReporters: ['html', 'text'],
moduleFileExtensions: ['js', 'json', 'ts', 'tsx', 'node'],
modulePathIgnorePatterns: ['__fixtures__/', 'target/'],
+ testEnvironment: 'jest-environment-jsdom-thirteen',
testMatch: ['**/*.test.{js,ts,tsx}'],
testPathIgnorePatterns: [
'/packages/kbn-ui-framework/(dist|doc_site|generator-kui)/',
diff --git a/src/dev/jest/setup/react_testing_library.js b/src/dev/jest/setup/react_testing_library.js
index 879292b540ba6..41f58354844a3 100644
--- a/src/dev/jest/setup/react_testing_library.js
+++ b/src/dev/jest/setup/react_testing_library.js
@@ -17,7 +17,7 @@
* under the License.
*/
-import '@testing-library/jest-dom/extend-expect';
+import '@testing-library/jest-dom';
/**
* Have to import "/pure" here to not register afterEach() hook clean up
* in the very beginning. There are couple tests which fail with clean up hook.
diff --git a/src/legacy/utils/streams/filter_stream.test.ts b/src/legacy/utils/streams/filter_stream.test.ts
index 7f4901f31c173..28b7f2588628e 100644
--- a/src/legacy/utils/streams/filter_stream.test.ts
+++ b/src/legacy/utils/streams/filter_stream.test.ts
@@ -31,34 +31,34 @@ describe('createFilterStream()', () => {
await createPromiseFromStreams([createListStream(['a', 'b', 'c']), createFilterStream(filter)]);
expect(filter).toMatchInlineSnapshot(`
-[MockFunction] {
- "calls": Array [
- Array [
- "a",
- ],
- Array [
- "b",
- ],
- Array [
- "c",
- ],
- ],
- "results": Array [
- Object {
- "type": "return",
- "value": true,
- },
- Object {
- "type": "return",
- "value": true,
- },
- Object {
- "type": "return",
- "value": true,
- },
- ],
-}
-`);
+ [MockFunction] {
+ "calls": Array [
+ Array [
+ "a",
+ ],
+ Array [
+ "b",
+ ],
+ Array [
+ "c",
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": true,
+ },
+ Object {
+ "type": "return",
+ "value": true,
+ },
+ Object {
+ "type": "return",
+ "value": true,
+ },
+ ],
+ }
+ `);
});
test('send the filtered values on the output stream', async () => {
@@ -69,9 +69,9 @@ describe('createFilterStream()', () => {
]);
expect(result).toMatchInlineSnapshot(`
-Array [
- 2,
-]
-`);
+ Array [
+ 2,
+ ]
+ `);
});
});
diff --git a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap
index 88f30e03df052..da18eb70e5874 100644
--- a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap
+++ b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap
@@ -381,7 +381,7 @@ exports[`Field for array setting should render user value if there is user value
helpText={
-
-
+
@@ -818,7 +818,7 @@ exports[`Field for boolean setting should render user value if there is user val
helpText={
-
-
+
@@ -1236,7 +1236,7 @@ exports[`Field for image setting should render user value if there is user value
helpText={
-
-
+
-
-
+
}
@@ -1620,7 +1620,7 @@ exports[`Field for json setting should render default value if there is no user
helpText={
-
-
+
@@ -1831,7 +1831,7 @@ exports[`Field for json setting should render user value if there is user value
helpText={
-
-
+
@@ -2358,7 +2358,7 @@ exports[`Field for markdown setting should render user value if there is user va
helpText={
-
-
+
@@ -2785,7 +2785,7 @@ exports[`Field for number setting should render user value if there is user valu
helpText={
-
-
+
@@ -3272,7 +3272,7 @@ exports[`Field for select setting should render user value if there is user valu
helpText={
-
-
+
@@ -3695,7 +3695,7 @@ exports[`Field for string setting should render user value if there is user valu
helpText={
-
-
+
@@ -4102,7 +4102,7 @@ exports[`Field for stringWithValidation setting should render user value if ther
helpText={
-
-
+
diff --git a/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap
index bce9cb67537db..e38ccb6866ab6 100644
--- a/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap
+++ b/src/plugins/advanced_settings/public/management_app/components/form/__snapshots__/form.test.tsx.snap
@@ -155,7 +155,7 @@ exports[`Form should not render no settings message when instructed not to 1`] =
id="advancedSettings.form.searchResultText"
values={
Object {
- "clearSearch":
@@ -165,7 +165,7 @@ exports[`Form should not render no settings message when instructed not to 1`] =
values={Object {}}
/>
- ,
+ ,
"settingsCount": 9,
}
}
@@ -367,7 +367,7 @@ exports[`Form should render no settings message when there are no settings 1`] =
id="advancedSettings.form.searchResultText"
values={
Object {
- "clearSearch":
@@ -377,7 +377,7 @@ exports[`Form should render no settings message when there are no settings 1`] =
values={Object {}}
/>
- ,
+ ,
"settingsCount": 9,
}
}
@@ -579,7 +579,7 @@ exports[`Form should render normally 1`] = `
id="advancedSettings.form.searchResultText"
values={
Object {
- "clearSearch":
@@ -589,7 +589,7 @@ exports[`Form should render normally 1`] = `
values={Object {}}
/>
- ,
+ ,
"settingsCount": 9,
}
}
@@ -791,7 +791,7 @@ exports[`Form should render read-only when saving is disabled 1`] = `
id="advancedSettings.form.searchResultText"
values={
Object {
- "clearSearch":
@@ -801,7 +801,7 @@ exports[`Form should render read-only when saving is disabled 1`] = `
values={Object {}}
/>
- ,
+ ,
"settingsCount": 9,
}
}
diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js
index 1cc626cfa668b..bd5932e88b5e9 100644
--- a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js
+++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js
@@ -1,5 +1,57 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* @notice
+ *
+ * This product includes code that is based on Ace editor, which was available
+ * under a "BSD" license.
+ *
+ * Distributed under the BSD license:
+ *
+ * Copyright (c) 2010, Ajax.org B.V.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of Ajax.org B.V. nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
/* eslint-disable */
-/*
+/*
This file is loaded up as a blob by Brace to hand to Ace to load as Jsonp
(hence the redefining of everything). It is based on the javascript
mode from the brace distro.
@@ -197,7 +249,6 @@ ace.define('ace/lib/oop', ['require', 'exports', 'module'], function (
acequire,
exports
) {
-
(exports.inherits = function (ctor, superCtor) {
(ctor.super_ = superCtor),
(ctor.prototype = Object.create(superCtor.prototype, {
@@ -221,7 +272,6 @@ ace.define('ace/range', ['require', 'exports', 'module'], function (
acequire,
exports
) {
-
let comparePoints = function (p1, p2) {
return p1.row - p2.row || p1.column - p2.column;
},
@@ -426,7 +476,6 @@ ace.define('ace/apply_delta', ['require', 'exports', 'module'], function (
acequire,
exports
) {
-
exports.applyDelta = function (docLines, delta) {
let row = delta.start.row,
startColumn = delta.start.column,
@@ -467,7 +516,6 @@ ace.define(
'ace/lib/event_emitter',
['require', 'exports', 'module'],
function (acequire, exports) {
-
let EventEmitter = {},
stopPropagation = function () {
this.propagationStopped = !0;
@@ -579,7 +627,6 @@ ace.define(
'ace/anchor',
['require', 'exports', 'module', 'ace/lib/oop', 'ace/lib/event_emitter'],
function (acequire, exports) {
-
let oop = acequire('./lib/oop'),
EventEmitter = acequire('./lib/event_emitter').EventEmitter,
Anchor = (exports.Anchor = function (doc, row, column) {
@@ -696,7 +743,6 @@ ace.define(
'ace/anchor',
],
function (acequire, exports) {
-
let oop = acequire('./lib/oop'),
applyDelta = acequire('./apply_delta').applyDelta,
EventEmitter = acequire('./lib/event_emitter').EventEmitter,
@@ -1064,7 +1110,6 @@ ace.define('ace/lib/lang', ['require', 'exports', 'module'], function (
acequire,
exports
) {
-
(exports.last = function (a) {
return a[a.length - 1];
}),
@@ -1215,7 +1260,6 @@ ace.define(
'ace/lib/lang',
],
function (acequire, exports) {
-
acequire('../range').Range;
let Document = acequire('../document').Document,
lang = acequire('../lib/lang'),
diff --git a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap
index 54da8e383b47a..7985d34b117f5 100644
--- a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap
+++ b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap
@@ -101,7 +101,7 @@ exports[`after fetch initialFilter 1`] = `
id="dashboard.listing.createNewDashboard.newToKibanaDescription"
values={
Object {
- "sampleDataInstallLink":
- ,
+ ,
}
}
/>
@@ -202,7 +202,7 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
id="dashboard.listing.createNewDashboard.newToKibanaDescription"
values={
Object {
- "sampleDataInstallLink":
- ,
+ ,
}
}
/>
@@ -303,7 +303,7 @@ exports[`after fetch renders table rows 1`] = `
id="dashboard.listing.createNewDashboard.newToKibanaDescription"
values={
Object {
- "sampleDataInstallLink":
- ,
+ ,
}
}
/>
@@ -404,7 +404,7 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
id="dashboard.listing.createNewDashboard.newToKibanaDescription"
values={
Object {
- "sampleDataInstallLink":
- ,
+ ,
}
}
/>
@@ -505,7 +505,7 @@ exports[`renders empty page in before initial fetch to avoid flickering 1`] = `
id="dashboard.listing.createNewDashboard.newToKibanaDescription"
values={
Object {
- "sampleDataInstallLink":
- ,
+ ,
}
}
/>
diff --git a/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts b/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts
index d585c5d6f2638..d598f28a0ad12 100644
--- a/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts
+++ b/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts
@@ -104,7 +104,7 @@ export function getMapsAppUrl(
return {
app: 'maps',
- path: `#/map?${mapAppParams.toString()}`,
+ path: `/map#?${mapAppParams.toString()}`,
};
}
diff --git a/src/plugins/embeddable/docs/README.md b/src/plugins/embeddable/docs/README.md
index 1b6c7be13b1d4..ce5e76d54a046 100644
--- a/src/plugins/embeddable/docs/README.md
+++ b/src/plugins/embeddable/docs/README.md
@@ -2,4 +2,5 @@
## Reference
-- [Embeddable containers and inherited input state](./containers_and_inherited_state.md)
+- [Input and output state](./input_and_output_state.md)
+- [Common mistakes with embeddable containers and inherited input state](./containers_and_inherited_state.md)
diff --git a/src/plugins/embeddable/docs/containers_and_inherited_state.md b/src/plugins/embeddable/docs/containers_and_inherited_state.md
index c950bef96002a..35e399f89c131 100644
--- a/src/plugins/embeddable/docs/containers_and_inherited_state.md
+++ b/src/plugins/embeddable/docs/containers_and_inherited_state.md
@@ -1,4 +1,4 @@
-## Embeddable containers and inherited input state
+## Common mistakes with embeddable containers and inherited input state
`updateInput` is typed as `updateInput(input: Partial)`. Notice it's _partial_. This is to support the use case of inherited state when an embeddable is inside a container.
diff --git a/src/plugins/embeddable/docs/input_and_output_state.md b/src/plugins/embeddable/docs/input_and_output_state.md
new file mode 100644
index 0000000000000..810dc72664f96
--- /dev/null
+++ b/src/plugins/embeddable/docs/input_and_output_state.md
@@ -0,0 +1,282 @@
+## Input and output state
+
+### What's the difference?
+
+Input vs Output State
+
+| Input | Output |
+| ----------- | ----------- |
+| Public, on the IEmbeddable interface. `embeddable.updateInput(changedInput)` | Protected inside the Embeddable class. `this.updateOutput(changedOutput)` |
+| Serializable representation of the embeddable | Does not need to be serializable |
+| Can be updated throughout the lifecycle of an Embeddable | Often derived from input state |
+
+Non-real examples to showcase the difference:
+
+| Input | Output |
+| ----------- | ----------- |
+| savedObjectId | savedObjectAttributes |
+| esQueryRequest | esQueryResponse |
+| props | renderComplete |
+
+### Types of input state
+
+#### Inherited input state
+
+The only reason we have different types of input state is to support embeddable containers, and children embeddables _inheriting_ state from the container.
+For example, when the dashboard time range changes, so does
+the time range of all children embeddables. Dashboard passes down time range as _inherited_ input state. From the viewpoint of the child Embeddable,
+time range is just input state. It doesn't care where it gets this data from.
+
+
+For example, imagine a container with this input:
+
+```js
+{
+ gridData: {...},
+ timeRange: 'now-15m to now',
+
+ // Every embeddable container has a panels mapping. It's how the base container class manages common changes like children being
+ // added, removed or edited.
+ panels: {
+ ['1']: {
+ // `type` is used to grab the right embeddable factory. Every PanelState must specify one.
+ type: 'clock',
+
+ // `explicitInput` is combined with `inheritedInput` to create `childInput`, and is use like:
+ // `embeddableFactories.get(type).create(childInput)`.
+ explicitInput: {
+
+ // All explicitInput is required to have an id. This is used as a way for the
+ // embeddable to know where it exists in the panels array if it's living in a container.
+ // Note, this is NOT THE SAVED OBJECT ID! Even though it's sometimes used to store the saved object id.
+ id: '1',
+ }
+ }
+ }
+}
+```
+
+That could result in the following input being passed to a child:
+
+```js
+{
+ timeRange: 'now-15m to now',
+ id: '1',
+}
+```
+
+Notice that `gridData` is not passed down, but `timeRange` is. What ends up as _inherited_ state, that is passed down to a child, is up to the specific
+implementation of a container and
+determined by the abstract function `Container.getInheritedInput()`
+
+#### Overridding inherited input
+
+We wanted to support _overriding_ this inherited state, to support the "Per panel time range" feature. The _inherited_ `timeRange` input can be
+overridden by the _explicit_ `timeRange` input.
+
+Take this example dashboard container input:
+
+```js
+{
+ gridData: {...},
+ timeRange: 'now-15m to now',
+ panels: {
+ ['1']: {
+ type: 'clock',
+ explicitInput: {
+ timeRange: 'now-30m to now',
+ id: '1',
+ }
+ },
+ ['2']: {
+ type: 'clock',
+ explicitInput: {
+ id: '2',
+ }
+ },
+}
+```
+
+The first child embeddable will get passed input state:
+
+```js
+{
+ timeRange: 'now-30m to now',
+ id: '1',
+}
+```
+
+This override wouldn't affect other children, so the second child would receive:
+
+```js
+{
+ timeRange: 'now-15m to now',
+ id: '2',
+}
+```
+
+#### EmbeddableInput.id and some technical debt
+
+Above I said:
+
+> From the viewpoint of the child Embeddable,
+> time range is just input state. It doesn't care where it gets this data from.
+
+and this is mostly true, however, the primary reason EmbeddableInput.id exists is to support the
+case where the custom time range badge action needs to look up a child's explicit input on the
+parent. It does this to determine whether or not to show the badge. The logic is something like:
+
+```ts
+ // If there is no explicit input defined on the parent then this embeddable inherits the
+ // time range from whatever the time range of the parent is.
+ return parent.getInput().panels[embeddable.id].explicitInput.timeRange === undefined;
+```
+
+It doesn't just compare the timeRange input on the parent (`embeddable.parent?.getInput().timeRange` )because even if they happen to match,
+we still want the badge showing to indicate the time range is "locked" on this particular panel.
+
+Note that `parent` can be retrieved from either `embeddabble.parent` or `embeddable.getRoot()`. The
+`getRoot` variety will walk up to find the root parent, even though we have no tested or used
+nested containers, it is theoretically possible.
+
+This EmbeddableInput.id parameter is marked as required on the `EmbeddableInput` interface, even though it's only used
+when an embeddable is inside a parent. There is also no
+typescript safety to ensure the id matches the panel id in the parents json:
+
+```js
+ ['2']: {
+ type: 'clock',
+ explicitInput: {
+ id: '3', // No! Should be 2!
+ }
+ },
+```
+
+It should probably be something that the parent passes down to the child specifically, based on the panel mapping key,
+and renamed to something like `panelKeyInParent`.
+
+Note that this has nothing to do with a saved object id, even though in dashboard app, the saved object happens to be
+used as the dashboard container id. Another reason this should probably not be required for embeddables not
+inside containers.
+
+#### A container can pass down any information to the children
+
+It doesn't have to be part of it's own input. It's possible for a container input like:
+
+
+```js
+{
+ timeRange: 'now-15m to now',
+ panels: {
+ ['1']: {
+ type: 'clock',
+ explicitInput: {
+ timeRange: 'now-30m to now',
+ id: '1',
+ }
+ }
+}
+```
+
+to pass down this input:
+
+```js
+{
+ timeRange: 'now-30m to now',
+ id: '1',
+ zed: 'bar', // <-- Where did this come from??
+}
+```
+
+I don't have a realistic use case for this, just noting it's possible in any containers implementation of `getInheritedInput`. Note this is still considered
+inherited input because it's coming from the container.
+
+#### Explicit input stored on behalf of the container
+
+It's possible for a container to store explicit input state on behalf of an embeddable, without knowing what that state is. For example, a container could
+have input state like:
+
+```js
+{
+ timeRange: 'now-15m to now',
+ panels: {
+ ['1']: {
+ type: 'clock',
+ explicitInput: {
+ display: 'analog',
+ id: '1',
+ }
+ }
+}
+```
+
+And what gets passed to the child is:
+
+```js
+{
+ timeRange: 'now-15m to now',
+ id: '1',
+ display: 'analog'
+}
+```
+
+even if a container has no idea about this `clock` embeddable implementation, nor this `explicitInput.display` field.
+
+There are two ways for this kind of state to end up in `panels[id].explicitInput`.
+
+1. `ClockEmbeddableFactory.getExplicitInput` returns it.
+2. `ClockEmbeddableFactory.getDefaultInput` returns it. (This function is largely unused. We may be able to get rid of it.)
+3. Someone called `embeddable.updateInput({ display: 'analog' })`, when the embeddable is a child in a container.
+
+#### Containers can pass down too much information
+
+Lets say our container state is:
+
+```js
+{
+ timeRange: 'now-15m to now',
+ panels: {
+ ['1']: {
+ type: 'helloWorld',
+ explicitInput: {
+ id: '1',
+ }
+ }
+}
+```
+
+What gets passed to the child is:
+
+```js
+{
+ timeRange: 'now-15m to now',
+ id: '1',
+}
+```
+
+It doesn't matter if the embeddable does not require, nor use, `timeRange`. The container passes down inherited input state to every child.
+This could present problems with trying to figure out which embeddables support
+different types of actions. For example, it'd be great if "Customize time range" action only showed up on embeddables that actually did something
+with the `timeRange`. You can't check at runtime whether `input.timeRange === undefined` to do so though, because it will be passed in by the container
+regardless.
+
+
+#### Tech debt warnings
+
+`EmbeddableFactory.getExplicitInput` was intended as a way for an embeddable to retrieve input state it needs, that will not
+be provided by a container. However, an embeddable won't know where it will be rendered, so how will the factory know which
+required data to ask from the user and which will be inherited from the container? I believe `getDefaultInput` was meant to solve this.
+`getDefaultInput` would provide default values, only if the container didn't supply them through inheritance. Explicit input would
+always provide these values, and would always be stored in a containers `panel[id].explicitInput`, even if the container _did_ provide
+them.
+
+There are no real life examples showcasing this, it may not even be really needed by current use cases. Containers were built as an abstraction, with
+the thinking being that it would support any type of rendering of child embeddables - whether in a "snap to grid" style like dashboard,
+or in a free form layout like canvas.
+
+The only real implementation of a container in production code at the time this is written is Dashboard however, with no plans to migrate
+Canvas over to use it (this was the original impetus for an abstraction). The container code is quite complicated with child management,
+so it makes creating a new container very easy, as you can see in the developer examples of containers. But, it's possible this layer was
+ an over abstraction without a real prod use case (I can say that because I wrote it, I'm only insulting myself!) :).
+
+Be sure to read [Common mistakes with embeddable containers and inherited input state](./containers_and_inherited_state.md) next!
\ No newline at end of file
diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx
index 18d064601eaed..51213288e47a7 100644
--- a/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx
+++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx
@@ -20,7 +20,6 @@
import React from 'react';
import { wait } from '@testing-library/dom';
import { cleanup, render } from '@testing-library/react/pure';
-import '@testing-library/jest-dom/extend-expect';
import {
HelloWorldEmbeddable,
HelloWorldEmbeddableFactoryDefinition,
diff --git a/src/plugins/es_ui_shared/public/console_lang/ace/modes/x_json/worker/x_json.ace.worker.js b/src/plugins/es_ui_shared/public/console_lang/ace/modes/x_json/worker/x_json.ace.worker.js
index 69a8cc86f1f73..53cdb5885c730 100644
--- a/src/plugins/es_ui_shared/public/console_lang/ace/modes/x_json/worker/x_json.ace.worker.js
+++ b/src/plugins/es_ui_shared/public/console_lang/ace/modes/x_json/worker/x_json.ace.worker.js
@@ -1,3 +1,55 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* @notice
+ *
+ * This product includes code that is based on Ace editor, which was available
+ * under a "BSD" license.
+ *
+ * Distributed under the BSD license:
+ *
+ * Copyright (c) 2010, Ajax.org B.V.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of Ajax.org B.V. nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
/* eslint-disable */
/*
This file is loaded up as a blob by Brace to hand to Ace to load as Jsonp
diff --git a/src/plugins/es_ui_shared/public/forms/form_wizard/README.md b/src/plugins/es_ui_shared/public/forms/form_wizard/README.md
new file mode 100644
index 0000000000000..56c792b89049b
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/forms/form_wizard/README.md
@@ -0,0 +1,45 @@
+# FormWizard
+
+The `` and `` components lets us declare form wizard in a declarative way. It works hand in hand with the `MultiContent` explained above to make building form wizards a breeze. 😊
+
+It takes care of enabling, disabling the `` steps as well as the "Back" and "Next" button.
+
+Let's see it through an example
+
+```js
+const MyForm = () => {
+ return (
+
+ defaultValue={wizardDefaultValue} // The MultiContent default value as explained above
+ onSave={onSaveTemplate} // A handler that will receive the multi-content data
+ isEditing={isEditing} // A boolean that will indicate if all steps are already "completed" and thus valid or if we need to complete them in order
+ isSaving={isSaving} // A boolean to show a "Saving..." text on the button on the last step
+ apiError={apiError} // Any API error to display on top of wizard
+ texts={i18nTexts} // i18n translations for the nav button.
+ >
+
+
+ Here you can put anything... but you probably want to put a Container from the
+ MultiContent example above.
+
+
+
+
+
+ Here you can put anything... but you probably want to put a Container from the
+ MultiContent example above.
+
+
+
+
+
+ Here you can put anything... but you probably want to put a Container from the
+ MultiContent example above.
+
+
+
+ );
+};
+```
+
+That's all we need to build a multi-step form wizard, making sure the data is cached when switching steps.
\ No newline at end of file
diff --git a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard.tsx b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard.tsx
new file mode 100644
index 0000000000000..cdb332e9e9130
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard.tsx
@@ -0,0 +1,139 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { EuiStepsHorizontal, EuiSpacer } from '@elastic/eui';
+
+import {
+ FormWizardProvider,
+ FormWizardConsumer,
+ Props as ProviderProps,
+} from './form_wizard_context';
+import { FormWizardNav, NavTexts } from './form_wizard_nav';
+
+interface Props extends ProviderProps {
+ isSaving?: boolean;
+ apiError: JSX.Element | null;
+ texts?: Partial;
+}
+
+export function FormWizard({
+ texts,
+ defaultActiveStep,
+ defaultValue,
+ apiError,
+ isEditing,
+ isSaving,
+ onSave,
+ onChange,
+ children,
+}: Props) {
+ return (
+
+ defaultValue={defaultValue}
+ isEditing={isEditing}
+ onSave={onSave}
+ onChange={onChange}
+ defaultActiveStep={defaultActiveStep}
+ >
+
+ {({ activeStepIndex, lastStep, steps, isCurrentStepValid, navigateToStep }) => {
+ const stepsRequiredArray = Object.values(steps).map(
+ (step) => Boolean(step.isRequired) && step.isComplete === false
+ );
+
+ const getIsStepDisabled = (stepIndex: number) => {
+ // Disable all steps when the current step is invalid
+ if (stepIndex !== activeStepIndex && isCurrentStepValid === false) {
+ return true;
+ }
+
+ let isDisabled = false;
+
+ if (stepIndex > activeStepIndex + 1) {
+ /**
+ * Rule explained:
+ * - all the previous steps are always enabled (we can go back anytime)
+ * - the next step is also always enabled (it acts as the "Next" button)
+ * - for the rest, the step is disabled if any of the previous step (_greater_ than the current
+ * active step), is marked as isRequired **AND** has not been completed.
+ */
+ isDisabled = stepsRequiredArray.reduce((acc, isRequired, i) => {
+ if (acc === true || i <= activeStepIndex || i >= stepIndex) {
+ return acc;
+ }
+ return Boolean(isRequired);
+ }, false);
+ }
+
+ return isDisabled;
+ };
+
+ const euiSteps = Object.values(steps).map(({ index, label }) => {
+ return {
+ title: label,
+ isComplete: activeStepIndex > index,
+ isSelected: activeStepIndex === index,
+ disabled: getIsStepDisabled(index),
+ onClick: () => navigateToStep(index),
+ };
+ });
+
+ const onBack = () => {
+ const prevStep = activeStepIndex - 1;
+ navigateToStep(prevStep);
+ };
+
+ const onNext = () => {
+ const nextStep = activeStepIndex + 1;
+ navigateToStep(nextStep);
+ };
+
+ return (
+ <>
+ {/* Horizontal Steps indicator */}
+
+
+
+
+ {/* Any possible API error when saving/updating */}
+ {apiError}
+
+ {/* Active step content */}
+ {children}
+
+
+
+ {/* Button navigation */}
+
+ >
+ );
+ }}
+
+
+ );
+}
diff --git a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_context.tsx b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_context.tsx
new file mode 100644
index 0000000000000..5667220881df2
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_context.tsx
@@ -0,0 +1,173 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useState, createContext, useContext, useCallback } from 'react';
+
+import { WithMultiContent, useMultiContentContext, HookProps } from '../multi_content';
+
+export interface Props {
+ onSave: (data: T) => void | Promise;
+ children: JSX.Element | JSX.Element[];
+ isEditing?: boolean;
+ defaultActiveStep?: number;
+ defaultValue?: HookProps['defaultValue'];
+ onChange?: HookProps['onChange'];
+}
+
+interface State {
+ activeStepIndex: number;
+ steps: Steps;
+}
+
+export interface Step {
+ id: string;
+ index: number;
+ label: string;
+ isRequired: boolean;
+ isComplete: boolean;
+}
+
+export interface Steps {
+ [stepId: string]: Step;
+}
+
+export interface Context extends State {
+ activeStepId: Id;
+ lastStep: number;
+ isCurrentStepValid: boolean | undefined;
+ navigateToStep: (stepId: number | Id) => void;
+ addStep: (id: Id, label: string, isRequired?: boolean) => void;
+}
+
+const formWizardContext = createContext({} as Context);
+
+export const FormWizardProvider = WithMultiContent>(function FormWizardProvider<
+ T extends object = { [key: string]: any }
+>({ children, defaultActiveStep = 0, isEditing, onSave }: Props) {
+ const { getData, validate, validation } = useMultiContentContext();
+
+ const [state, setState] = useState({
+ activeStepIndex: defaultActiveStep,
+ steps: {},
+ });
+
+ const activeStepId = state.steps[state.activeStepIndex]?.id;
+ const lastStep = Object.keys(state.steps).length - 1;
+ const isCurrentStepValid = validation.contents[activeStepId as keyof T];
+
+ const addStep = useCallback(
+ (id: string, label: string, isRequired = false) => {
+ setState((prev) => {
+ const index = Object.keys(prev.steps).length;
+
+ return {
+ ...prev,
+ steps: {
+ ...prev.steps,
+ [index]: { id, index, label, isRequired, isComplete: isEditing ?? false },
+ },
+ };
+ });
+ },
+ [isEditing]
+ );
+
+ /**
+ * Get the step index from a step id.
+ */
+ const getStepIndex = useCallback(
+ (stepId: number | string) => {
+ if (typeof stepId === 'number') {
+ return stepId;
+ }
+
+ // We provided a string stepId, we need to find the corresponding index
+ const targetStep: Step | undefined = Object.values(state.steps).find(
+ (_step) => _step.id === stepId
+ );
+ if (!targetStep) {
+ throw new Error(`Can't navigate to step "${stepId}" as there are no step with that ID.`);
+ }
+ return targetStep.index;
+ },
+ [state.steps]
+ );
+
+ const navigateToStep = useCallback(
+ async (stepId: number | string) => {
+ // Before navigating away we validate the active content in the DOM
+ const isValid = await validate();
+
+ // If step is not valid do not go any further
+ if (!isValid) {
+ return;
+ }
+
+ const nextStepIndex = getStepIndex(stepId);
+
+ if (nextStepIndex > lastStep) {
+ // We are on the last step, save the data and don't go any further
+ onSave(getData() as T);
+ return;
+ }
+
+ // Update the active step
+ setState((prev) => {
+ const currentStep = prev.steps[prev.activeStepIndex];
+
+ const nextState = {
+ ...prev,
+ activeStepIndex: nextStepIndex,
+ };
+
+ if (nextStepIndex > prev.activeStepIndex && !currentStep.isComplete) {
+ // Mark the current step as completed
+ nextState.steps[prev.activeStepIndex] = {
+ ...currentStep,
+ isComplete: true,
+ };
+ }
+
+ return nextState;
+ });
+ },
+ [getStepIndex, validate, onSave, getData]
+ );
+
+ const value: Context = {
+ ...state,
+ activeStepId,
+ lastStep,
+ isCurrentStepValid,
+ addStep,
+ navigateToStep,
+ };
+
+ return {children};
+});
+
+export const FormWizardConsumer = formWizardContext.Consumer;
+
+export function useFormWizardContext() {
+ const ctx = useContext(formWizardContext);
+ if (ctx === undefined) {
+ throw new Error('useFormWizardContext() must be called within a ');
+ }
+ return ctx as Context;
+}
diff --git a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_nav.tsx b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_nav.tsx
new file mode 100644
index 0000000000000..3e0e9cf897b5d
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_nav.tsx
@@ -0,0 +1,105 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+interface Props {
+ activeStepIndex: number;
+ lastStep: number;
+ onBack: () => void;
+ onNext: () => void;
+ isSaving?: boolean;
+ isStepValid?: boolean;
+ texts?: Partial;
+}
+
+export interface NavTexts {
+ back: string | JSX.Element;
+ next: string | JSX.Element;
+ save: string | JSX.Element;
+ saving: string | JSX.Element;
+}
+
+const DEFAULT_TEXTS = {
+ back: i18n.translate('esUi.formWizard.backButtonLabel', { defaultMessage: 'Back' }),
+ next: i18n.translate('esUi.formWizard.nextButtonLabel', { defaultMessage: 'Next' }),
+ save: i18n.translate('esUi.formWizard.saveButtonLabel', { defaultMessage: 'Save' }),
+ saving: i18n.translate('esUi.formWizard.savingButtonLabel', { defaultMessage: 'Saving...' }),
+};
+
+export const FormWizardNav = ({
+ activeStepIndex,
+ lastStep,
+ isStepValid,
+ isSaving,
+ onBack,
+ onNext,
+ texts,
+}: Props) => {
+ const isLastStep = activeStepIndex === lastStep;
+ const labels = {
+ ...DEFAULT_TEXTS,
+ ...texts,
+ };
+
+ const nextButtonLabel = isLastStep
+ ? Boolean(isSaving)
+ ? labels.saving
+ : labels.save
+ : labels.next;
+
+ return (
+
+
+
+ {/* Back button */}
+ {activeStepIndex > 0 ? (
+
+
+ {labels.back}
+
+
+ ) : null}
+
+ {/* Next button */}
+
+
+ {nextButtonLabel}
+
+
+
+
+
+ );
+};
diff --git a/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_step.tsx b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_step.tsx
new file mode 100644
index 0000000000000..c073c188a6ad6
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/forms/form_wizard/form_wizard_step.tsx
@@ -0,0 +1,39 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useEffect } from 'react';
+
+import { useFormWizardContext } from './form_wizard_context';
+
+interface Props {
+ id: string;
+ label: string;
+ children: JSX.Element;
+ isRequired?: boolean;
+}
+
+export const FormWizardStep = ({ id, label, isRequired, children }: Props) => {
+ const { activeStepId, addStep } = useFormWizardContext();
+
+ useEffect(() => {
+ addStep(id, label, isRequired);
+ }, [id, label, isRequired, addStep]);
+
+ return activeStepId === id ? children : null;
+};
diff --git a/src/plugins/es_ui_shared/public/forms/form_wizard/index.ts b/src/plugins/es_ui_shared/public/forms/form_wizard/index.ts
new file mode 100644
index 0000000000000..b1cb11735a110
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/forms/form_wizard/index.ts
@@ -0,0 +1,32 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { FormWizard } from './form_wizard';
+
+export { FormWizardStep } from './form_wizard_step';
+
+export {
+ FormWizardProvider,
+ FormWizardConsumer,
+ useFormWizardContext,
+ Step,
+ Steps,
+} from './form_wizard_context';
+
+export { FormWizardNav, NavTexts } from './form_wizard_nav';
diff --git a/src/plugins/es_ui_shared/public/forms/index.ts b/src/plugins/es_ui_shared/public/forms/index.ts
new file mode 100644
index 0000000000000..96140c9b46185
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/forms/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export * from './form_wizard';
+
+export * from './multi_content';
diff --git a/src/plugins/es_ui_shared/public/forms/multi_content/README.md b/src/plugins/es_ui_shared/public/forms/multi_content/README.md
new file mode 100644
index 0000000000000..08c37c20b5bf6
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/forms/multi_content/README.md
@@ -0,0 +1,157 @@
+# MultiContent
+
+## The problem
+
+Building resource creations/edition flows in the UI, that have multiple contents that need to be merged together at the end of the flow and at the same time keeping a reference of each content state, is not trivial. Indeed, when we switch tab or we go to the next step, the old step data needs to be saved somewhere.
+
+The first thing that comes to mind is: "Ok, I'll lift the state up" and make each step "content" a controlled component (when its value changes, it sends it to the global state and then it receives it back as prop). This works well up to a certain point. What happens if the internal state that the step content works with, is not the same as the outputted state?
+
+Something like this:
+
+```js
+// StepOne internal state, flat map of fields
+const internalState: {
+ fields: {
+ ate426jn: { name: 'hello', value: 'world', parent: 'rwtsdg3' },
+ rwtsdg3: { name: 'myObject', type: 'object' },
+ }
+}
+
+// Outputed data
+
+const output = {
+ stepOne: {
+ myObject: {
+ hello: 'world'
+ }
+ }
+}
+```
+
+We need some sort of serializer to go from the internal state to the output object. If we lift the state up this means that the global state needs to be aware of the intrinsic of the content, leaking implementation details.
+This also means that the content **can't be a reusable component** as it depends on an external state to do part of its work (think: the mappings editor).
+
+This is where `MultiContent` comes into play. It lets us declare `content` objects and automatically saves a snapshot of their content when the component unmounts (which occurs when switching a tab for example). If we navigate back to the tab, the tab content gets its `defaultValue` from that cache state.
+
+Let see it through a concrete example
+
+```js
+// my_comp_wrapper.tsx
+
+// Always good to have an interface for our contents
+interface MyMultiContent {
+ contentOne: { myField: string };
+ contentTwo: { anotherField: string };
+ contentThree: { yetAnotherField: boolean };
+}
+
+// Each content data will be a slice of the multi-content defaultValue
+const defaultValue: MyMultiContent = {
+ contentOne: {
+ myField: 'value',
+ },
+ contentTwo: {
+ anotherField: 'value',
+ },
+ contentThree: {
+ yetAnotherField: true,
+ },
+};
+```
+
+```js
+// my_comp.tsx
+
+/**
+ * We wrap our component with the HOC that will provide the and let us use the "useMultiContentContext()" hook
+ *
+ * MyComponent connects to the multi-content context and renders each step
+ * content without worrying about their internal state.
+ */
+const MyComponent = WithMultiContent(() => {
+ const { validation, getData, validate } = useMultiContentContext();
+
+ const totalSteps = 3;
+ const [currentStep, setCurrentStep] = useState(0);
+
+ const renderContent = () => {
+ switch (currentStep) {
+ case 0:
+ return ;
+ case 1:
+ return ;
+ case 2:
+ return ;
+ }
+ };
+
+ const onNext = () => {
+ // Validate the multi content
+ const isValid = await validate();
+
+ if (!isValid) {
+ return;
+ }
+
+ if (currentStep < totalSteps - 1) {
+ // Navigate to the next content
+ setCurrentStep((curentStep += 1));
+ } else {
+ // On last step we need to save so we read the multi-content data
+ console.log('About to save:', getData());
+ }
+ };
+
+ return (
+ <>
+ {renderContent()}
+
+ {/* Each content validity is accessible from the `validation.contents` object */}
+
+ Next
+
+ >
+ );
+});
+```
+
+```js
+// content_one_container.tsx
+
+// From the good old days of Redux, it is a good practice to separate the connection to the multi-content
+// from the UI that is rendered.
+const ContentOneContainer = () => {
+
+ // Declare a new content and get its default Value + a handler to update the content in the multi-content
+ // This will update the "contentOne" slice of the multi-content.
+ const { defaultValue, updateContent } = useContent('contentOne');
+
+ return
+};
+```
+
+```js
+// content_one.tsx
+
+const ContentOne = ({ defaultValue, onChange }) => {
+ // Use the defaultValue as a starting point for the internal state
+ const [internalStateValue, setInternalStateValue] = useState(defaultValue.myField);
+
+ useEffect(() => {
+ // Update the multi content state for this content
+ onChange({
+ isValid: true, // because in this example it is always valid
+ validate: async () => true,
+ getData: () => ({
+ myField: internalStateValue,
+ }),
+ });
+ }, [internalStateValue]);
+
+ return (
+ setInternalStateValue(e.target.value)} />
+ );
+}
+```
+
+And just like that, `` is a reusable component that gets a `defaultValue` object and an `onChange` handler to communicate any internal state changes. He is responsible to provide a `getData()` handler as part of the `onChange` that will do any necessary serialization and sanitization, and the outside world does not need to know about it.
\ No newline at end of file
diff --git a/src/plugins/es_ui_shared/public/forms/multi_content/index.ts b/src/plugins/es_ui_shared/public/forms/multi_content/index.ts
new file mode 100644
index 0000000000000..a7df0e386d173
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/forms/multi_content/index.ts
@@ -0,0 +1,29 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export {
+ MultiContentProvider,
+ MultiContentConsumer,
+ useMultiContentContext,
+ useContent,
+} from './multi_content_context';
+
+export { useMultiContent, HookProps, Content, MultiContent } from './use_multi_content';
+
+export { WithMultiContent } from './with_multi_content';
diff --git a/src/plugins/es_ui_shared/public/forms/multi_content/multi_content_context.tsx b/src/plugins/es_ui_shared/public/forms/multi_content/multi_content_context.tsx
new file mode 100644
index 0000000000000..5fbe3d2bbbdd4
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/forms/multi_content/multi_content_context.tsx
@@ -0,0 +1,79 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useEffect, useCallback, createContext, useContext } from 'react';
+
+import { useMultiContent, HookProps, Content, MultiContent } from './use_multi_content';
+
+const multiContentContext = createContext>({} as MultiContent);
+
+interface Props extends HookProps {
+ children: JSX.Element | JSX.Element[];
+}
+
+export function MultiContentProvider({
+ defaultValue,
+ onChange,
+ children,
+}: Props) {
+ const multiContent = useMultiContent({ defaultValue, onChange });
+
+ return (
+ {children}
+ );
+}
+
+export const MultiContentConsumer = multiContentContext.Consumer;
+
+export function useMultiContentContext() {
+ const ctx = useContext(multiContentContext);
+ if (Object.keys(ctx).length === 0) {
+ throw new Error('useMultiContentContext must be used within a ');
+ }
+ return ctx as MultiContent;
+}
+
+/**
+ * Hook to declare a new content and get its defaultValue and a handler to update its content
+ *
+ * @param contentId The content id to be added to the "contents" map
+ */
+export function useContent(contentId: keyof T) {
+ const { updateContentAt, saveSnapshotAndRemoveContent, getData } = useMultiContentContext();
+
+ const updateContent = useCallback(
+ (content: Content) => {
+ updateContentAt(contentId, content);
+ },
+ [contentId, updateContentAt]
+ );
+
+ useEffect(() => {
+ return () => {
+ // On unmount: save a snapshot of the data and remove content from our contents map
+ saveSnapshotAndRemoveContent(contentId);
+ };
+ }, [contentId, saveSnapshotAndRemoveContent]);
+
+ return {
+ defaultValue: getData()[contentId]!,
+ updateContent,
+ getData,
+ };
+}
diff --git a/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts b/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts
new file mode 100644
index 0000000000000..0a2c7bb651959
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/forms/multi_content/use_multi_content.ts
@@ -0,0 +1,215 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useState, useCallback, useRef } from 'react';
+
+export interface Content {
+ isValid: boolean | undefined;
+ validate(): Promise;
+ getData(): T;
+}
+
+type Contents = {
+ [K in keyof T]: Content;
+};
+
+interface Validation {
+ isValid: boolean | undefined;
+ contents: {
+ [K in keyof T]: boolean | undefined;
+ };
+}
+
+export interface HookProps {
+ defaultValue?: T;
+ onChange?: (output: Content) => void;
+}
+
+export interface MultiContent {
+ updateContentAt: (id: keyof T, content: Content) => void;
+ saveSnapshotAndRemoveContent: (id: keyof T) => void;
+ getData: () => T;
+ validate: () => Promise;
+ validation: Validation;
+}
+
+export function useMultiContent({
+ defaultValue,
+ onChange,
+}: HookProps): MultiContent {
+ /**
+ * Each content validity is kept in this state. When updating a content with "updateContentAt()", we
+ * update the state validity and trigger a re-render.
+ */
+ const [validation, setValidation] = useState>({
+ isValid: true,
+ contents: {},
+ } as Validation);
+
+ /**
+ * The updated data where a content current data is merged when it unmounts
+ */
+ const [stateData, setStateData] = useState(defaultValue ?? ({} as T));
+
+ /**
+ * A map object of all the active content(s) present in the DOM. In a multi step
+ * form wizard, there is only 1 content at the time in the DOM, but in long vertical
+ * flow content, multiple content could be present.
+ * When a content unmounts it will remove itself from this map.
+ */
+ const contents = useRef>({} as Contents);
+
+ const updateContentDataAt = useCallback(function (updatedData: { [key in keyof T]?: any }) {
+ setStateData((prev) => ({
+ ...prev,
+ ...updatedData,
+ }));
+ }, []);
+
+ /**
+ * Read the multi content data.
+ */
+ const getData = useCallback((): T => {
+ /**
+ * If there is one or more active content(s) in the DOM, and it is valid,
+ * we read its data and merge it into our stateData before returning it.
+ */
+ const activeContentData: Partial = {};
+
+ for (const [id, _content] of Object.entries(contents.current)) {
+ if (validation.contents[id as keyof T]) {
+ const contentData = (_content as Content).getData();
+
+ // Replace the getData() handler with the cached value
+ (_content as Content).getData = () => contentData;
+
+ activeContentData[id as keyof T] = contentData;
+ }
+ }
+
+ return {
+ ...stateData,
+ ...activeContentData,
+ };
+ }, [stateData, validation]);
+
+ const updateContentValidity = useCallback(
+ (updatedData: { [key in keyof T]?: boolean | undefined }): boolean | undefined => {
+ let allContentValidity: boolean | undefined;
+
+ setValidation((prev) => {
+ if (
+ Object.entries(updatedData).every(
+ ([contentId, isValid]) => prev.contents[contentId as keyof T] === isValid
+ )
+ ) {
+ // No change in validation, nothing to update
+ allContentValidity = prev.isValid;
+ return prev;
+ }
+
+ const nextContentsValidityState = {
+ ...prev.contents,
+ ...updatedData,
+ };
+
+ allContentValidity = Object.values(nextContentsValidityState).some(
+ (_isValid) => _isValid === undefined
+ )
+ ? undefined
+ : Object.values(nextContentsValidityState).every(Boolean);
+
+ return {
+ isValid: allContentValidity,
+ contents: nextContentsValidityState,
+ };
+ });
+
+ return allContentValidity;
+ },
+ []
+ );
+
+ /**
+ * Validate the multi-content active content(s) in the DOM
+ */
+ const validate = useCallback(async () => {
+ const updatedValidation = {} as { [key in keyof T]?: boolean | undefined };
+
+ for (const [id, _content] of Object.entries(contents.current)) {
+ const isValid = await (_content as Content).validate();
+ (_content as Content).validate = async () => isValid;
+ updatedValidation[id as keyof T] = isValid;
+ }
+
+ return Boolean(updateContentValidity(updatedValidation));
+ }, [updateContentValidity]);
+
+ /**
+ * Update a content. It replaces the content in our "contents" map and update
+ * the state validation object.
+ */
+ const updateContentAt = useCallback(
+ function (contentId: keyof T, content: Content) {
+ contents.current[contentId] = content;
+
+ const updatedValidity = { [contentId]: content.isValid } as {
+ [key in keyof T]: boolean | undefined;
+ };
+ const isValid = updateContentValidity(updatedValidity);
+
+ if (onChange !== undefined) {
+ onChange({
+ isValid,
+ validate,
+ getData,
+ });
+ }
+ },
+ [updateContentValidity, onChange]
+ );
+
+ /**
+ * When a content unmounts we want to save its current data state so we will be able
+ * to provide it as "defaultValue" the next time the component is mounted.
+ */
+ const saveSnapshotAndRemoveContent = useCallback(
+ function (contentId: keyof T) {
+ if (contents.current[contentId]) {
+ // Merge the data in our stateData
+ const updatedData = {
+ [contentId]: contents.current[contentId].getData(),
+ } as { [key in keyof T]?: any };
+ updateContentDataAt(updatedData);
+
+ // Remove the content from our map
+ delete contents.current[contentId];
+ }
+ },
+ [updateContentDataAt]
+ );
+
+ return {
+ getData,
+ validate,
+ validation,
+ updateContentAt,
+ saveSnapshotAndRemoveContent,
+ };
+}
diff --git a/src/plugins/es_ui_shared/public/forms/multi_content/with_multi_content.tsx b/src/plugins/es_ui_shared/public/forms/multi_content/with_multi_content.tsx
new file mode 100644
index 0000000000000..e69ce4c6fa145
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/forms/multi_content/with_multi_content.tsx
@@ -0,0 +1,40 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+
+import { MultiContentProvider } from './multi_content_context';
+import { HookProps } from './use_multi_content';
+
+/**
+ * HOC to wrap a component with the MultiContentProvider
+ *
+ * @param Component The component to wrap with the MultiContentProvider
+ */
+export function WithMultiContent<
+ P extends object = { [key: string]: any } // The Props for the wrapped component
+>(Component: React.FunctionComponent
>) {
+ return function (props: P & HookProps) {
+ const { defaultValue, onChange, ...rest } = props;
+ return (
+ defaultValue={defaultValue} onChange={onChange}>
+
+
+ );
+ };
+}
diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts
index 4ab791289dd88..28baa3d8372f0 100644
--- a/src/plugins/es_ui_shared/public/index.ts
+++ b/src/plugins/es_ui_shared/public/index.ts
@@ -17,6 +17,12 @@
* under the License.
*/
+/**
+ * Create a namespace for Forms
+ * In the future, each top level folder should be exported like that to avoid naming collision
+ */
+import * as Forms from './forms';
+
export { JsonEditor, OnJsonEditorUpdateHandler } from './components/json_editor';
export { SectionLoading } from './components/section_loading';
@@ -63,6 +69,8 @@ export {
useAuthorizationContext,
} from './authorization';
+export { Forms };
+
/** dummy plugin, we just want esUiShared to have its own bundle */
export function plugin() {
return new (class EsUiSharedPlugin {
diff --git a/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap
index 1c67f332a12ab..4e66fd9e14c81 100644
--- a/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap
+++ b/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap
@@ -125,7 +125,6 @@ exports[`should render a Welcome screen with the telemetry disclaimer 1`] = `
values={Object {}}
/>
@@ -141,7 +140,7 @@ exports[`should render a Welcome screen with the telemetry disclaimer 1`] = `
values={Object {}}
/>
@@ -240,7 +238,7 @@ exports[`should render a Welcome screen with the telemetry disclaimer when optIn
values={Object {}}
/>
@@ -339,7 +336,7 @@ exports[`should render a Welcome screen with the telemetry disclaimer when optIn
values={Object {}}
/>
-
@@ -169,7 +169,7 @@ exports[`statusCheckState checking status 1`] = `
custom btn label
-
+
@@ -257,7 +257,7 @@ exports[`statusCheckState failed status check - error 1`] = `
},
Object {
"children":
-
@@ -276,7 +276,7 @@ exports[`statusCheckState failed status check - error 1`] = `
custom btn label
-
+
@@ -368,7 +368,7 @@ exports[`statusCheckState failed status check - no data 1`] = `
},
Object {
"children":
-
@@ -387,7 +387,7 @@ exports[`statusCheckState failed status check - no data 1`] = `
custom btn label
-
+
@@ -479,7 +479,7 @@ exports[`statusCheckState initial state - no check has been attempted 1`] = `
},
Object {
"children":
-
@@ -498,7 +498,7 @@ exports[`statusCheckState initial state - no check has been attempted 1`] = `
custom btn label
-
+
@@ -586,7 +586,7 @@ exports[`statusCheckState successful status check 1`] = `
},
Object {
"children":
-
@@ -605,7 +605,7 @@ exports[`statusCheckState successful status check 1`] = `
custom btn label
-
+
diff --git a/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap b/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap
index 595ad14bb0fe3..1e7b3d5c6284c 100644
--- a/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap
+++ b/src/plugins/home/public/application/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap
@@ -120,7 +120,7 @@ exports[`bulkCreate should display error message when bulkCreate request fails 1
Array [
Object {
"children":
-
@@ -142,7 +142,7 @@ exports[`bulkCreate should display error message when bulkCreate request fails 1
Load Kibana objects
-
+
@@ -421,7 +421,7 @@ exports[`bulkCreate should display success message when bulkCreate is successful
Array [
Object {
"children":
-
@@ -443,7 +443,7 @@ exports[`bulkCreate should display success message when bulkCreate is successful
Load Kibana objects
-
+
@@ -627,7 +627,7 @@ exports[`renders 1`] = `
Array [
Object {
"children":
-
@@ -649,7 +649,7 @@ exports[`renders 1`] = `
Load Kibana objects
-
+
diff --git a/src/plugins/home/public/application/components/welcome.test.tsx b/src/plugins/home/public/application/components/welcome.test.tsx
index 1332e03ffdc81..701fab3af7539 100644
--- a/src/plugins/home/public/application/components/welcome.test.tsx
+++ b/src/plugins/home/public/application/components/welcome.test.tsx
@@ -30,56 +30,39 @@ jest.mock('../kibana_services', () => ({
}));
test('should render a Welcome screen with the telemetry disclaimer', () => {
- const telemetry = telemetryPluginMock.createSetupContract();
- const component = shallow(
- // @ts-ignore
- {}} telemetry={telemetry} />
- );
+ const telemetry = telemetryPluginMock.createStartContract();
+ const component = shallow( {}} telemetry={telemetry} />);
expect(component).toMatchSnapshot();
});
test('should render a Welcome screen with the telemetry disclaimer when optIn is true', () => {
- const telemetry = telemetryPluginMock.createSetupContract();
+ const telemetry = telemetryPluginMock.createStartContract();
telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true);
- const component = shallow(
- // @ts-ignore
- {}} telemetry={telemetry} />
- );
+ const component = shallow( {}} telemetry={telemetry} />);
expect(component).toMatchSnapshot();
});
test('should render a Welcome screen with the telemetry disclaimer when optIn is false', () => {
- const telemetry = telemetryPluginMock.createSetupContract();
+ const telemetry = telemetryPluginMock.createStartContract();
telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false);
- const component = shallow(
- // @ts-ignore
- {}} telemetry={telemetry} />
- );
+ const component = shallow( {}} telemetry={telemetry} />);
expect(component).toMatchSnapshot();
});
test('should render a Welcome screen with no telemetry disclaimer', () => {
- // @ts-ignore
- const component = shallow(
- // @ts-ignore
- {}} telemetry={null} />
- );
+ const component = shallow( {}} />);
expect(component).toMatchSnapshot();
});
test('fires opt-in seen when mounted', () => {
- const telemetry = telemetryPluginMock.createSetupContract();
+ const telemetry = telemetryPluginMock.createStartContract();
const mockSetOptedInNoticeSeen = jest.fn();
- // @ts-ignore
telemetry.telemetryNotifications.setOptedInNoticeSeen = mockSetOptedInNoticeSeen;
- shallow(
- // @ts-ignore
- {}} telemetry={telemetry} />
- );
+ shallow( {}} telemetry={telemetry} />);
expect(mockSetOptedInNoticeSeen).toHaveBeenCalled();
});
diff --git a/src/plugins/home/public/application/components/welcome.tsx b/src/plugins/home/public/application/components/welcome.tsx
index d4dcaca317806..cacb507009c70 100644
--- a/src/plugins/home/public/application/components/welcome.tsx
+++ b/src/plugins/home/public/application/components/welcome.tsx
@@ -38,7 +38,6 @@ import { METRIC_TYPE } from '@kbn/analytics';
import { FormattedMessage } from '@kbn/i18n/react';
import { getServices } from '../kibana_services';
import { TelemetryPluginStart } from '../../../../telemetry/public';
-import { PRIVACY_STATEMENT_URL } from '../../../../telemetry/common/constants';
import { SampleDataCard } from './sample_data';
interface Props {
@@ -101,7 +100,7 @@ export class Welcome extends React.Component {
id="home.dataManagementDisableCollection"
defaultMessage=" To stop collection, "
/>
-
+ {
id="home.dataManagementEnableCollection"
defaultMessage=" To start collection, "
/>
-
+ {
id="home.dataManagementDisclaimerPrivacy"
defaultMessage="To learn about how usage data helps us manage and improve our products and services, see our "
/>
-
+
- ,
- "learnHowLink": ,
+ "learnHowLink":
- ,
+ ,
"needToIndex":
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/__snapshots__/time_field.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/__snapshots__/time_field.test.tsx.snap
index 7d056433f55df..886a4ccad39cc 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/__snapshots__/time_field.test.tsx.snap
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/__snapshots__/time_field.test.tsx.snap
@@ -27,7 +27,7 @@ exports[`TimeField should render a loading state 1`] = `
}
label={
-
-
+
}
labelType="label"
>
@@ -98,7 +98,7 @@ exports[`TimeField should render a selected time field 1`] = `
}
label={
-
-
@@ -126,9 +126,9 @@ exports[`TimeField should render a selected time field 1`] = `
id="indexPatternManagement.createIndexPattern.stepTime.refreshButton"
values={Object {}}
/>
-
+
-
+
}
labelType="label"
>
@@ -179,7 +179,7 @@ exports[`TimeField should render normally 1`] = `