diff --git a/NOTICE.txt b/NOTICE.txt
index 946b328b8766c1..94312d46c35ecb 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 00000000000000..d81049dfbd340f
--- /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 c50e8924c9dd40..6a6bacf1eef40b 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 00000000000000..4ea67cf895a27e
--- /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 00000000000000..54db6bce5f1614
--- /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 00000000000000..d9d77809570abd
--- /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 6667779c1c7aea..da348a2282b1a0 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 fa86da18393f59..00000000000000
--- 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 2dd832813afb82..b12983836d9e5a 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 bf2922c62c15f0..f7dffee2e125ca 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 00000000000000..e8b2a0fc2cbaac
--- /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 00000000000000..a95c8da64fdb07
--- /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 53239da516b25e..bc99c1217f72bd 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 147a72016b2351..0f1bbbe7176e50 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. |
@@ -157,6 +158,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry |
| [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) | |
| [SavedObjectsFindResponse](./kibana-plugin-core-server.savedobjectsfindresponse.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. |
+| [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) | |
| [SavedObjectsImportConflictError](./kibana-plugin-core-server.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. |
| [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) | Represents a failure to import. |
| [SavedObjectsImportMissingReferencesError](./kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md
index a1b1a7a056206d..4ed069d1598fe4 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md
@@ -20,6 +20,6 @@ export interface SavedObjectsFindResponse
| --- | --- | --- |
| [page](./kibana-plugin-core-server.savedobjectsfindresponse.page.md) | number | |
| [per\_page](./kibana-plugin-core-server.savedobjectsfindresponse.per_page.md) | number | |
-| [saved\_objects](./kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md) | Array<SavedObject<T>> | |
+| [saved\_objects](./kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md) | Array<SavedObjectsFindResult<T>> | |
| [total](./kibana-plugin-core-server.savedobjectsfindresponse.total.md) | number | |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md
index adad0dd2b11767..7a91367f6ef0bc 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md
@@ -7,5 +7,5 @@
Signature:
```typescript
-saved_objects: Array>;
+saved_objects: Array>;
```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md
new file mode 100644
index 00000000000000..e455074a7d11bb
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md
@@ -0,0 +1,19 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md)
+
+## SavedObjectsFindResult interface
+
+
+Signature:
+
+```typescript
+export interface SavedObjectsFindResult extends SavedObject
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [score](./kibana-plugin-core-server.savedobjectsfindresult.score.md) | number | The Elasticsearch _score of this result. |
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.score.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.score.md
new file mode 100644
index 00000000000000..c6646df6ee4700
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.score.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) > [score](./kibana-plugin-core-server.savedobjectsfindresult.score.md)
+
+## SavedObjectsFindResult.score property
+
+The Elasticsearch `_score` of this result.
+
+Signature:
+
+```typescript
+score: number;
+```
diff --git a/package.json b/package.json
index e91c5e96b78ab4..06dfb4cdfe3872 100644
--- a/package.json
+++ b/package.json
@@ -144,7 +144,6 @@
"@kbn/test-subj-selector": "0.2.1",
"@kbn/ui-framework": "1.0.0",
"@kbn/ui-shared-deps": "1.0.0",
- "@types/tar": "^4.0.3",
"JSONStream": "1.3.5",
"abortcontroller-polyfill": "^1.4.0",
"accept": "3.0.2",
@@ -390,9 +389,10 @@
"@types/styled-components": "^5.1.0",
"@types/supertest": "^2.0.5",
"@types/supertest-as-promised": "^2.0.38",
+ "@types/tar": "^4.0.3",
+ "@types/testing-library__dom": "^6.10.0",
"@types/testing-library__react": "^9.1.2",
"@types/testing-library__react-hooks": "^3.1.0",
- "@types/testing-library__dom": "^6.10.0",
"@types/type-detect": "^4.0.1",
"@types/uuid": "^3.4.4",
"@types/vinyl-fs": "^2.4.11",
@@ -486,12 +486,10 @@
"prettier": "^2.0.5",
"proxyquire": "1.8.0",
"react-popper-tooltip": "^2.10.1",
- "react-textarea-autosize": "^7.1.2",
"regenerate": "^1.4.0",
"sass-lint": "^1.12.1",
"selenium-webdriver": "^4.0.0-alpha.7",
"simple-git": "1.116.0",
- "simplebar-react": "^2.1.0",
"sinon": "^7.4.2",
"strip-ansi": "^3.0.1",
"supertest": "^3.1.0",
diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts
index 2eb6c6cc5aac6b..861ea0988692c5 100644
--- a/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts
+++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts
@@ -18,7 +18,7 @@
*/
import { ToolingLog } from '../tooling_log';
-import { KbnClientRequester, ReqOptions } from './kbn_client_requester';
+import { KibanaConfig, KbnClientRequester, ReqOptions } from './kbn_client_requester';
import { KbnClientStatus } from './kbn_client_status';
import { KbnClientPlugins } from './kbn_client_plugins';
import { KbnClientVersion } from './kbn_client_version';
@@ -26,7 +26,7 @@ import { KbnClientSavedObjects } from './kbn_client_saved_objects';
import { KbnClientUiSettings, UiSettingValues } from './kbn_client_ui_settings';
export class KbnClient {
- private readonly requester = new KbnClientRequester(this.log, this.kibanaUrls);
+ private readonly requester = new KbnClientRequester(this.log, this.kibanaConfig);
readonly status = new KbnClientStatus(this.requester);
readonly plugins = new KbnClientPlugins(this.status);
readonly version = new KbnClientVersion(this.status);
@@ -43,10 +43,10 @@ export class KbnClient {
*/
constructor(
private readonly log: ToolingLog,
- private readonly kibanaUrls: string[],
+ private readonly kibanaConfig: KibanaConfig,
private readonly uiSettingDefaults?: UiSettingValues
) {
- if (!kibanaUrls.length) {
+ if (!kibanaConfig.url) {
throw new Error('missing Kibana urls');
}
}
diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts
index ea4159de557499..2aba2be56f277b 100644
--- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts
+++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts
@@ -16,10 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
-
import Url from 'url';
-
-import Axios from 'axios';
+import Https from 'https';
+import Axios, { AxiosResponse } from 'axios';
import { isAxiosRequestError, isAxiosResponseError } from '../axios';
import { ToolingLog } from '../tooling_log';
@@ -70,20 +69,38 @@ const delay = (ms: number) =>
setTimeout(resolve, ms);
});
+export interface KibanaConfig {
+ url: string;
+ ssl?: {
+ enabled: boolean;
+ key: string;
+ certificate: string;
+ certificateAuthorities: string;
+ };
+}
+
export class KbnClientRequester {
- constructor(private readonly log: ToolingLog, private readonly kibanaUrls: string[]) {}
+ private readonly httpsAgent: Https.Agent | null;
+ constructor(private readonly log: ToolingLog, private readonly kibanaConfig: KibanaConfig) {
+ this.httpsAgent =
+ kibanaConfig.ssl && kibanaConfig.ssl.enabled
+ ? new Https.Agent({
+ cert: kibanaConfig.ssl.certificate,
+ key: kibanaConfig.ssl.key,
+ ca: kibanaConfig.ssl.certificateAuthorities,
+ })
+ : null;
+ }
private pickUrl() {
- const url = this.kibanaUrls.shift()!;
- this.kibanaUrls.push(url);
- return url;
+ return this.kibanaConfig.url;
}
public resolveUrl(relativeUrl: string = '/') {
return Url.resolve(this.pickUrl(), relativeUrl);
}
- async request(options: ReqOptions): Promise {
+ async request(options: ReqOptions): Promise> {
const url = Url.resolve(this.pickUrl(), options.path);
const description = options.description || `${options.method} ${url}`;
let attempt = 0;
@@ -93,7 +110,7 @@ export class KbnClientRequester {
attempt += 1;
try {
- const response = await Axios.request({
+ const response = await Axios.request({
method: options.method,
url,
data: options.body,
@@ -101,9 +118,10 @@ export class KbnClientRequester {
headers: {
'kbn-xsrf': 'kbn-client',
},
+ httpsAgent: this.httpsAgent,
});
- return response.data;
+ return response;
} catch (error) {
const conflictOnGet = isConcliftOnGetError(error);
const requestedRetries = options.retries !== undefined;
diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts
index e671061b343523..7334c6353debfc 100644
--- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts
+++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts
@@ -71,12 +71,13 @@ export class KbnClientSavedObjects {
public async migrate() {
this.log.debug('Migrating saved objects');
- return await this.requester.request({
+ const { data } = await this.requester.request({
description: 'migrate saved objects',
path: uriencode`/internal/saved_objects/_migrate`,
method: 'POST',
body: {},
});
+ return data;
}
/**
@@ -85,11 +86,12 @@ export class KbnClientSavedObjects {
public async get>(options: GetOptions) {
this.log.debug('Gettings saved object: %j', options);
- return await this.requester.request>({
+ const { data } = await this.requester.request>({
description: 'get saved object',
path: uriencode`/api/saved_objects/${options.type}/${options.id}`,
method: 'GET',
});
+ return data;
}
/**
@@ -98,7 +100,7 @@ export class KbnClientSavedObjects {
public async create>(options: IndexOptions) {
this.log.debug('Creating saved object: %j', options);
- return await this.requester.request>({
+ const { data } = await this.requester.request>({
description: 'update saved object',
path: options.id
? uriencode`/api/saved_objects/${options.type}/${options.id}`
@@ -113,6 +115,7 @@ export class KbnClientSavedObjects {
references: options.references,
},
});
+ return data;
}
/**
@@ -121,7 +124,7 @@ export class KbnClientSavedObjects {
public async update>(options: UpdateOptions) {
this.log.debug('Updating saved object: %j', options);
- return await this.requester.request>({
+ const { data } = await this.requester.request>({
description: 'update saved object',
path: uriencode`/api/saved_objects/${options.type}/${options.id}`,
query: {
@@ -134,6 +137,7 @@ export class KbnClientSavedObjects {
references: options.references,
},
});
+ return data;
}
/**
@@ -142,10 +146,12 @@ export class KbnClientSavedObjects {
public async delete(options: GetOptions) {
this.log.debug('Deleting saved object %s/%s', options);
- return await this.requester.request({
+ const { data } = await this.requester.request({
description: 'delete saved object',
path: uriencode`/api/saved_objects/${options.type}/${options.id}`,
method: 'DELETE',
});
+
+ return data;
}
}
diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts
index 22baf4a3304168..4f203e73620f35 100644
--- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts
+++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts
@@ -52,10 +52,11 @@ export class KbnClientStatus {
* Get the full server status
*/
async get() {
- return await this.requester.request({
+ const { data } = await this.requester.request({
method: 'GET',
path: 'api/status',
});
+ return data;
}
/**
diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts
index dbfa87e70032bf..6ee2d3bfe59b0c 100644
--- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts
+++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts
@@ -57,10 +57,11 @@ export class KbnClientUiSettings {
* Unset a uiSetting
*/
async unset(setting: string) {
- return await this.requester.request({
+ const { data } = await this.requester.request({
path: uriencode`/api/kibana/settings/${setting}`,
method: 'DELETE',
});
+ return data;
}
/**
@@ -105,11 +106,11 @@ export class KbnClientUiSettings {
}
private async getAll() {
- const resp = await this.requester.request({
+ const { data } = await this.requester.request({
path: '/api/kibana/settings',
method: 'GET',
});
- return resp.settings;
+ return data.settings;
}
}
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 29ec28175a8519..e9aeee87f1a3b2 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
@@ -38,6 +38,14 @@ const urlPartsSchema = () =>
password: Joi.string(),
pathname: Joi.string().regex(/^\//, 'start with a /'),
hash: Joi.string().regex(/^\//, 'start with a /'),
+ ssl: Joi.object()
+ .keys({
+ enabled: Joi.boolean().default(false),
+ certificate: Joi.string().optional(),
+ certificateAuthorities: Joi.string().optional(),
+ key: Joi.string().optional(),
+ })
+ .default(),
})
.default();
@@ -122,6 +130,7 @@ export const schema = Joi.object()
type: Joi.string().valid('chrome', 'firefox', 'ie', 'msedge').default('chrome'),
logPollingMs: Joi.number().default(100),
+ acceptInsecureCerts: Joi.boolean().default(false),
})
.default(),
diff --git a/packages/kbn-test/src/kbn/index.js b/packages/kbn-test/src/kbn/index.ts
similarity index 100%
rename from packages/kbn-test/src/kbn/index.js
rename to packages/kbn-test/src/kbn/index.ts
diff --git a/packages/kbn-test/src/kbn/kbn_test_config.js b/packages/kbn-test/src/kbn/kbn_test_config.ts
similarity index 76%
rename from packages/kbn-test/src/kbn/kbn_test_config.js
rename to packages/kbn-test/src/kbn/kbn_test_config.ts
index c43efabb4b747c..909c94098cf5d6 100644
--- a/packages/kbn-test/src/kbn/kbn_test_config.js
+++ b/packages/kbn-test/src/kbn/kbn_test_config.ts
@@ -16,26 +16,34 @@
* specific language governing permissions and limitations
* under the License.
*/
-
-import { kibanaTestUser } from './users';
import url from 'url';
+import { kibanaTestUser } from './users';
+
+interface UrlParts {
+ protocol?: string;
+ hostname?: string;
+ port?: number;
+ auth?: string;
+ username?: string;
+ password?: string;
+}
export const kbnTestConfig = new (class KbnTestConfig {
getPort() {
return this.getUrlParts().port;
}
- getUrlParts() {
+ getUrlParts(): UrlParts {
// allow setting one complete TEST_KIBANA_URL for ES like https://elastic:changeme@example.com:9200
if (process.env.TEST_KIBANA_URL) {
const testKibanaUrl = url.parse(process.env.TEST_KIBANA_URL);
return {
- protocol: testKibanaUrl.protocol.slice(0, -1),
+ protocol: testKibanaUrl.protocol?.slice(0, -1),
hostname: testKibanaUrl.hostname,
- port: parseInt(testKibanaUrl.port, 10),
+ port: testKibanaUrl.port ? parseInt(testKibanaUrl.port, 10) : undefined,
auth: testKibanaUrl.auth,
- username: testKibanaUrl.auth.split(':')[0],
- password: testKibanaUrl.auth.split(':')[1],
+ username: testKibanaUrl.auth?.split(':')[0],
+ password: testKibanaUrl.auth?.split(':')[1],
};
}
@@ -44,7 +52,7 @@ export const kbnTestConfig = new (class KbnTestConfig {
return {
protocol: process.env.TEST_KIBANA_PROTOCOL || 'http',
hostname: process.env.TEST_KIBANA_HOSTNAME || 'localhost',
- port: parseInt(process.env.TEST_KIBANA_PORT, 10) || 5620,
+ port: process.env.TEST_KIBANA_PORT ? parseInt(process.env.TEST_KIBANA_PORT, 10) : 5620,
auth: `${username}:${password}`,
username,
password,
diff --git a/packages/kbn-test/src/kbn/users.js b/packages/kbn-test/src/kbn/users.ts
similarity index 100%
rename from packages/kbn-test/src/kbn/users.js
rename to packages/kbn-test/src/kbn/users.ts
diff --git a/rfcs/text/0011_reporting_as_an_api.md b/rfcs/text/0011_reporting_as_an_api.md
new file mode 100644
index 00000000000000..8975d51dbd909d
--- /dev/null
+++ b/rfcs/text/0011_reporting_as_an_api.md
@@ -0,0 +1,284 @@
+- Start Date: 2020-04-23
+- RFC PR: (leave this empty)
+- Kibana Issue: (leave this empty)
+
+# Summary
+
+The reporting plugin is migrating to a purely REST API interface, deprecating page-level integrations such as Dashboard and Discover.
+
+# Basic example
+
+Currently, reporting does expose an API for Dashboard exports as seen below.
+
+```sh
+# Massively truncated URL
+curl -x POST http://localhost:5601/api/reporting/generate/printablePdf?jobParams=%28browserTimezone%3AAmerica%2FLos_Angeles%2Clayout...
+```
+
+Going forth, reporting would only offer a JSON-based REST API, deprecating older ad-hoc solutions:
+
+```sh
+curl -x POST http://localhost:5601/api/reporting/pdf
+{
+ “baseUrl”: “/my/kibana/page/route?foo=bar&reporting=true”,
+ "waitUntil": {
+ "event": “complete”,
+ },
+ “viewport”: {
+ “width”: 1920,
+ “height”: 1080,
+ "scale": 1
+ },
+ "mediaType": "screen"
+}
+```
+
+A simple JSON response is returned, with an identifier to query for status.
+
+```json
+{
+ "id": "123"
+}
+```
+
+Further information can be found via GET call with the job's ID:
+
+```sh
+curl http://localhost:5601/api/reporting/123/status
+```
+
+# Motivation
+
+The reporting functionality that currently exists in Kibana was originally purpose-built for the Discover, Dashboard and Canvas applications. Because of this, reportings underlying technologies and infrastructure are hard to improve upon and make generally available for pages across Kibana. Currently, the team has to:
+
+- Build and maintain our own Chromium binary for the 3 main operating systems we support.
+- Fix and help troubleshoot issues encountered by our users and their complex deployment topologies.
+- Ensure successful operation in smaller-sized cloud deployments.
+- Help other teams get their applications “reportable”.
+- Continue to adapt changes in Discover and Dashboard so that they can be reportable (WebGL for instance).
+
+In order to ensure the reporting works in a secure manner, we also maintain complex logic that ensures nothing goes wrong during report generation. These include:
+
+- Home-rolled security role checks.
+- A custom-built network firewall via puppeteer to ensure chromium can’t be hijacked for nefarious purposes.
+- Network request interception to apply authorization contexts.
+- Configuration checks for both Elasticsearch and Kibana, to ensure the user's configuration is valid and workable.
+- CSV formula injection checks, encodings, and other challenges.
+
+It's important that there be a barrier between *how* reporting works, as well as *how* an application in Kibana is rendered. As of today no such barrier exists.
+
+While we understand that many of these requirements are similar across teams, however, in order to better serve the application teams that depend on reporting the time has come to rethink reportings role inside of Kibana, and how we can scale it across our product suite.
+
+# Detailed design
+
+## REST API
+
+Though we plan to support additional functionality longer-term (for instance a client-api or support for scheduling), the initial product will solely be a REST API involving a 4 part life-cycle:
+
+1. Starting a new job.
+2. Querying a job's status.
+3. Downloading the job's results.
+4. Deleting a job
+
+Reporting will return a list of HTTP codes to indicate acceptance or rejection of any HTTP interaction:
+
+**Possible HTTP responses**
+
+`200`: Job is accepted and is queued for execution
+
+`204`: OK response, but no message returned (used in DELETE calls)
+
+`400`: There was a malformation of the request, and consumers should review the returned message
+
+`403`: The user is not allowed to create a job
+
+`404`: Job wasn't found
+
+`401`: The request isn't properly authorized
+
+### 1. Starting a new job
+
+The primary export type in this phase will be a PDF binary (retrieved in Step 3). Registering can be as complex as below:
+
+```sh
+curl -x POST http://kibana-host:kibana-port/api/reporting/pdf
+[{
+ “baseUrl”: “/my/kibana/page/route?page=1&reporting=true”,
+ "waitUntil": {
+ "event": “complete”,
+ },
+ “viewport”: {
+ “width”: 1920,
+ “height”: 1080,
+ "scale": 2
+ },
+ "mediaType": "screen",
+ "timeout": 30000,
+}, {
+ “baseUrl”: “/my/kibana/page/route?page=2&reporting=true”,
+ "waitUntil": {
+ "event": “complete”,
+ },
+ “viewport”: {
+ “width”: 1920,
+ “height”: 1080,
+ "scale": 2
+ },
+ "mediaType": "screen",
+ "timeout": 30000,
+}]
+```
+
+In the above example, a consumer posts an array of URLs to be exported. When doing so, the assumption here is that the pages relate to each other in some fashion (workpad's in Canvas, for instance), and thus can be optimized by using the page and browser objects. It should be noted that even though we're given a collection of pages to export that *they'll be rendered in series and not parallel*.
+
+`baseUrl: string`: The URL of the page you wish to export, relative to Kibana's default path. For instance, if canvas wanted to export a page at `http://localhost:5601/app/canvas#/workpad/workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e/page/1`, the `baseUrl` would be `/app/canvas#/workpad/workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e/page/1`. This is done to prevent our chromium process from being "hijacked" to navigate elsewhere. You're free to do whatever you'd like for the URL, including any query-string parameters or other variables in order to properly render you page for reporting. For instance, you'll notice the `reporting=true` param listed above.
+
+`waitUntil: { event: string; selector: string }`: An object, specifying a custom `DOM` event to "listen" for in our chromium process, or the presence of a DOM selector. Either options are valid, however we won't allow for both options to be set. For instance, if a page inserts a `
` in its markup, then the appropriate payload would be:
+
+```json
+"waitUntil": {
+ "selector": "div.loaded",
+},
+```
+
+`viewport: { width: number; height: number; scale: number }`: Viewport will allow consumers the ability to make rigid dimensions of the browser, such that the formatting of their pages sized appropriately. Scale, in this context, refers roughly to pixel density if there's a need for a higher resolution. A page that needs high-resolution PDF, could set this by:
+
+```json
+“viewport”: {
+ “width”: 1920,
+ “height”: 1080,
+ "scale": 2
+},
+```
+
+`mediaType: "screen" | "print"`: It's often the case that pages would like to use print media-queries, and this allows for opting-in or out of that behavior. For example, if a page wishes to utilize its print media queries, a payload with:
+
+```json
+"mediaType": "print"
+```
+
+`timeout: number`: When present, this allows for consumers to override the default reporting timeout. This is useful if a job is known to take much longer to process, or supporting our users without requiring them to restart their Kibana servers for a simple configuration change. Value here is milliseconds.
+
+```json
+"timeout": 60000
+```
+
+**Full job creation example:**
+
+```curl
+curl -x POST http://localhost:5601/api/reporting/pdf
+[{
+ “baseUrl”: “/my/kibana/page/route?page=1&reporting=true”,
+ "waitUntil": {
+ "event": “complete”,
+ },
+ “viewport”: {
+ “width”: 1920,
+ “height”: 1080,
+ "scale": 2
+ },
+ "mediaType": "screen",
+ "timeout": 30000,
+}, {
+ “baseUrl”: “/my/kibana/page/route?page=2&reporting=true”,
+ "waitUntil": {
+ "event": “complete”,
+ },
+ “viewport”: {
+ “width”: 1920,
+ “height”: 1080,
+ "scale": 2
+ },
+ "mediaType": "screen",
+ "timeout": 30000,
+}]
+
+# Response (note the single ID of the response)
+# 200 Content-Type application/json
+{
+ "id": "123"
+}
+```
+
+### 2. Querying and altering a job's status.
+
+Once created, a user can simply issue simple GET call to see the status of the job.
+
+**Get a job's status:**
+
+```curl
+curl -x GET http://localhost:5601/api/reporting/123/status
+
+# Response
+# 200 Content-Type application/json
+# We might provide other meta-data here as well when required
+{
+ "status": "pending",
+ "elapsedTime": 12345
+}
+```
+
+Possible types for `status` here are: `pending`, `running`, `complete`, `complete-warnings`, `failed`, or `timedout`. We can add more detail here if needed, such as the current URL being operated or whatever other information is valuable to consumers.
+
+### 4. Deleting a job
+
+A DELETE call will remove the report. If a report is in pending/running state, this will attempt to terminate the running job. Once a report is complete, the call to delete will permanently (hard delete) remove the job's output in ElasticSearch.
+
+When successfully deleted, reporting will simply respond with a `204` HTTP code, indicating success.
+
+```curl
+curl -x DELETE http://localhost:5601/api/reporting/123
+
+# Response (no body, 204 indicates success)
+# 204 Content-Type text/plain;charset=UTF-8
+```
+
+# Drawbacks
+
+Due to the new nature of this RFC, there are definitely drawbacks to this approach short-term. These short-term drawbacks become miniscule longer-term, since the work being done here frees both reporting and downstream teams to operate in parallel.
+
+- Initial work to build this pipeline will freeze some current efforts (scheduled reports, etc).
+- Doesn't solve complex architectural issues experienced by our customers.
+- Requires work to migrate our existing apps (Canvas, dashboard, visualizations).
+- Doesn't offer any performance characteristics over our current implementation.
+
+Though there's some acute pain felt here shorter term, they pale in comparison to building custom ad-hoc solutions for each application inside of Kibana.
+
+# Alternatives
+
+Going through the process of developing this current RFC, we did entertain a few other strategies:
+
+## No changes in how we operate
+
+This strategy doesn't scale beyond the current two team members since we field many support issues that are application-specific, and not reporting specific. This keeps our trajectory where it currently is, short term, but hamstrings us longer term. Unfortunately, for teams to have the best experience with regards to reporting, they'll need to have ownership on the rendering aspects of their pages.
+
+## A new plugin
+
+We debated offering a new plugin, or having apps consume this type of service as a plugin, but ultimately it was too much overhead for the nature of what we're offering. More information on the prior RFC is here: https://github.com/elastic/kibana/pull/59084.
+
+## Each page builds its own pipeline
+
+This would allow teams to operate how they best see fit, but would come with a host of issues:
+
+- Each team would need to ramp up on how we handle chromium and all of its sharp edges.
+- Potential for many requests to be in flight at once, causing exhaustion of resources.
+- Mixed experience across different apps, and varying degrees of success.
+- No central management of a users general reports.
+
+# Adoption strategy
+
+After work on the service is complete in its initial phase, we'll begin to migrate the Dashboard app over to the new service. This will give a clear example of:
+
+- Moving a complex page over to this service.
+- Where the divisions of labor reside (who does what).
+- How to embed rendering-specific logic into your pages.
+
+Since reporting only exists on a few select pages, there won't be need for a massive migration effort. Instead, folks wanting to move over to the new rendering service can simply take a look at how Dashboard handles their exporting.
+
+In short, the adoption strategy is fairly minimal due to the lack of pages being reported on.
+
+# Unresolved questions
+
+- How to troubleshoot complex customer environments?
+- When do we do this work?
+- Nuances in the API, are we missing other critical information?
diff --git a/src/core/TESTING.md b/src/core/TESTING.md
index bed41ab583496e..a62922d9b5d64b 100644
--- a/src/core/TESTING.md
+++ b/src/core/TESTING.md
@@ -29,6 +29,14 @@ This document outlines best practices and patterns for testing Kibana Plugins.
- [Testing dependencies usages](#testing-dependencies-usages)
- [Testing components consuming the dependencies](#testing-components-consuming-the-dependencies)
- [Testing optional plugin dependencies](#testing-optional-plugin-dependencies)
+ - [RXJS testing](#rxjs-testing)
+ - [Testing RXJS observables with marble](#rxjs-testing-with-marble)
+ - [Precondition](#preconditions-2)
+ - [Examples](#example-5)
+ - [Testing an interval based observable](#testing-an-interval-based-observable)
+ - [Testing observable completion](#testing-observable-completion)
+ - [Testing observable errors](#testing-observable-errors)
+ - [Testing promise based observables](#testing-promise-based-observables)
## Strategy
@@ -1087,3 +1095,271 @@ describe('Plugin', () => {
});
});
```
+
+## RXJS testing
+
+### Testing RXJS observables with marble
+
+Testing observable based APIs can be challenging, specially when asynchronous operators or sources are used,
+or when trying to assert against emission's timing.
+
+Fortunately, RXJS comes with it's own `marble` testing module to greatly facilitate that kind of testing.
+
+See [the official doc](https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing) for more information about marble testing.
+
+### Preconditions
+
+The following examples all assume that the following snippet is included in every test file:
+
+```typescript
+import { TestScheduler } from 'rxjs/testing';
+
+const getTestScheduler = () =>
+ new TestScheduler((actual, expected) => {
+ expect(actual).toEqual(expected);
+ });
+```
+
+`getTestScheduler` creates a `TestScheduler` that is wired on `jest`'s `expect` statement when comparing an observable's time frame.
+
+### Examples
+
+#### Testing an interval based observable
+
+Here is a very basic example of an interval-based API:
+
+```typescript
+class FooService {
+ setup() {
+ return {
+ getUpdate$: () => {
+ return interval(100).pipe(map((count) => `update-${count + 1}`));
+ },
+ };
+ }
+}
+```
+
+If we were to be adding a test that asserts the correct behavior of this API without using marble testing, it
+would probably be something like:
+
+```typescript
+it('getUpdate$ emits updates every 100ms', async () => {
+ const service = new FooService();
+ const { getUpdate$ } = service.setup();
+ expect(await getUpdate$().pipe(take(3), toArray()).toPromise()).toEqual([
+ 'update-1',
+ 'update-2',
+ 'update-3',
+ ]);
+});
+```
+
+Note that if we are able to test the correct value of each emission, we don't have any way to assert that
+the interval of 100ms was respected. Even using a subscription based test to try to do so would result
+in potential flakiness, as the subscription execution could trigger on the `101ms` time frame for example.
+
+It also may be important to note:
+- as we need to convert the observable to a promise and wait for the result, the test is `async`
+- we need to perform observable transformation (`take` + `toArray`) in the test to have an usable value to assert against.
+
+Marble testing would allow to get rid of these limitations. An equivalent and improved marble test could be:
+
+```typescript
+ describe('getUpdate$', () => {
+ it('emits updates every 100ms', () => {
+ getTestScheduler().run(({ expectObservable }) => {
+ const { getUpdate$ } = service.setup();
+ expectObservable(getUpdate$(), '301ms !').toBe('100ms a 99ms b 99ms c', {
+ a: 'update-1',
+ b: 'update-2',
+ c: 'update-3',
+ });
+ });
+ });
+ });
+```
+
+Notes:
+- the test is now synchronous
+- the second parameter of `expectObservable` (`'301ms !'`) is used to perform manual unsubscription to the observable, as
+ `interval` never ends.
+- an emission is considered a time frame, meaning that after the initial `a` emission, we are at the frame `101`, not `100`
+ which is why we are then only using a `99ms` gap between a->b and b->c.
+
+#### Testing observable completion
+
+Let's 'improve' our `getUpdate$` API by allowing the consumer to manually terminate the observable chain using
+a new `abort$` option:
+
+```typescript
+class FooService {
+ setup() {
+ return {
+ // note: using an abortion observable is usually an anti-pattern, as unsubscribing from the observable
+ // is, most of the time, a better solution. This is only used for the example purpose.
+ getUpdate$: ({ abort$ = EMPTY }: { abort$?: Observable } = {}) => {
+ return interval(100).pipe(
+ takeUntil(abort$),
+ map((count) => `update-${count + 1}`)
+ );
+ },
+ };
+ }
+}
+```
+
+We would then add a test to assert than this new option usage is respected:
+
+```typescript
+it('getUpdate$ completes when `abort$` emits', () => {
+ const service = new FooService();
+ getTestScheduler().run(({ expectObservable, hot }) => {
+ const { getUpdate$ } = service.setup();
+ const abort$ = hot('149ms a', { a: undefined });
+ expectObservable(getUpdate$({ abort$ })).toBe('100ms a 48ms |', {
+ a: 'update-1',
+ });
+ });
+});
+```
+
+Notes:
+ - the `|` symbol represents the completion of the observable.
+ - we are here using the `hot` testing utility to create the `abort$` observable to ensure correct emission timing.
+
+#### Testing observable errors
+
+Testing errors thrown by the observable is very close to the previous examples and is done using
+the third parameter of `expectObservable`.
+
+Say we have a service in charge of processing data from an observable and returning the results in a new observable:
+
+```typescript
+interface SomeDataType {
+ id: string;
+}
+
+class BarService {
+ setup() {
+ return {
+ processDataStream: (data$: Observable) => {
+ return data$.pipe(
+ map((data) => {
+ if (data.id === 'invalid') {
+ throw new Error(`invalid data: '${data.id}'`);
+ }
+ return {
+ ...data,
+ processed: 'additional-data',
+ };
+ })
+ );
+ },
+ };
+ }
+}
+```
+
+We could write a test that asserts the service properly emit processed results until an invalid data is encountered:
+
+```typescript
+it('processDataStream throw an error when processing invalid data', () => {
+ getTestScheduler().run(({ expectObservable, hot }) => {
+ const service = new BarService();
+ const { processDataStream } = service.setup();
+
+ const data = hot('--a--b--(c|)', {
+ a: { id: 'a' },
+ b: { id: 'invalid' },
+ c: { id: 'c' },
+ });
+
+ expectObservable(processDataStream(data)).toBe(
+ '--a--#',
+ {
+ a: { id: 'a', processed: 'additional-data' },
+ },
+ `'[Error: invalid data: 'invalid']'`
+ );
+ });
+});
+```
+
+Notes:
+ - the `-` symbol represents one virtual time frame.
+ - the `#` symbol represents an error.
+ - when throwing custom `Error` classes, the assertion can be against an error instance, but this doesn't work
+ with base errors.
+
+#### Testing promise based observables
+
+In some cases, the observable we want to test is based on a Promise (like `of(somePromise).pipe(...)`). This can occur
+when using promise-based services, such as core's `http`, for instance.
+
+```typescript
+export const callServerAPI = (
+ http: HttpStart,
+ body: Record,
+ { abort$ }: { abort$: Observable }
+): Observable => {
+ let controller: AbortController | undefined;
+ if (abort$) {
+ controller = new AbortController();
+ abort$.subscribe(() => {
+ controller!.abort();
+ });
+ }
+ return from(
+ http.post('/api/endpoint', {
+ body,
+ signal: controller?.signal,
+ })
+ ).pipe(
+ takeUntil(abort$ ?? EMPTY),
+ map((response) => response.results)
+ );
+};
+```
+
+Testing that kind of promise based observable does not work out of the box with marble testing, as the asynchronous promise resolution
+is not handled by the test scheduler's 'sandbox'.
+
+Fortunately, there are workarounds for this problem. The most common one being to mock the promise-returning API to return
+an observable instead for testing, as `of(observable)` also works and returns the input observable.
+
+Note that when doing so, the test suite must also include tests using a real promise value to ensure correct behavior in real situation.
+
+```typescript
+
+// NOTE: test scheduler do not properly work with promises because of their asynchronous nature.
+// we are cheating here by having `http.post` return an observable instead of a promise.
+// this still allows more finely grained testing about timing, and asserting that the method
+// works properly when `post` returns a real promise is handled in other tests of this suite
+
+it('callServerAPI result observable emits when the response is received', () => {
+ const http = httpServiceMock.createStartContract();
+ getTestScheduler().run(({ expectObservable, hot }) => {
+ // need to cast the observable as `any` because http.post.mockReturnValue expects a promise, see previous comment
+ http.post.mockReturnValue(hot('---(a|)', { a: { someData: 'foo' } }) as any);
+
+ const results = callServerAPI(http, { query: 'term' }, {});
+
+ expectObservable(results).toBe('---(a|)', {
+ a: { someData: 'foo' },
+ });
+ });
+});
+
+it('completes without returning results if aborted$ emits before the response', () => {
+ const http = httpServiceMock.createStartContract();
+ getTestScheduler().run(({ expectObservable, hot }) => {
+ // need to cast the observable as `any` because http.post.mockReturnValue expects a promise, see previous comment
+ http.post.mockReturnValue(hot('---(a|)', { a: { someData: 'foo' } }) as any);
+ const aborted$ = hot('-(a|)', { a: undefined });
+ const results = callServerAPI(http, { query: 'term' }, { aborted$ });
+
+ expectObservable(results).toBe('-|');
+ });
+});
+```
\ No newline at end of file
diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts
index 5c8eca4a33ec57..cb279b2cc4c8f9 100644
--- a/src/core/public/saved_objects/saved_objects_client.ts
+++ b/src/core/public/saved_objects/saved_objects_client.ts
@@ -303,7 +303,6 @@ export class SavedObjectsClient {
query,
});
return request.then((resp) => {
- resp.saved_objects = resp.saved_objects.map((d) => this.createSavedObject(d));
return renameKeys<
PromiseType>,
SavedObjectsFindResponsePublic
@@ -314,7 +313,10 @@ export class SavedObjectsClient {
per_page: 'perPage',
page: 'page',
},
- resp
+ {
+ ...resp,
+ saved_objects: resp.saved_objects.map((d) => this.createSavedObject(d)),
+ }
) as SavedObjectsFindResponsePublic;
});
};
diff --git a/src/core/server/capabilities/capabilities_service.test.ts b/src/core/server/capabilities/capabilities_service.test.ts
index 7d2e7391aa8d4b..42dc1604281b82 100644
--- a/src/core/server/capabilities/capabilities_service.test.ts
+++ b/src/core/server/capabilities/capabilities_service.test.ts
@@ -17,19 +17,19 @@
* under the License.
*/
-import { httpServiceMock, HttpServiceSetupMock } from '../http/http_service.mock';
+import { httpServiceMock, InternalHttpServiceSetupMock } from '../http/http_service.mock';
import { mockRouter, RouterMock } from '../http/router/router.mock';
import { CapabilitiesService, CapabilitiesSetup } from './capabilities_service';
import { mockCoreContext } from '../core_context.mock';
describe('CapabilitiesService', () => {
- let http: HttpServiceSetupMock;
+ let http: InternalHttpServiceSetupMock;
let service: CapabilitiesService;
let setup: CapabilitiesSetup;
let router: RouterMock;
beforeEach(() => {
- http = httpServiceMock.createSetupContract();
+ http = httpServiceMock.createInternalSetupContract();
router = mockRouter.create();
http.createRouter.mockReturnValue(router);
service = new CapabilitiesService(mockCoreContext.create());
diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts
index e7dab3807733a5..8bf0df74186a99 100644
--- a/src/core/server/elasticsearch/elasticsearch_service.test.ts
+++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts
@@ -39,7 +39,7 @@ const delay = async (durationMs: number) =>
let elasticsearchService: ElasticsearchService;
const configService = configServiceMock.create();
const deps = {
- http: httpServiceMock.createSetupContract(),
+ http: httpServiceMock.createInternalSetupContract(),
};
configService.atPath.mockReturnValue(
new BehaviorSubject({
diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts
index 1798c3a921da42..9a5deb9b455627 100644
--- a/src/core/server/http/http_server.test.ts
+++ b/src/core/server/http/http_server.test.ts
@@ -1046,17 +1046,6 @@ describe('setup contract', () => {
});
});
- describe('#isTlsEnabled', () => {
- it('returns "true" if TLS enabled', async () => {
- const { isTlsEnabled } = await server.setup(configWithSSL);
- expect(isTlsEnabled).toBe(true);
- });
- it('returns "false" if TLS not enabled', async () => {
- const { isTlsEnabled } = await server.setup(config);
- expect(isTlsEnabled).toBe(false);
- });
- });
-
describe('#getServerInfo', () => {
it('returns correct information', async () => {
let { getServerInfo } = await server.setup(config);
diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts
index 8089ee901fa65c..d4615dd4744e58 100644
--- a/src/core/server/http/http_server.ts
+++ b/src/core/server/http/http_server.ts
@@ -53,7 +53,6 @@ export interface HttpServerSetup {
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
registerOnPreResponse: HttpServiceSetup['registerOnPreResponse'];
- isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
getAuthHeaders: GetAuthHeaders;
auth: {
get: GetAuthState;
@@ -133,7 +132,6 @@ export class HttpServer {
port: config.port,
protocol: this.server!.info.protocol,
}),
- isTlsEnabled: config.ssl.enabled,
// Return server instance with the connection options so that we can properly
// bridge core and the "legacy" Kibana internally. Once this bridge isn't
// needed anymore we shouldn't return the instance from this method.
diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts
index 0788a8f2af7a14..02ae6f5d95a879 100644
--- a/src/core/server/http/http_service.mock.ts
+++ b/src/core/server/http/http_service.mock.ts
@@ -19,9 +19,14 @@
import { Server } from 'hapi';
import { CspConfig } from '../csp';
-import { mockRouter } from './router/router.mock';
+import { mockRouter, RouterMock } from './router/router.mock';
import { configMock } from '../config/config.mock';
-import { InternalHttpServiceSetup } from './types';
+import {
+ InternalHttpServiceSetup,
+ HttpServiceSetup,
+ HttpServiceStart,
+ InternalHttpServiceStart,
+} from './types';
import { HttpService } from './http_service';
import { AuthStatus } from './auth_state_storage';
import { OnPreAuthToolkit } from './lifecycle/on_pre_auth';
@@ -32,7 +37,23 @@ import { OnPreResponseToolkit } from './lifecycle/on_pre_response';
type BasePathMocked = jest.Mocked;
type AuthMocked = jest.Mocked;
-export type HttpServiceSetupMock = jest.Mocked & {
+
+export type HttpServiceSetupMock = jest.Mocked<
+ Omit
+> & {
+ basePath: BasePathMocked;
+ createRouter: jest.MockedFunction<() => RouterMock>;
+};
+export type InternalHttpServiceSetupMock = jest.Mocked<
+ Omit
+> & {
+ basePath: BasePathMocked;
+ createRouter: jest.MockedFunction<(path: string) => RouterMock>;
+};
+export type HttpServiceStartMock = jest.Mocked & {
+ basePath: BasePathMocked;
+};
+export type InternalHttpServiceStartMock = jest.Mocked & {
basePath: BasePathMocked;
};
@@ -54,8 +75,8 @@ const createAuthMock = () => {
return mock;
};
-const createSetupContractMock = () => {
- const setupContract: HttpServiceSetupMock = {
+const createInternalSetupContractMock = () => {
+ const mock: InternalHttpServiceSetupMock = {
// we can mock other hapi server methods when we need it
server: ({
name: 'http-server-test',
@@ -77,31 +98,78 @@ const createSetupContractMock = () => {
csp: CspConfig.DEFAULT,
auth: createAuthMock(),
getAuthHeaders: jest.fn(),
- isTlsEnabled: false,
getServerInfo: jest.fn(),
};
- setupContract.createCookieSessionStorageFactory.mockResolvedValue(
- sessionStorageMock.createFactory()
- );
- setupContract.createRouter.mockImplementation(() => mockRouter.create());
- setupContract.getAuthHeaders.mockReturnValue({ authorization: 'authorization-header' });
- setupContract.getServerInfo.mockReturnValue({
+ mock.createCookieSessionStorageFactory.mockResolvedValue(sessionStorageMock.createFactory());
+ mock.createRouter.mockImplementation(() => mockRouter.create());
+ mock.getAuthHeaders.mockReturnValue({ authorization: 'authorization-header' });
+ mock.getServerInfo.mockReturnValue({
host: 'localhost',
name: 'kibana',
port: 80,
protocol: 'http',
});
- return setupContract;
+ return mock;
+};
+
+const createSetupContractMock = () => {
+ const internalMock = createInternalSetupContractMock();
+
+ const mock: HttpServiceSetupMock = {
+ createCookieSessionStorageFactory: internalMock.createCookieSessionStorageFactory,
+ registerOnPreAuth: internalMock.registerOnPreAuth,
+ registerAuth: internalMock.registerAuth,
+ registerOnPostAuth: internalMock.registerOnPostAuth,
+ registerOnPreResponse: internalMock.registerOnPreResponse,
+ basePath: internalMock.basePath,
+ csp: CspConfig.DEFAULT,
+ createRouter: jest.fn(),
+ registerRouteHandlerContext: jest.fn(),
+ auth: {
+ get: internalMock.auth.get,
+ isAuthenticated: internalMock.auth.isAuthenticated,
+ },
+ getServerInfo: internalMock.getServerInfo,
+ };
+
+ mock.createRouter.mockImplementation(() => internalMock.createRouter(''));
+
+ return mock;
+};
+
+const createStartContractMock = () => {
+ const mock: HttpServiceStartMock = {
+ auth: createAuthMock(),
+ basePath: createBasePathMock(),
+ getServerInfo: jest.fn(),
+ };
+
+ return mock;
+};
+
+const createInternalStartContractMock = () => {
+ const mock: InternalHttpServiceStartMock = {
+ ...createStartContractMock(),
+ isListening: jest.fn(),
+ };
+
+ mock.isListening.mockReturnValue(true);
+
+ return mock;
};
type HttpServiceContract = PublicMethodsOf;
+
const createHttpServiceMock = () => {
const mocked: jest.Mocked = {
setup: jest.fn(),
+ getStartContract: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
- mocked.setup.mockResolvedValue(createSetupContractMock());
+ mocked.setup.mockResolvedValue(createInternalSetupContractMock());
+ mocked.getStartContract.mockReturnValue(createInternalStartContractMock());
+ mocked.start.mockResolvedValue(createInternalStartContractMock());
return mocked;
};
@@ -128,7 +196,10 @@ export const httpServiceMock = {
create: createHttpServiceMock,
createBasePath: createBasePathMock,
createAuth: createAuthMock,
+ createInternalSetupContract: createInternalSetupContractMock,
createSetupContract: createSetupContractMock,
+ createInternalStartContract: createInternalStartContractMock,
+ createStartContract: createStartContractMock,
createOnPreAuthToolkit: createOnPreAuthToolkitMock,
createOnPostAuthToolkit: createOnPostAuthToolkitMock,
createOnPreResponseToolkit: createOnPreResponseToolkitMock,
diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts
index ae9d53f9fd3db2..c2fd6539181719 100644
--- a/src/core/server/http/http_service.ts
+++ b/src/core/server/http/http_service.ts
@@ -22,6 +22,7 @@ import { first, map } from 'rxjs/operators';
import { Server } from 'hapi';
import { CoreService } from '../../types';
+import { pick } from '../../utils';
import { Logger, LoggerFactory } from '../logging';
import { ContextSetup } from '../context';
import { Env } from '../config';
@@ -38,7 +39,7 @@ import {
RequestHandlerContextContainer,
RequestHandlerContextProvider,
InternalHttpServiceSetup,
- HttpServiceStart,
+ InternalHttpServiceStart,
} from './types';
import { RequestHandlerContext } from '../../server';
@@ -49,7 +50,8 @@ interface SetupDeps {
}
/** @internal */
-export class HttpService implements CoreService {
+export class HttpService
+ implements CoreService {
private readonly httpServer: HttpServer;
private readonly httpsRedirectServer: HttpsRedirectServer;
private readonly config$: Observable;
@@ -59,6 +61,7 @@ export class HttpService implements CoreService {
@@ -114,7 +117,16 @@ export class HttpService implements CoreService this.requestHandlerContext!.registerContext(pluginOpaqueId, contextName, provider),
};
- return contract;
+ return this.internalSetup;
+ }
+
+ // this method exists because we need the start contract to create the `CoreStart` used to start
+ // the `plugin` and `legacy` services.
+ public getStartContract(): InternalHttpServiceStart {
+ return {
+ ...pick(this.internalSetup!, ['auth', 'basePath', 'getServerInfo']),
+ isListening: () => this.httpServer.isListening(),
+ };
}
public async start() {
@@ -134,9 +146,7 @@ export class HttpService implements CoreService this.httpServer.isListening(),
- };
+ return this.getStartContract();
}
/**
diff --git a/src/core/server/http/router/router.mock.ts b/src/core/server/http/router/router.mock.ts
index 651d1712100ee5..f85f187164c92a 100644
--- a/src/core/server/http/router/router.mock.ts
+++ b/src/core/server/http/router/router.mock.ts
@@ -19,7 +19,7 @@
import { IRouter } from './router';
-export type RouterMock = DeeplyMockedKeys;
+export type RouterMock = jest.Mocked;
function create({ routerPath = '' }: { routerPath?: string } = {}): RouterMock {
return {
diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts
index 77e0d6b61692da..7f2e70545d0159 100644
--- a/src/core/server/http/types.ts
+++ b/src/core/server/http/types.ts
@@ -47,6 +47,22 @@ export type RequestHandlerContextProvider<
TContextName extends keyof RequestHandlerContext
> = IContextProvider, TContextName>;
+/**
+ * @public
+ */
+export interface HttpAuth {
+ /**
+ * Gets authentication state for a request. Returned by `auth` interceptor.
+ * {@link GetAuthState}
+ */
+ get: GetAuthState;
+ /**
+ * Returns authentication status for a request.
+ * {@link IsAuthenticated}
+ */
+ isAuthenticated: IsAuthenticated;
+}
+
/**
* Kibana HTTP Service provides own abstraction for work with HTTP stack.
* Plugins don't have direct access to `hapi` server and its primitives anymore. Moreover,
@@ -185,28 +201,18 @@ export interface HttpServiceSetup {
*/
basePath: IBasePath;
- auth: {
- /**
- * Gets authentication state for a request. Returned by `auth` interceptor.
- * {@link GetAuthState}
- */
- get: GetAuthState;
- /**
- * Returns authentication status for a request.
- * {@link IsAuthenticated}
- */
- isAuthenticated: IsAuthenticated;
- };
-
/**
- * The CSP config used for Kibana.
+ * Auth status.
+ * See {@link HttpAuth}
+ *
+ * @deprecated use {@link HttpServiceStart.auth | the start contract} instead.
*/
- csp: ICspConfig;
+ auth: HttpAuth;
/**
- * Flag showing whether a server was configured to use TLS connection.
+ * The CSP config used for Kibana.
*/
- isTlsEnabled: boolean;
+ csp: ICspConfig;
/**
* Provides ability to declare a handler function for a particular path and HTTP request method.
@@ -276,8 +282,28 @@ export interface InternalHttpServiceSetup
/** @public */
export interface HttpServiceStart {
- /** Indicates if http server is listening on a given port */
- isListening: (port: number) => boolean;
+ /**
+ * Access or manipulate the Kibana base path
+ * See {@link IBasePath}.
+ */
+ basePath: IBasePath;
+
+ /**
+ * Auth status.
+ * See {@link HttpAuth}
+ */
+ auth: HttpAuth;
+
+ /**
+ * Provides common {@link HttpServerInfo | information} about the running http server.
+ */
+ getServerInfo: () => HttpServerInfo;
+}
+
+/** @internal */
+export interface InternalHttpServiceStart extends HttpServiceStart {
+ /** Indicates if the http server is listening on the configured port */
+ isListening: () => boolean;
}
/** @public */
diff --git a/src/core/server/http_resources/http_resources_service.test.ts b/src/core/server/http_resources/http_resources_service.test.ts
index e6f129ba12d78d..80afddc1665708 100644
--- a/src/core/server/http_resources/http_resources_service.test.ts
+++ b/src/core/server/http_resources/http_resources_service.test.ts
@@ -37,7 +37,7 @@ describe('HttpResources service', () => {
describe('#createRegistrar', () => {
beforeEach(() => {
setupDeps = {
- http: httpServiceMock.createSetupContract(),
+ http: httpServiceMock.createInternalSetupContract(),
rendering: renderingMock.createSetupContract(),
};
service = new HttpResourcesService(coreContext);
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 658c24f835020d..0da7e5d66cf2a7 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -46,7 +46,7 @@ import {
ElasticsearchServiceStart,
} from './elasticsearch';
-import { HttpServiceSetup } from './http';
+import { HttpServiceSetup, HttpServiceStart } from './http';
import { HttpResources } from './http_resources';
import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins';
@@ -121,6 +121,7 @@ export {
CustomHttpResponseOptions,
GetAuthHeaders,
GetAuthState,
+ HttpAuth,
HttpResponseOptions,
HttpResponsePayload,
HttpServerInfo,
@@ -217,6 +218,7 @@ export {
SavedObjectsErrorHelpers,
SavedObjectsExportOptions,
SavedObjectsExportResultDetails,
+ SavedObjectsFindResult,
SavedObjectsFindResponse,
SavedObjectsImportConflictError,
SavedObjectsImportError,
@@ -420,6 +422,8 @@ export interface CoreStart {
capabilities: CapabilitiesStart;
/** {@link ElasticsearchServiceStart} */
elasticsearch: ElasticsearchServiceStart;
+ /** {@link HttpServiceStart} */
+ http: HttpServiceStart;
/** {@link SavedObjectsServiceStart} */
savedObjects: SavedObjectsServiceStart;
/** {@link UiSettingsServiceStart} */
diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts
index 09ec772a417561..f68ab633dcbe65 100644
--- a/src/core/server/internal_types.ts
+++ b/src/core/server/internal_types.ts
@@ -23,7 +23,7 @@ import { CapabilitiesSetup, CapabilitiesStart } from './capabilities';
import { ConfigDeprecationProvider } from './config';
import { ContextSetup } from './context';
import { InternalElasticsearchServiceSetup, ElasticsearchServiceStart } from './elasticsearch';
-import { InternalHttpServiceSetup } from './http';
+import { InternalHttpServiceSetup, InternalHttpServiceStart } from './http';
import {
InternalSavedObjectsServiceSetup,
InternalSavedObjectsServiceStart,
@@ -56,6 +56,7 @@ export interface InternalCoreSetup {
export interface InternalCoreStart {
capabilities: CapabilitiesStart;
elasticsearch: ElasticsearchServiceStart;
+ http: InternalHttpServiceStart;
savedObjects: InternalSavedObjectsServiceStart;
uiSettings: InternalUiSettingsServiceStart;
}
diff --git a/src/core/server/legacy/legacy_internals.test.ts b/src/core/server/legacy/legacy_internals.test.ts
index 2ae5e3a3fd1e84..67f2f433d4570d 100644
--- a/src/core/server/legacy/legacy_internals.test.ts
+++ b/src/core/server/legacy/legacy_internals.test.ts
@@ -45,7 +45,7 @@ describe('LegacyInternals', () => {
beforeEach(async () => {
uiExports = findLegacyPluginSpecsMock().uiExports;
config = configMock.create() as any;
- server = httpServiceMock.createSetupContract().server;
+ server = httpServiceMock.createInternalSetupContract().server;
legacyInternals = new LegacyInternals(uiExports, config, server);
});
@@ -107,7 +107,7 @@ describe('LegacyInternals', () => {
beforeEach(async () => {
uiExports = findLegacyPluginSpecsMock().uiExports;
config = configMock.create() as any;
- server = httpServiceMock.createSetupContract().server;
+ server = httpServiceMock.createInternalSetupContract().server;
legacyInternals = new LegacyInternals(uiExports, config, server);
});
diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts
index d9a0ac5e4ecffb..fb9dc0776716ac 100644
--- a/src/core/server/legacy/legacy_service.test.ts
+++ b/src/core/server/legacy/legacy_service.test.ts
@@ -85,7 +85,7 @@ beforeEach(() => {
elasticsearch: { legacy: {} } as any,
uiSettings: uiSettingsServiceMock.createSetupContract(),
http: {
- ...httpServiceMock.createSetupContract(),
+ ...httpServiceMock.createInternalSetupContract(),
auth: {
getAuthHeaders: () => undefined,
} as any,
@@ -119,7 +119,7 @@ beforeEach(() => {
startDeps = {
core: {
- ...coreMock.createStart(),
+ ...coreMock.createInternalStart(),
savedObjects: savedObjectsServiceMock.createInternalStartContract(),
plugins: { contracts: new Map() },
},
diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts
index 2ced8b47624060..cfc53b10d91f0a 100644
--- a/src/core/server/legacy/legacy_service.ts
+++ b/src/core/server/legacy/legacy_service.ts
@@ -264,6 +264,11 @@ export class LegacyService implements CoreService {
const coreStart: CoreStart = {
capabilities: startDeps.core.capabilities,
elasticsearch: startDeps.core.elasticsearch,
+ http: {
+ auth: startDeps.core.http.auth,
+ basePath: startDeps.core.http.basePath,
+ getServerInfo: startDeps.core.http.getServerInfo,
+ },
savedObjects: {
getScopedClient: startDeps.core.savedObjects.getScopedClient,
createScopedRepository: startDeps.core.savedObjects.createScopedRepository,
@@ -302,7 +307,6 @@ export class LegacyService implements CoreService {
isAuthenticated: setupDeps.core.http.auth.isAuthenticated,
},
csp: setupDeps.core.http.csp,
- isTlsEnabled: setupDeps.core.http.isTlsEnabled,
getServerInfo: setupDeps.core.http.getServerInfo,
},
metrics: {
diff --git a/src/core/server/metrics/metrics_service.test.ts b/src/core/server/metrics/metrics_service.test.ts
index 01a7429745cda5..b3cc06ffca1d21 100644
--- a/src/core/server/metrics/metrics_service.test.ts
+++ b/src/core/server/metrics/metrics_service.test.ts
@@ -30,7 +30,7 @@ const testInterval = 100;
const dummyMetrics = { metricA: 'value', metricB: 'otherValue' };
describe('MetricsService', () => {
- const httpMock = httpServiceMock.createSetupContract();
+ const httpMock = httpServiceMock.createInternalSetupContract();
let metricsService: MetricsService;
beforeEach(() => {
diff --git a/src/core/server/metrics/ops_metrics_collector.test.ts b/src/core/server/metrics/ops_metrics_collector.test.ts
index 559588db60a425..9e76895b14578d 100644
--- a/src/core/server/metrics/ops_metrics_collector.test.ts
+++ b/src/core/server/metrics/ops_metrics_collector.test.ts
@@ -29,7 +29,7 @@ describe('OpsMetricsCollector', () => {
let collector: OpsMetricsCollector;
beforeEach(() => {
- const hapiServer = httpServiceMock.createSetupContract().server;
+ const hapiServer = httpServiceMock.createInternalSetupContract().server;
collector = new OpsMetricsCollector(hapiServer);
mockOsCollector.collect.mockResolvedValue('osMetrics');
diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts
index b6e9ffef6f3f13..f3ae5462f16316 100644
--- a/src/core/server/mocks.ts
+++ b/src/core/server/mocks.ts
@@ -19,7 +19,6 @@
import { of } from 'rxjs';
import { duration } from 'moment';
import { PluginInitializerContext, CoreSetup, CoreStart, StartServicesAccessor } from '.';
-import { CspConfig } from './csp';
import { loggingServiceMock } from './logging/logging_service.mock';
import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock';
import { httpServiceMock } from './http/http_service.mock';
@@ -112,26 +111,10 @@ function createCoreSetupMock({
pluginStartDeps?: object;
pluginStartContract?: any;
} = {}) {
- const httpService = httpServiceMock.createSetupContract();
const httpMock: jest.Mocked = {
- createCookieSessionStorageFactory: httpService.createCookieSessionStorageFactory,
- registerOnPreAuth: httpService.registerOnPreAuth,
- registerAuth: httpService.registerAuth,
- registerOnPostAuth: httpService.registerOnPostAuth,
- registerOnPreResponse: httpService.registerOnPreResponse,
- basePath: httpService.basePath,
- csp: CspConfig.DEFAULT,
- isTlsEnabled: httpService.isTlsEnabled,
- createRouter: jest.fn(),
- registerRouteHandlerContext: jest.fn(),
- auth: {
- get: httpService.auth.get,
- isAuthenticated: httpService.auth.isAuthenticated,
- },
+ ...httpServiceMock.createSetupContract(),
resources: httpResourcesMock.createRegistrar(),
- getServerInfo: httpService.getServerInfo,
};
- httpMock.createRouter.mockImplementation(() => httpService.createRouter(''));
const uiSettingsMock = {
register: uiSettingsServiceMock.createSetupContract().register,
@@ -159,6 +142,7 @@ function createCoreStartMock() {
const mock: MockedKeys = {
capabilities: capabilitiesServiceMock.createStartContract(),
elasticsearch: elasticsearchServiceMock.createStart(),
+ http: httpServiceMock.createStartContract(),
savedObjects: savedObjectsServiceMock.createStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
};
@@ -171,7 +155,7 @@ function createInternalCoreSetupMock() {
capabilities: capabilitiesServiceMock.createSetupContract(),
context: contextServiceMock.createSetupContract(),
elasticsearch: elasticsearchServiceMock.createInternalSetup(),
- http: httpServiceMock.createSetupContract(),
+ http: httpServiceMock.createInternalSetupContract(),
metrics: metricsServiceMock.createInternalSetupContract(),
savedObjects: savedObjectsServiceMock.createInternalSetupContract(),
status: statusServiceMock.createInternalSetupContract(),
@@ -187,6 +171,7 @@ function createInternalCoreStartMock() {
const startDeps: InternalCoreStart = {
capabilities: capabilitiesServiceMock.createStartContract(),
elasticsearch: elasticsearchServiceMock.createStart(),
+ http: httpServiceMock.createInternalStartContract(),
savedObjects: savedObjectsServiceMock.createInternalStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
};
diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts
index f0db3a25e313d9..31e36db49223a7 100644
--- a/src/core/server/plugins/plugin_context.ts
+++ b/src/core/server/plugins/plugin_context.ts
@@ -164,7 +164,6 @@ export function createPluginSetupContext(
basePath: deps.http.basePath,
auth: { get: deps.http.auth.get, isAuthenticated: deps.http.auth.isAuthenticated },
csp: deps.http.csp,
- isTlsEnabled: deps.http.isTlsEnabled,
getServerInfo: deps.http.getServerInfo,
},
metrics: {
@@ -211,6 +210,11 @@ export function createPluginStartContext(
resolveCapabilities: deps.capabilities.resolveCapabilities,
},
elasticsearch: deps.elasticsearch,
+ http: {
+ auth: deps.http.auth,
+ basePath: deps.http.basePath,
+ getServerInfo: deps.http.getServerInfo,
+ },
savedObjects: {
getScopedClient: deps.savedObjects.getScopedClient,
createInternalRepository: deps.savedObjects.createInternalRepository,
diff --git a/src/core/server/rendering/__mocks__/params.ts b/src/core/server/rendering/__mocks__/params.ts
index 3e668b3f26ab59..ce2eea119d1bb3 100644
--- a/src/core/server/rendering/__mocks__/params.ts
+++ b/src/core/server/rendering/__mocks__/params.ts
@@ -23,7 +23,7 @@ import { pluginServiceMock } from '../../plugins/plugins_service.mock';
import { legacyServiceMock } from '../../legacy/legacy_service.mock';
const context = mockCoreContext.create();
-const http = httpServiceMock.createSetupContract();
+const http = httpServiceMock.createInternalSetupContract();
const uiPlugins = pluginServiceMock.createUiPlugins();
const legacyPlugins = legacyServiceMock.createDiscoverPlugins();
diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts
index 32485f461f59b9..5da2235828b5c8 100644
--- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts
+++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts
@@ -47,6 +47,7 @@ describe('getSortedObjectsForExport()', () => {
id: '2',
type: 'search',
attributes: {},
+ score: 1,
references: [
{
name: 'name',
@@ -59,6 +60,7 @@ describe('getSortedObjectsForExport()', () => {
id: '1',
type: 'index-pattern',
attributes: {},
+ score: 1,
references: [],
},
],
@@ -133,6 +135,7 @@ describe('getSortedObjectsForExport()', () => {
id: '2',
type: 'search',
attributes: {},
+ score: 1,
references: [
{
name: 'name',
@@ -145,6 +148,7 @@ describe('getSortedObjectsForExport()', () => {
id: '1',
type: 'index-pattern',
attributes: {},
+ score: 1,
references: [],
},
],
@@ -192,6 +196,7 @@ describe('getSortedObjectsForExport()', () => {
id: '2',
type: 'search',
attributes: {},
+ score: 1,
references: [
{
name: 'name',
@@ -204,6 +209,7 @@ describe('getSortedObjectsForExport()', () => {
id: '1',
type: 'index-pattern',
attributes: {},
+ score: 1,
references: [],
},
],
@@ -279,6 +285,7 @@ describe('getSortedObjectsForExport()', () => {
id: '2',
type: 'search',
attributes: {},
+ score: 1,
references: [
{
name: 'name',
@@ -291,6 +298,7 @@ describe('getSortedObjectsForExport()', () => {
id: '1',
type: 'index-pattern',
attributes: {},
+ score: 1,
references: [],
},
],
@@ -366,6 +374,7 @@ describe('getSortedObjectsForExport()', () => {
id: '2',
type: 'search',
attributes: {},
+ score: 1,
references: [
{
type: 'index-pattern',
@@ -378,6 +387,7 @@ describe('getSortedObjectsForExport()', () => {
id: '1',
type: 'index-pattern',
attributes: {},
+ score: 1,
references: [],
},
],
@@ -405,6 +415,7 @@ describe('getSortedObjectsForExport()', () => {
attributes: {
name: 'baz',
},
+ score: 1,
references: [],
},
{
@@ -413,6 +424,7 @@ describe('getSortedObjectsForExport()', () => {
attributes: {
name: 'foo',
},
+ score: 1,
references: [],
},
{
@@ -421,6 +433,7 @@ describe('getSortedObjectsForExport()', () => {
attributes: {
name: 'bar',
},
+ score: 1,
references: [],
},
],
diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts
index cafaa5a3147db3..6e985c25aeaef9 100644
--- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts
+++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts
@@ -116,8 +116,11 @@ async function fetchObjectsToExport({
}
// sorts server-side by _id, since it's only available in fielddata
- return findResponse.saved_objects.sort((a: SavedObject, b: SavedObject) =>
- a.id > b.id ? 1 : -1
+ return (
+ findResponse.saved_objects
+ // exclude the find-specific `score` property from the exported objects
+ .map(({ score, ...obj }) => obj)
+ .sort((a: SavedObject, b: SavedObject) => (a.id > b.id ? 1 : -1))
);
} else {
throw Boom.badRequest('Either `type` or `objects` are required.');
diff --git a/src/core/server/saved_objects/routes/integration_tests/find.test.ts b/src/core/server/saved_objects/routes/integration_tests/find.test.ts
index 31bda1d6b9cbd2..33e12dd4e517dd 100644
--- a/src/core/server/saved_objects/routes/integration_tests/find.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/find.test.ts
@@ -79,6 +79,7 @@ describe('GET /api/saved_objects/_find', () => {
timeFieldName: '@timestamp',
notExpandable: true,
attributes: {},
+ score: 1,
references: [],
},
{
@@ -88,6 +89,7 @@ describe('GET /api/saved_objects/_find', () => {
timeFieldName: '@timestamp',
notExpandable: true,
attributes: {},
+ score: 1,
references: [],
},
],
diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts
index 9fba2728003d23..e8b2cf0b583b1b 100644
--- a/src/core/server/saved_objects/saved_objects_service.test.ts
+++ b/src/core/server/saved_objects/saved_objects_service.test.ts
@@ -61,7 +61,7 @@ describe('SavedObjectsService', () => {
const createSetupDeps = () => {
const elasticsearchMock = elasticsearchServiceMock.createInternalSetup();
return {
- http: httpServiceMock.createSetupContract(),
+ http: httpServiceMock.createInternalSetupContract(),
elasticsearch: elasticsearchMock,
legacyPlugins: legacyServiceMock.createDiscoverPlugins(),
};
diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js
index d631ef9cb353cc..ea749235cbb41b 100644
--- a/src/core/server/saved_objects/service/lib/repository.test.js
+++ b/src/core/server/saved_objects/service/lib/repository.test.js
@@ -1939,7 +1939,7 @@ describe('SavedObjectsRepository', () => {
{
_index: '.kibana',
_id: `${namespace ? `${namespace}:` : ''}config:6.0.0-alpha1`,
- _score: 1,
+ _score: 2,
...mockVersionProps,
_source: {
namespace,
@@ -1954,7 +1954,7 @@ describe('SavedObjectsRepository', () => {
{
_index: '.kibana',
_id: `${namespace ? `${namespace}:` : ''}index-pattern:stocks-*`,
- _score: 1,
+ _score: 3,
...mockVersionProps,
_source: {
namespace,
@@ -1970,7 +1970,7 @@ describe('SavedObjectsRepository', () => {
{
_index: '.kibana',
_id: `${NAMESPACE_AGNOSTIC_TYPE}:something`,
- _score: 1,
+ _score: 4,
...mockVersionProps,
_source: {
type: NAMESPACE_AGNOSTIC_TYPE,
@@ -2131,6 +2131,7 @@ describe('SavedObjectsRepository', () => {
type: doc._source.type,
...mockTimestampFields,
version: mockVersion,
+ score: doc._score,
attributes: doc._source[doc._source.type],
references: [],
});
@@ -2153,6 +2154,7 @@ describe('SavedObjectsRepository', () => {
type: doc._source.type,
...mockTimestampFields,
version: mockVersion,
+ score: doc._score,
attributes: doc._source[doc._source.type],
references: [],
});
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index 03538f23948459..40c5282a77e499 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -41,6 +41,7 @@ import {
SavedObjectsBulkUpdateResponse,
SavedObjectsCreateOptions,
SavedObjectsFindResponse,
+ SavedObjectsFindResult,
SavedObjectsUpdateOptions,
SavedObjectsUpdateResponse,
SavedObjectsBulkUpdateObject,
@@ -674,8 +675,11 @@ export class SavedObjectsRepository {
page,
per_page: perPage,
total: response.hits.total,
- saved_objects: response.hits.hits.map((hit: SavedObjectsRawDoc) =>
- this._rawToSavedObject(hit)
+ saved_objects: response.hits.hits.map(
+ (hit: SavedObjectsRawDoc): SavedObjectsFindResult => ({
+ ...this._rawToSavedObject(hit),
+ score: (hit as any)._score,
+ })
),
};
}
diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts
index 8780f07cc3091b..e15a92c92772f3 100644
--- a/src/core/server/saved_objects/service/saved_objects_client.ts
+++ b/src/core/server/saved_objects/service/saved_objects_client.ts
@@ -79,6 +79,17 @@ export interface SavedObjectsBulkResponse {
saved_objects: Array>;
}
+/**
+ *
+ * @public
+ */
+export interface SavedObjectsFindResult extends SavedObject {
+ /**
+ * The Elasticsearch `_score` of this result.
+ */
+ score: number;
+}
+
/**
* Return type of the Saved Objects `find()` method.
*
@@ -88,7 +99,7 @@ export interface SavedObjectsBulkResponse {
* @public
*/
export interface SavedObjectsFindResponse {
- saved_objects: Array>;
+ saved_objects: Array>;
total: number;
per_page: number;
page: number;
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index ecfa09fbd37f39..9dc3ac9b94d96d 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -657,6 +657,8 @@ export interface CoreStart {
// (undocumented)
elasticsearch: ElasticsearchServiceStart;
// (undocumented)
+ http: HttpServiceStart;
+ // (undocumented)
savedObjects: SavedObjectsServiceStart;
// (undocumented)
uiSettings: UiSettingsServiceStart;
@@ -905,6 +907,12 @@ export type Headers = {
[header: string]: string | string[] | undefined;
};
+// @public (undocumented)
+export interface HttpAuth {
+ get: GetAuthState;
+ isAuthenticated: IsAuthenticated;
+}
+
// @public
export interface HttpResources {
register:
(route: RouteConfig
, handler: HttpResourcesRequestHandler
) => 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
@@ -2039,11 +2045,16 @@ export interface SavedObjectsFindResponse {
// (undocumented)
per_page: number;
// (undocumented)
- saved_objects: Array>;
+ saved_objects: Array>;
// (undocumented)
total: number;
}
+// @public (undocumented)
+export interface SavedObjectsFindResult extends SavedObject {
+ score: number;
+}
+
// @public
export interface SavedObjectsImportConflictError {
// (undocumented)
diff --git a/src/core/server/server.ts b/src/core/server/server.ts
index 6ca580083648f8..ae1a02cf71b886 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/ui_settings/ui_settings_service.test.ts b/src/core/server/ui_settings/ui_settings_service.test.ts
index ebcb0cf1d762fb..096ca347e6f4b9 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/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
index d1fb544de733c3..745a3d1f0c8307 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
@@ -235,6 +235,8 @@ kibana_vars=(
xpack.security.session.lifespan
xpack.security.loginAssistanceMessage
xpack.security.loginHelp
+ xpack.spaces.enabled
+ xpack.spaces.maxSpaces
telemetry.allowChangingOptInStatus
telemetry.enabled
telemetry.optIn
diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts
index 2f785896da8d55..85bfd4a7a4d260 100644
--- a/src/dev/storybook/aliases.ts
+++ b/src/dev/storybook/aliases.ts
@@ -22,7 +22,6 @@ export const storybookAliases = {
canvas: 'x-pack/plugins/canvas/scripts/storybook_new.js',
codeeditor: 'src/plugins/kibana_react/public/code_editor/scripts/storybook.ts',
dashboard_enhanced: 'x-pack/plugins/dashboard_enhanced/scripts/storybook.js',
- drilldowns: 'x-pack/plugins/drilldowns/scripts/storybook.js',
embeddable: 'src/plugins/embeddable/scripts/storybook.js',
infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js',
security_solution: 'x-pack/plugins/security_solution/scripts/storybook.js',
diff --git a/src/es_archiver/es_archiver.ts b/src/es_archiver/es_archiver.ts
index f36cbb3f516b94..e335652195b863 100644
--- a/src/es_archiver/es_archiver.ts
+++ b/src/es_archiver/es_archiver.ts
@@ -49,7 +49,7 @@ export class EsArchiver {
this.client = client;
this.dataDir = dataDir;
this.log = log;
- this.kbnClient = new KbnClient(log, [kibanaUrl]);
+ this.kbnClient = new KbnClient(log, { url: kibanaUrl });
}
/**
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 1cc626cfa668b7..bd5932e88b5e9e 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/index.ts b/src/plugins/dashboard/public/index.ts
index 28606b7dd9784c..17968dd0281e6c 100644
--- a/src/plugins/dashboard/public/index.ts
+++ b/src/plugins/dashboard/public/index.ts
@@ -32,8 +32,10 @@ export {
export { DashboardConstants, createDashboardEditUrl } from './dashboard_constants';
export { DashboardStart, DashboardUrlGenerator } from './plugin';
-export { DASHBOARD_APP_URL_GENERATOR } from './url_generator';
+export { DASHBOARD_APP_URL_GENERATOR, createDashboardUrlGenerator } from './url_generator';
export { addEmbeddableToDashboardUrl } from './url_utils/url_helper';
+export { SavedObjectDashboard } from './saved_dashboards';
+export { SavedDashboardPanel } from './types';
export function plugin(initializerContext: PluginInitializerContext) {
return new DashboardPlugin(initializerContext);
diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts
index d6805b2d94119e..188de7fd857be8 100644
--- a/src/plugins/dashboard/public/url_generator.ts
+++ b/src/plugins/dashboard/public/url_generator.ts
@@ -28,6 +28,7 @@ import {
import { setStateToKbnUrl } from '../../kibana_utils/public';
import { UrlGeneratorsDefinition, UrlGeneratorState } from '../../share/public';
import { SavedObjectLoader } from '../../saved_objects/public';
+import { ViewMode } from '../../embeddable/public';
export const STATE_STORAGE_KEY = '_a';
export const GLOBAL_STATE_STORAGE_KEY = '_g';
@@ -73,6 +74,11 @@ export type DashboardAppLinkGeneratorState = UrlGeneratorState<{
* true is default
*/
preserveSavedFilters?: boolean;
+
+ /**
+ * View mode of the dashboard.
+ */
+ viewMode?: ViewMode;
}>;
export const createDashboardUrlGenerator = (
@@ -123,6 +129,7 @@ export const createDashboardUrlGenerator = (
cleanEmptyKeys({
query: state.query,
filters: filters?.filter((f) => !esFilters.isFilterPinned(f)),
+ viewMode: state.viewMode,
}),
{ useHash },
`${appBasePath}#/${hash}`
diff --git a/src/plugins/embeddable/docs/README.md b/src/plugins/embeddable/docs/README.md
index 1b6c7be13b1d42..ce5e76d54a0469 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 c950bef96002ae..35e399f89c1313 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 00000000000000..810dc72664f96d
--- /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/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx
index 8ba7be7880a7bd..7b66f29cc27267 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx
@@ -81,7 +81,7 @@ function renderNotifications(
if (tooltip) {
badge = (
-
+
{badge}
);
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 69a8cc86f1f736..53cdb5885c730e 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 00000000000000..56c792b89049b7
--- /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 00000000000000..cdb332e9e9130d
--- /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 00000000000000..5667220881df2c
--- /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 00000000000000..3e0e9cf897b5d2
--- /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 00000000000000..c073c188a6ad6e
--- /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 00000000000000..b1cb11735a1103
--- /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 00000000000000..96140c9b461851
--- /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 00000000000000..08c37c20b5bf6c
--- /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 00000000000000..a7df0e386d1739
--- /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 00000000000000..5fbe3d2bbbdd4c
--- /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 00000000000000..0a2c7bb6519593
--- /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 00000000000000..e69ce4c6fa1454
--- /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 4ab791289dd886..28baa3d8372f0d 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 1c67f332a12ab1..bdf1f075967cba 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 {}}
/>
@@ -224,7 +223,6 @@ exports[`should render a Welcome screen with the telemetry disclaimer when optIn
values={Object {}}
/>
@@ -323,7 +321,6 @@ exports[`should render a Welcome screen with the telemetry disclaimer when optIn
values={Object {}}
/>
diff --git a/src/plugins/home/public/application/components/welcome.test.tsx b/src/plugins/home/public/application/components/welcome.test.tsx
index 1332e03ffdc81f..701fab3af75390 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 d4dcaca317806e..f82bd024b80b83 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 {
@@ -162,7 +161,11 @@ export class Welcome extends React.Component {
id="home.dataManagementDisclaimerPrivacy"
defaultMessage="To learn about how usage data helps us manage and improve our products and services, see our "
/>
-
+ {
let savedObjectsClient: ReturnType;
- const createObj = (id: number): SavedObject => ({
+ const createObj = (id: number): SavedObjectsFindResult => ({
type: 'type',
id: `id-${id}`,
attributes: {},
+ score: 1,
references: [],
});
diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts
index 2c8997c9af21ab..e18a45d9bdf44c 100644
--- a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts
+++ b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts
@@ -77,6 +77,7 @@ describe('findRelationships', () => {
type: 'parent-type',
id: 'parent-id',
attributes: {},
+ score: 1,
references: [],
},
],
diff --git a/src/plugins/telemetry/public/mocks.ts b/src/plugins/telemetry/public/mocks.ts
index 9ec4a3ae86cc73..dd7e5a4cc4ce30 100644
--- a/src/plugins/telemetry/public/mocks.ts
+++ b/src/plugins/telemetry/public/mocks.ts
@@ -25,7 +25,7 @@ import { httpServiceMock } from '../../../core/public/http/http_service.mock';
import { notificationServiceMock } from '../../../core/public/notifications/notifications_service.mock';
import { TelemetryService } from './services/telemetry_service';
import { TelemetryNotifications } from './services/telemetry_notifications/telemetry_notifications';
-import { TelemetryPluginStart, TelemetryPluginConfig } from './plugin';
+import { TelemetryPluginStart, TelemetryPluginSetup, TelemetryPluginConfig } from './plugin';
// The following is to be able to access private methods
/* eslint-disable dot-notation */
@@ -77,20 +77,35 @@ export function mockTelemetryNotifications({
});
}
-export type Setup = jest.Mocked;
+export type Setup = jest.Mocked;
+export type Start = jest.Mocked;
export const telemetryPluginMock = {
createSetupContract,
+ createStartContract,
};
function createSetupContract(): Setup {
const telemetryService = mockTelemetryService();
- const telemetryNotifications = mockTelemetryNotifications({ telemetryService });
const setupContract: Setup = {
telemetryService,
- telemetryNotifications,
};
return setupContract;
}
+
+function createStartContract(): Start {
+ const telemetryService = mockTelemetryService();
+ const telemetryNotifications = mockTelemetryNotifications({ telemetryService });
+
+ const startContract: Start = {
+ telemetryService,
+ telemetryNotifications,
+ telemetryConstants: {
+ getPrivacyStatementUrl: jest.fn(),
+ },
+ };
+
+ return startContract;
+}
diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts
index a363953978d79d..3846e7cb96a191 100644
--- a/src/plugins/telemetry/public/plugin.ts
+++ b/src/plugins/telemetry/public/plugin.ts
@@ -38,6 +38,7 @@ import {
getTelemetrySendUsageFrom,
} from '../common/telemetry_config';
import { getNotifyUserAboutOptInDefault } from '../common/telemetry_config/get_telemetry_notify_user_about_optin_default';
+import { PRIVACY_STATEMENT_URL } from '../common/constants';
export interface TelemetryPluginSetup {
telemetryService: TelemetryService;
@@ -46,6 +47,9 @@ export interface TelemetryPluginSetup {
export interface TelemetryPluginStart {
telemetryService: TelemetryService;
telemetryNotifications: TelemetryNotifications;
+ telemetryConstants: {
+ getPrivacyStatementUrl: () => string;
+ };
}
export interface TelemetryPluginConfig {
@@ -115,6 +119,9 @@ export class TelemetryPlugin implements Plugin PRIVACY_STATEMENT_URL,
+ },
};
}
diff --git a/test/accessibility/services/a11y/a11y.ts b/test/accessibility/services/a11y/a11y.ts
index 525445c60818ad..35cef2b81d64ea 100644
--- a/test/accessibility/services/a11y/a11y.ts
+++ b/test/accessibility/services/a11y/a11y.ts
@@ -99,6 +99,9 @@ export function A11yProvider({ getService }: FtrProviderContext) {
'color-contrast': {
enabled: false,
},
+ bypass: {
+ enabled: false, // disabled because it's too flaky
+ },
},
};
diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js
index 7a57d182bc8124..7cb5955e4a43d9 100644
--- a/test/api_integration/apis/saved_objects/find.js
+++ b/test/api_integration/apis/saved_objects/find.js
@@ -46,6 +46,7 @@ export default function ({ getService }) {
attributes: {
title: 'Count of requests',
},
+ score: 0,
migrationVersion: resp.body.saved_objects[0].migrationVersion,
references: [
{
@@ -134,6 +135,7 @@ export default function ({ getService }) {
.searchSourceJSON,
},
},
+ score: 0,
references: [
{
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts
index e15a9e989d21ff..4d9f1c1658139e 100644
--- a/test/api_integration/apis/saved_objects_management/find.ts
+++ b/test/api_integration/apis/saved_objects_management/find.ts
@@ -56,6 +56,7 @@ export default function ({ getService }: FtrProviderContext) {
type: 'index-pattern',
},
],
+ score: 0,
updated_at: '2017-09-21T18:51:23.794Z',
meta: {
editUrl:
diff --git a/test/common/services/kibana_server/kibana_server.ts b/test/common/services/kibana_server/kibana_server.ts
index 16039d6fee8335..4a251cca044d3c 100644
--- a/test/common/services/kibana_server/kibana_server.ts
+++ b/test/common/services/kibana_server/kibana_server.ts
@@ -27,9 +27,9 @@ export function KibanaServerProvider({ getService }: FtrProviderContext) {
const config = getService('config');
const lifecycle = getService('lifecycle');
const url = Url.format(config.get('servers.kibana'));
+ const ssl = config.get('servers.kibana').ssl;
const defaults = config.get('uiSettings.defaults');
-
- const kbn = new KbnClient(log, [url], defaults);
+ const kbn = new KbnClient(log, { url, ssl }, defaults);
if (defaults) {
lifecycle.beforeTests.add(async () => {
diff --git a/test/common/services/security/role.ts b/test/common/services/security/role.ts
index dfc6ff9b164e50..caa5549a70f0c4 100644
--- a/test/common/services/security/role.ts
+++ b/test/common/services/security/role.ts
@@ -17,27 +17,20 @@
* under the License.
*/
-import axios, { AxiosInstance } from 'axios';
import util from 'util';
-import { ToolingLog } from '@kbn/dev-utils';
+import { KbnClient, ToolingLog } from '@kbn/dev-utils';
export class Role {
- private log: ToolingLog;
- private axios: AxiosInstance;
-
- constructor(url: string, log: ToolingLog) {
- this.log = log;
- this.axios = axios.create({
- headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/role' },
- baseURL: url,
- maxRedirects: 0,
- validateStatus: () => true, // we do our own validation below and throw better error messages
- });
- }
+ constructor(private log: ToolingLog, private kibanaServer: KbnClient) {}
public async create(name: string, role: any) {
this.log.debug(`creating role ${name}`);
- const { data, status, statusText } = await this.axios.put(`/api/security/role/${name}`, role);
+ const { data, status, statusText } = await this.kibanaServer.request({
+ path: `/api/security/role/${name}`,
+ method: 'PUT',
+ body: role,
+ retries: 0,
+ });
if (status !== 204) {
throw new Error(
`Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}`
@@ -47,7 +40,10 @@ export class Role {
public async delete(name: string) {
this.log.debug(`deleting role ${name}`);
- const { data, status, statusText } = await this.axios.delete(`/api/security/role/${name}`);
+ const { data, status, statusText } = await this.kibanaServer.request({
+ path: `/api/security/role/${name}`,
+ method: 'DELETE',
+ });
if (status !== 204 && status !== 404) {
throw new Error(
`Expected status code of 204 or 404, received ${status} ${statusText}: ${util.inspect(
diff --git a/test/common/services/security/role_mappings.ts b/test/common/services/security/role_mappings.ts
index cc2fa238254987..7951d4b5b47b27 100644
--- a/test/common/services/security/role_mappings.ts
+++ b/test/common/services/security/role_mappings.ts
@@ -17,30 +17,19 @@
* under the License.
*/
-import axios, { AxiosInstance } from 'axios';
import util from 'util';
-import { ToolingLog } from '@kbn/dev-utils';
+import { KbnClient, ToolingLog } from '@kbn/dev-utils';
export class RoleMappings {
- private log: ToolingLog;
- private axios: AxiosInstance;
-
- constructor(url: string, log: ToolingLog) {
- this.log = log;
- this.axios = axios.create({
- headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/role_mappings' },
- baseURL: url,
- maxRedirects: 0,
- validateStatus: () => true, // we do our own validation below and throw better error messages
- });
- }
+ constructor(private log: ToolingLog, private kbnClient: KbnClient) {}
public async create(name: string, roleMapping: Record) {
this.log.debug(`creating role mapping ${name}`);
- const { data, status, statusText } = await this.axios.post(
- `/internal/security/role_mapping/${name}`,
- roleMapping
- );
+ const { data, status, statusText } = await this.kbnClient.request({
+ path: `/internal/security/role_mapping/${name}`,
+ method: 'POST',
+ body: roleMapping,
+ });
if (status !== 200) {
throw new Error(
`Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}`
@@ -51,9 +40,10 @@ export class RoleMappings {
public async delete(name: string) {
this.log.debug(`deleting role mapping ${name}`);
- const { data, status, statusText } = await this.axios.delete(
- `/internal/security/role_mapping/${name}`
- );
+ const { data, status, statusText } = await this.kbnClient.request({
+ path: `/internal/security/role_mapping/${name}`,
+ method: 'DELETE',
+ });
if (status !== 200 && status !== 404) {
throw new Error(
`Expected status code of 200 or 404, received ${status} ${statusText}: ${util.inspect(
diff --git a/test/common/services/security/security.ts b/test/common/services/security/security.ts
index 6ad0933a2a5a23..fae4c9198cab6d 100644
--- a/test/common/services/security/security.ts
+++ b/test/common/services/security/security.ts
@@ -17,8 +17,6 @@
* under the License.
*/
-import { format as formatUrl } from 'url';
-
import { Role } from './role';
import { User } from './user';
import { RoleMappings } from './role_mappings';
@@ -28,14 +26,14 @@ import { createTestUserService } from './test_user';
export async function SecurityServiceProvider(context: FtrProviderContext) {
const { getService } = context;
const log = getService('log');
- const config = getService('config');
- const url = formatUrl(config.get('servers.kibana'));
- const role = new Role(url, log);
- const user = new User(url, log);
+ const kibanaServer = getService('kibanaServer');
+
+ const role = new Role(log, kibanaServer);
+ const user = new User(log, kibanaServer);
const testUser = await createTestUserService(role, user, context);
return new (class SecurityService {
- roleMappings = new RoleMappings(url, log);
+ roleMappings = new RoleMappings(log, kibanaServer);
testUser = testUser;
role = role;
user = user;
diff --git a/test/common/services/security/user.ts b/test/common/services/security/user.ts
index ae02127043234c..58c4d0f1cf34ea 100644
--- a/test/common/services/security/user.ts
+++ b/test/common/services/security/user.ts
@@ -17,33 +17,22 @@
* under the License.
*/
-import axios, { AxiosInstance } from 'axios';
import util from 'util';
-import { ToolingLog } from '@kbn/dev-utils';
+import { KbnClient, ToolingLog } from '@kbn/dev-utils';
export class User {
- private log: ToolingLog;
- private axios: AxiosInstance;
-
- constructor(url: string, log: ToolingLog) {
- this.log = log;
- this.axios = axios.create({
- headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/user' },
- baseURL: url,
- maxRedirects: 0,
- validateStatus: () => true, // we do our own validation below and throw better error messages
- });
- }
+ constructor(private log: ToolingLog, private kbnClient: KbnClient) {}
public async create(username: string, user: any) {
this.log.debug(`creating user ${username}`);
- const { data, status, statusText } = await this.axios.post(
- `/internal/security/users/${username}`,
- {
+ const { data, status, statusText } = await this.kbnClient.request({
+ path: `/internal/security/users/${username}`,
+ method: 'POST',
+ body: {
username,
...user,
- }
- );
+ },
+ });
if (status !== 200) {
throw new Error(
`Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}`
@@ -54,9 +43,10 @@ export class User {
public async delete(username: string) {
this.log.debug(`deleting user ${username}`);
- const { data, status, statusText } = await this.axios.delete(
- `/internal/security/users/${username}`
- );
+ const { data, status, statusText } = await await this.kbnClient.request({
+ path: `/internal/security/users/${username}`,
+ method: 'DELETE',
+ });
if (status !== 204) {
throw new Error(
`Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}`
diff --git a/test/functional/apps/dashboard/dashboard_snapshots.js b/test/functional/apps/dashboard/dashboard_snapshots.js
index 787e839aa08a5c..20bc30c889d651 100644
--- a/test/functional/apps/dashboard/dashboard_snapshots.js
+++ b/test/functional/apps/dashboard/dashboard_snapshots.js
@@ -28,7 +28,8 @@ export default function ({ getService, getPageObjects, updateBaselines }) {
const dashboardPanelActions = getService('dashboardPanelActions');
const dashboardAddPanel = getService('dashboardAddPanel');
- describe('dashboard snapshots', function describeIndexTests() {
+ // FLAKY: https://github.com/elastic/kibana/issues/52854
+ describe.skip('dashboard snapshots', function describeIndexTests() {
before(async function () {
await esArchiver.load('dashboard/current/kibana');
await kibanaServer.uiSettings.replace({
diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts
index 3297f6e094f7c8..d6a4fc91481de2 100644
--- a/test/functional/services/common/browser.ts
+++ b/test/functional/services/common/browser.ts
@@ -529,5 +529,10 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
await driver.executeScript('document.body.scrollLeft = ' + scrollSize);
return this.getScrollLeft();
}
+
+ public async switchToFrame(idOrElement: number | WebElementWrapper) {
+ const _id = idOrElement instanceof WebElementWrapper ? idOrElement._webElement : idOrElement;
+ await driver.switchTo().frame(_id);
+ }
})();
}
diff --git a/test/functional/services/remote/remote.ts b/test/functional/services/remote/remote.ts
index 5a3a775cae0c50..99643929c4682c 100644
--- a/test/functional/services/remote/remote.ts
+++ b/test/functional/services/remote/remote.ts
@@ -23,7 +23,7 @@ import { resolve } from 'path';
import { mergeMap } from 'rxjs/operators';
import { FtrProviderContext } from '../../ftr_provider_context';
-import { initWebDriver } from './webdriver';
+import { initWebDriver, BrowserConfig } from './webdriver';
import { Browsers } from './browsers';
export async function RemoteProvider({ getService }: FtrProviderContext) {
@@ -58,12 +58,12 @@ export async function RemoteProvider({ getService }: FtrProviderContext) {
Fs.writeFileSync(path, JSON.stringify(JSON.parse(coverageJson), null, 2));
};
- const { driver, consoleLog$ } = await initWebDriver(
- log,
- browserType,
- lifecycle,
- config.get('browser.logPollingMs')
- );
+ const browserConfig: BrowserConfig = {
+ logPollingMs: config.get('browser.logPollingMs'),
+ acceptInsecureCerts: config.get('browser.acceptInsecureCerts'),
+ };
+
+ const { driver, consoleLog$ } = await initWebDriver(log, browserType, lifecycle, browserConfig);
const isW3CEnabled = (driver as any).executor_.w3c;
const caps = await driver.getCapabilities();
diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts
index 9fbbf28bbf42cb..27814060e70c1d 100644
--- a/test/functional/services/remote/webdriver.ts
+++ b/test/functional/services/remote/webdriver.ts
@@ -73,13 +73,18 @@ Executor.prototype.execute = preventParallelCalls(
(command: { getName: () => string }) => NO_QUEUE_COMMANDS.includes(command.getName())
);
+export interface BrowserConfig {
+ logPollingMs: number;
+ acceptInsecureCerts: boolean;
+}
+
let attemptCounter = 0;
let edgePaths: { driverPath: string | undefined; browserPath: string | undefined };
async function attemptToCreateCommand(
log: ToolingLog,
browserType: Browsers,
lifecycle: Lifecycle,
- logPollingMs: number
+ config: BrowserConfig
) {
const attemptId = ++attemptCounter;
log.debug('[webdriver] Creating session');
@@ -114,6 +119,7 @@ async function attemptToCreateCommand(
if (certValidation === '0') {
chromeOptions.push('ignore-certificate-errors');
}
+
if (remoteDebug === '1') {
// Visit chrome://inspect in chrome to remotely view/debug
chromeOptions.push('headless', 'disable-gpu', 'remote-debugging-port=9222');
@@ -125,6 +131,7 @@ async function attemptToCreateCommand(
});
chromeCapabilities.set('unexpectedAlertBehaviour', 'accept');
chromeCapabilities.set('goog:loggingPrefs', { browser: 'ALL' });
+ chromeCapabilities.setAcceptInsecureCerts(config.acceptInsecureCerts);
const session = await new Builder()
.forBrowser(browserType)
@@ -137,7 +144,7 @@ async function attemptToCreateCommand(
consoleLog$: pollForLogEntry$(
session,
logging.Type.BROWSER,
- logPollingMs,
+ config.logPollingMs,
lifecycle.cleanup.after$
).pipe(
takeUntil(lifecycle.cleanup.after$),
@@ -174,7 +181,7 @@ async function attemptToCreateCommand(
consoleLog$: pollForLogEntry$(
session,
logging.Type.BROWSER,
- logPollingMs,
+ config.logPollingMs,
lifecycle.cleanup.after$
).pipe(
takeUntil(lifecycle.cleanup.after$),
@@ -206,6 +213,7 @@ async function attemptToCreateCommand(
'browser.helperApps.neverAsk.saveToDisk',
'application/comma-separated-values, text/csv, text/plain'
);
+ firefoxOptions.setAcceptInsecureCerts(config.acceptInsecureCerts);
if (headlessBrowser === '1') {
// See: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode
@@ -317,7 +325,7 @@ export async function initWebDriver(
log: ToolingLog,
browserType: Browsers,
lifecycle: Lifecycle,
- logPollingMs: number
+ config: BrowserConfig
) {
const logger = getLogger('webdriver.http.Executor');
logger.setLevel(logging.Level.FINEST);
@@ -348,7 +356,7 @@ export async function initWebDriver(
while (true) {
const command = await Promise.race([
delay(30 * SECOND),
- attemptToCreateCommand(log, browserType, lifecycle, logPollingMs),
+ attemptToCreateCommand(log, browserType, lifecycle, config),
]);
if (!command) {
diff --git a/test/scripts/jenkins_xpack_firefox_smoke.sh b/test/scripts/jenkins_xpack_firefox_smoke.sh
index fdaee76cafa9de..ae924a5e105527 100755
--- a/test/scripts/jenkins_xpack_firefox_smoke.sh
+++ b/test/scripts/jenkins_xpack_firefox_smoke.sh
@@ -7,4 +7,5 @@ checks-reporter-with-killswitch "X-Pack firefox smoke test" \
--debug --bail \
--kibana-install-dir "$KIBANA_INSTALL_DIR" \
--include-tag "includeFirefox" \
- --config test/functional/config.firefox.js;
+ --config test/functional/config.firefox.js \
+ --config test/functional_embedded/config.firefox.ts;
diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy
index ca9af5d2346cd2..4a5c8dff8a2301 100644
--- a/vars/kibanaPipeline.groovy
+++ b/vars/kibanaPipeline.groovy
@@ -98,7 +98,7 @@ def withGcsArtifactUpload(workerName, closure) {
def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}"
def ARTIFACT_PATTERNS = [
'target/kibana-*',
- 'target/kibana-siem/**/*.png',
+ 'target/kibana-security-solution/**/*.png',
'target/junit/**/*',
'test/**/screenshots/**/*.png',
'test/functional/failure_debug/html/*.html',
diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json
index 21b2df3ba12f84..278968cb472315 100644
--- a/x-pack/.i18nrc.json
+++ b/x-pack/.i18nrc.json
@@ -13,7 +13,6 @@
"xpack.crossClusterReplication": "plugins/cross_cluster_replication",
"xpack.dashboardMode": "legacy/plugins/dashboard_mode",
"xpack.data": "plugins/data_enhanced",
- "xpack.drilldowns": "plugins/drilldowns",
"xpack.embeddableEnhanced": "plugins/embeddable_enhanced",
"xpack.endpoint": "plugins/endpoint",
"xpack.features": "plugins/features",
diff --git a/x-pack/legacy/plugins/beats_management/common/config_block_validation.ts b/x-pack/legacy/plugins/beats_management/common/config_block_validation.ts
deleted file mode 100644
index f3d1b9164e9768..00000000000000
--- a/x-pack/legacy/plugins/beats_management/common/config_block_validation.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import * as t from 'io-ts';
-import { PathReporter } from 'io-ts/lib/PathReporter';
-import { isLeft } from 'fp-ts/lib/Either';
-import { configBlockSchemas } from './config_schemas';
-import { ConfigurationBlock, createConfigurationBlockInterface } from './domain_types';
-
-export const validateConfigurationBlocks = (configurationBlocks: ConfigurationBlock[]) => {
- const validationMap = {
- isHosts: t.array(t.string),
- isString: t.string,
- isPeriod: t.string,
- isPath: t.string,
- isPaths: t.array(t.string),
- isYaml: t.string,
- };
-
- for (const [index, block] of configurationBlocks.entries()) {
- const blockSchema = configBlockSchemas.find((s) => s.id === block.type);
- if (!blockSchema) {
- throw new Error(
- `Invalid config type of ${block.type} used in 'configuration_blocks' at index ${index}`
- );
- }
-
- const interfaceConfig = blockSchema.configs.reduce((props, config) => {
- if (config.options) {
- props[config.id] = t.keyof(
- Object.fromEntries(config.options.map((opt) => [opt.value, null])) as Record
- );
- } else if (config.validation) {
- props[config.id] = validationMap[config.validation];
- }
-
- return props;
- }, {} as t.Props);
-
- const runtimeInterface = createConfigurationBlockInterface(
- t.literal(blockSchema.id),
- t.interface(interfaceConfig)
- );
-
- const validationResults = runtimeInterface.decode(block);
-
- if (isLeft(validationResults)) {
- throw new Error(
- `configuration_blocks validation error, configuration_blocks at index ${index} is invalid. ${
- PathReporter.report(validationResults)[0]
- }`
- );
- }
- }
-};
diff --git a/x-pack/legacy/plugins/beats_management/common/config_schemas_translations_map.ts b/x-pack/legacy/plugins/beats_management/common/config_schemas_translations_map.ts
index 1aec3e80817088..7cae2a85dc4ca8 100644
--- a/x-pack/legacy/plugins/beats_management/common/config_schemas_translations_map.ts
+++ b/x-pack/legacy/plugins/beats_management/common/config_schemas_translations_map.ts
@@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
import { ConfigBlockSchema } from './domain_types';
-export const supportedConfigLabelsMap = new Map([
+const supportedConfigLabelsMap = new Map([
[
'filebeatInputConfig.paths.ui.label',
i18n.translate('xpack.beatsManagement.filebeatInputConfig.pathsLabel', {
diff --git a/x-pack/legacy/plugins/beats_management/common/domain_types.ts b/x-pack/legacy/plugins/beats_management/common/domain_types.ts
index b4a9ac8a074798..32e1d81451c652 100644
--- a/x-pack/legacy/plugins/beats_management/common/domain_types.ts
+++ b/x-pack/legacy/plugins/beats_management/common/domain_types.ts
@@ -7,8 +7,6 @@ import * as t from 'io-ts';
import { configBlockSchemas } from './config_schemas';
import { DateFromString } from './io_ts_types';
-export const OutputTypesArray = ['elasticsearch', 'logstash', 'kafka', 'redis'];
-
// Here we create the runtime check for a generic, unknown beat config type.
// We can also pass in optional params to create spacific runtime checks that
// can be used to validate blocs on the API and UI
diff --git a/x-pack/legacy/plugins/beats_management/common/io_ts_types.ts b/x-pack/legacy/plugins/beats_management/common/io_ts_types.ts
index d77ad922986995..7d71ea5ad82562 100644
--- a/x-pack/legacy/plugins/beats_management/common/io_ts_types.ts
+++ b/x-pack/legacy/plugins/beats_management/common/io_ts_types.ts
@@ -7,7 +7,7 @@
import * as t from 'io-ts';
import { isRight } from 'fp-ts/lib/Either';
-export class DateFromStringType extends t.Type {
+class DateFromStringType extends t.Type {
// eslint-disable-next-line
public readonly _tag: 'DateFromISOStringType' = 'DateFromISOStringType';
constructor() {
diff --git a/x-pack/legacy/plugins/beats_management/common/return_types.ts b/x-pack/legacy/plugins/beats_management/common/return_types.ts
index a7125795a5c7d5..7e0e39e12e60aa 100644
--- a/x-pack/legacy/plugins/beats_management/common/return_types.ts
+++ b/x-pack/legacy/plugins/beats_management/common/return_types.ts
@@ -34,11 +34,6 @@ export interface ReturnTypeBulkCreate extends BaseReturnType {
}>;
}
-// delete
-export interface ReturnTypeDelete extends BaseReturnType {
- action: 'deleted';
-}
-
export interface ReturnTypeBulkDelete extends BaseReturnType {
results: Array<{
success: boolean;
@@ -84,12 +79,6 @@ export interface ReturnTypeBulkGet extends BaseReturnType {
items: T[];
}
-// action -- e.g. validate config block. Like ES simulate endpoint
-export interface ReturnTypeAction extends BaseReturnType {
- result: {
- [key: string]: any;
- };
-}
// e.g.
// {
// result: {
diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts
deleted file mode 100644
index afae87c4901588..00000000000000
--- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { intersection, omit } from 'lodash';
-
-import { CMBeat } from '../../../../common/domain_types';
-import { FrameworkUser } from '../framework/adapter_types';
-import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types';
-
-export class MemoryBeatsAdapter implements CMBeatsAdapter {
- private beatsDB: CMBeat[];
-
- constructor(beatsDB: CMBeat[]) {
- this.beatsDB = beatsDB;
- }
-
- public async get(user: FrameworkUser, id: string) {
- return this.beatsDB.find((beat) => beat.id === id) || null;
- }
-
- public async insert(user: FrameworkUser, beat: CMBeat) {
- this.beatsDB.push(beat);
- }
-
- public async update(user: FrameworkUser, beat: CMBeat) {
- const beatIndex = this.beatsDB.findIndex((b) => b.id === beat.id);
-
- this.beatsDB[beatIndex] = {
- ...this.beatsDB[beatIndex],
- ...beat,
- };
- }
-
- public async getWithIds(user: FrameworkUser, beatIds: string[]) {
- return this.beatsDB.filter((beat) => beatIds.includes(beat.id));
- }
-
- public async getAllWithTags(user: FrameworkUser, tagIds: string[]): Promise {
- return this.beatsDB.filter((beat) => intersection(tagIds, beat.tags || []).length !== 0);
- }
-
- public async getBeatWithToken(
- user: FrameworkUser,
- enrollmentToken: string
- ): Promise {
- return this.beatsDB.find((beat) => enrollmentToken === beat.enrollment_token) || null;
- }
-
- public async getAll(user: FrameworkUser) {
- return this.beatsDB.map((beat: any) => omit(beat, ['access_token']));
- }
-
- public async removeTagsFromBeats(
- user: FrameworkUser,
- removals: BeatsTagAssignment[]
- ): Promise {
- const beatIds = removals.map((r) => r.beatId);
-
- const response = this.beatsDB
- .filter((beat) => beatIds.includes(beat.id))
- .map((beat) => {
- const tagData = removals.find((r) => r.beatId === beat.id);
- if (tagData) {
- if (beat.tags) {
- beat.tags = beat.tags.filter((tag) => tag !== tagData.tag);
- }
- }
- return beat;
- });
-
- return response.map((item: CMBeat, resultIdx: number) => ({
- idxInRequest: removals[resultIdx].idxInRequest,
- result: 'updated',
- status: 200,
- }));
- }
-
- public async assignTagsToBeats(
- user: FrameworkUser,
- assignments: BeatsTagAssignment[]
- ): Promise {
- const beatIds = assignments.map((r) => r.beatId);
-
- this.beatsDB
- .filter((beat) => beatIds.includes(beat.id))
- .map((beat) => {
- // get tags that need to be assigned to this beat
- const tags = assignments
- .filter((a) => a.beatId === beat.id)
- .map((t: BeatsTagAssignment) => t.tag);
-
- if (tags.length > 0) {
- if (!beat.tags) {
- beat.tags = [];
- }
- const nonExistingTags = tags.filter((t: string) => beat.tags && !beat.tags.includes(t));
-
- if (nonExistingTags.length > 0) {
- beat.tags = beat.tags.concat(nonExistingTags);
- }
- }
- return beat;
- });
-
- return assignments.map((item: BeatsTagAssignment, resultIdx: number) => ({
- idxInRequest: assignments[resultIdx].idxInRequest,
- result: 'updated',
- status: 200,
- }));
- }
-
- public setDB(beatsDB: CMBeat[]) {
- this.beatsDB = beatsDB;
- }
-}
diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/memory_tags_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/memory_tags_adapter.ts
deleted file mode 100644
index ea8a75c92fad27..00000000000000
--- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/memory_tags_adapter.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import Chance from 'chance'; // eslint-disable-line
-import { ConfigurationBlock } from '../../../../common/domain_types';
-import { FrameworkUser } from '../framework/adapter_types';
-import { ConfigurationBlockAdapter } from './adapter_types';
-
-const chance = new Chance();
-
-export class MemoryConfigurationBlockAdapter implements ConfigurationBlockAdapter {
- private db: ConfigurationBlock[] = [];
-
- constructor(db: ConfigurationBlock[]) {
- this.db = db.map((config) => {
- if (config.id === undefined) {
- config.id = chance.word();
- }
- return config as ConfigurationBlock & { id: string };
- });
- }
-
- public async getByIds(user: FrameworkUser, ids: string[]) {
- return this.db.filter((block) => ids.includes(block.id));
- }
- public async delete(user: FrameworkUser, blockIds: string[]) {
- this.db = this.db.filter((block) => !blockIds.includes(block.id));
- return blockIds.map((id) => ({
- id,
- success: true,
- }));
- }
- public async deleteForTags(
- user: FrameworkUser,
- tagIds: string[]
- ): Promise<{ success: boolean; reason?: string }> {
- this.db = this.db.filter((block) => !tagIds.includes(block.tag));
- return {
- success: true,
- };
- }
-
- public async getForTags(user: FrameworkUser, tagIds: string[], page?: number, size?: number) {
- const results = this.db.filter((block) => tagIds.includes(block.id));
- return {
- page: 0,
- total: results.length,
- blocks: results,
- };
- }
-
- public async create(user: FrameworkUser, blocks: ConfigurationBlock[]) {
- return blocks.map((block) => {
- const existingIndex = this.db.findIndex((t) => t.id === block.id);
- if (existingIndex !== -1) {
- this.db[existingIndex] = block;
- } else {
- this.db.push(block);
- }
- return block.id;
- });
- }
-
- public setDB(db: ConfigurationBlock[]) {
- this.db = db.map((block) => {
- if (block.id === undefined) {
- block.id = chance.word();
- }
- return block;
- });
- }
-}
diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.ts
deleted file mode 100644
index 460fc412e94910..00000000000000
--- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-// file.skip
-
-// @ts-ignore
-import { createLegacyEsTestCluster } from '@kbn/test';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { Root } from 'src/core/server/root';
-// @ts-ignore
-import * as kbnTestServer from '../../../../../../../../../src/test_utils/kbn_server';
-import { DatabaseKbnESPlugin } from '../adapter_types';
-import { KibanaDatabaseAdapter } from '../kibana_database_adapter';
-import { contractTests } from './test_contract';
-const es = createLegacyEsTestCluster({});
-
-let legacyServer: any;
-let rootServer: Root;
-contractTests('Kibana Database Adapter', {
- before: async () => {
- await es.start();
-
- rootServer = kbnTestServer.createRootWithCorePlugins({
- server: { maxPayloadBytes: 100 },
- });
-
- await rootServer.setup();
- legacyServer = kbnTestServer.getKbnServer(rootServer);
- return await legacyServer.plugins.elasticsearch.waitUntilReady();
- },
- after: async () => {
- await rootServer.shutdown();
- return await es.cleanup();
- },
- adapterSetup: () => {
- return new KibanaDatabaseAdapter(legacyServer.plugins.elasticsearch as DatabaseKbnESPlugin);
- },
-});
diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts
deleted file mode 100644
index 369c2e10562118..00000000000000
--- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import { DatabaseAdapter } from '../adapter_types';
-
-interface ContractConfig {
- before?(): Promise;
- after?(): Promise;
- adapterSetup(): DatabaseAdapter;
-}
-
-export const contractTests = (testName: string, config: ContractConfig) => {
- describe.skip(testName, () => {
- let database: DatabaseAdapter;
- beforeAll(async () => {
- jest.setTimeout(100000); // 1 second
-
- if (config.before) {
- await config.before();
- }
- });
- afterAll(async () => config.after && (await config.after()));
- beforeEach(async () => {
- database = config.adapterSetup();
- });
-
- it('Unauthorized users cant query', async () => {
- const params = {
- id: `beat:foo`,
- ignore: [404],
- index: '.management-beats',
- };
- let ranWithoutError = false;
- try {
- await database.get({ kind: 'unauthenticated' }, params);
- ranWithoutError = true;
- } catch (e) {
- expect(e).not.toEqual(null);
- }
- expect(ranWithoutError).toEqual(false);
- });
-
- it('Should query ES', async () => {
- const params = {
- id: `beat:foo`,
- ignore: [404],
- index: '.management-beats',
- };
- const response = await database.get({ kind: 'internal' }, params);
-
- expect(response).not.toEqual(undefined);
- // @ts-ignore
- expect(response.found).toEqual(undefined);
- });
- });
-};
diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/adapter_types.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/adapter_types.ts
index 0a06c3dcc6412d..90519840af213b 100644
--- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/adapter_types.ts
+++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/adapter_types.ts
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { FrameworkRequest, FrameworkUser } from '../framework/adapter_types';
+import { FrameworkUser } from '../framework/adapter_types';
export interface DatabaseAdapter {
get
+ );
+
+ const RequestTab = () => {
+ const endpoint = `PUT _template/${name || ''}`;
+ const templateString = JSON.stringify(serializedTemplate, null, 2);
+ const request = `${endpoint}\n${templateString}`;
+
+ // Beyond a certain point, highlighting the syntax will bog down performance to unacceptable
+ // levels. This way we prevent that happening for very large requests.
+ const language = request.length < 60000 ? 'json' : undefined;
+
+ return (
+
- );
+
+
- const RequestTab = () => {
- const endpoint = `PUT _template/${name || ''}`;
- const templateString = JSON.stringify(serializedTemplate, null, 2);
- const request = `${endpoint}\n${templateString}`;
+
- // Beyond a certain point, highlighting the syntax will bog down performance to unacceptable
- // levels. This way we prevent that happening for very large requests.
- const language = request.length < 60000 ? 'json' : undefined;
+
+ {request}
+
+