diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.http.md b/docs/development/core/server/kibana-plugin-core-server.corestart.http.md new file mode 100644 index 0000000000000..d81049dfbd340 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.http.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreStart](./kibana-plugin-core-server.corestart.md) > [http](./kibana-plugin-core-server.corestart.http.md) + +## CoreStart.http property + +[HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) + +Signature: + +```typescript +http: HttpServiceStart; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.md b/docs/development/core/server/kibana-plugin-core-server.corestart.md index c50e8924c9dd4..6a6bacf1eef40 100644 --- a/docs/development/core/server/kibana-plugin-core-server.corestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.md @@ -18,6 +18,7 @@ export interface CoreStart | --- | --- | --- | | [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | CapabilitiesStart | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) | | [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | ElasticsearchServiceStart | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | +| [http](./kibana-plugin-core-server.corestart.http.md) | HttpServiceStart | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | | [savedObjects](./kibana-plugin-core-server.corestart.savedobjects.md) | SavedObjectsServiceStart | [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) | | [uiSettings](./kibana-plugin-core-server.corestart.uisettings.md) | UiSettingsServiceStart | [UiSettingsServiceStart](./kibana-plugin-core-server.uisettingsservicestart.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.httpauth.get.md b/docs/development/core/server/kibana-plugin-core-server.httpauth.get.md new file mode 100644 index 0000000000000..4ea67cf895a27 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpauth.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpAuth](./kibana-plugin-core-server.httpauth.md) > [get](./kibana-plugin-core-server.httpauth.get.md) + +## HttpAuth.get property + +Gets authentication state for a request. Returned by `auth` interceptor. [GetAuthState](./kibana-plugin-core-server.getauthstate.md) + +Signature: + +```typescript +get: GetAuthState; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpauth.isauthenticated.md b/docs/development/core/server/kibana-plugin-core-server.httpauth.isauthenticated.md new file mode 100644 index 0000000000000..54db6bce5f161 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpauth.isauthenticated.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpAuth](./kibana-plugin-core-server.httpauth.md) > [isAuthenticated](./kibana-plugin-core-server.httpauth.isauthenticated.md) + +## HttpAuth.isAuthenticated property + +Returns authentication status for a request. [IsAuthenticated](./kibana-plugin-core-server.isauthenticated.md) + +Signature: + +```typescript +isAuthenticated: IsAuthenticated; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpauth.md b/docs/development/core/server/kibana-plugin-core-server.httpauth.md new file mode 100644 index 0000000000000..d9d77809570ab --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpauth.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpAuth](./kibana-plugin-core-server.httpauth.md) + +## HttpAuth interface + + +Signature: + +```typescript +export interface HttpAuth +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [get](./kibana-plugin-core-server.httpauth.get.md) | GetAuthState | Gets authentication state for a request. Returned by auth interceptor. [GetAuthState](./kibana-plugin-core-server.getauthstate.md) | +| [isAuthenticated](./kibana-plugin-core-server.httpauth.isauthenticated.md) | IsAuthenticated | Returns authentication status for a request. [IsAuthenticated](./kibana-plugin-core-server.isauthenticated.md) | + diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.auth.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.auth.md index 6667779c1c7ae..da348a2282b1a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.auth.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.auth.md @@ -4,11 +4,15 @@ ## HttpServiceSetup.auth property +> Warning: This API is now obsolete. +> +> use [the start contract](./kibana-plugin-core-server.httpservicestart.auth.md) instead. +> + +Auth status. See [HttpAuth](./kibana-plugin-core-server.httpauth.md) + Signature: ```typescript -auth: { - get: GetAuthState; - isAuthenticated: IsAuthenticated; - }; +auth: HttpAuth; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.istlsenabled.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.istlsenabled.md deleted file mode 100644 index fa86da18393f5..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.istlsenabled.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) > [isTlsEnabled](./kibana-plugin-core-server.httpservicesetup.istlsenabled.md) - -## HttpServiceSetup.isTlsEnabled property - -Flag showing whether a server was configured to use TLS connection. - -Signature: - -```typescript -isTlsEnabled: boolean; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md index 2dd832813afb8..b12983836d9e5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md @@ -81,13 +81,12 @@ async (context, request, response) => { | Property | Type | Description | | --- | --- | --- | -| [auth](./kibana-plugin-core-server.httpservicesetup.auth.md) | {
get: GetAuthState;
isAuthenticated: IsAuthenticated;
} | | +| [auth](./kibana-plugin-core-server.httpservicesetup.auth.md) | HttpAuth | Auth status. See [HttpAuth](./kibana-plugin-core-server.httpauth.md) | | [basePath](./kibana-plugin-core-server.httpservicesetup.basepath.md) | IBasePath | Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-core-server.ibasepath.md). | | [createCookieSessionStorageFactory](./kibana-plugin-core-server.httpservicesetup.createcookiesessionstoragefactory.md) | <T>(cookieOptions: SessionStorageCookieOptions<T>) => Promise<SessionStorageFactory<T>> | Creates cookie based session storage factory [SessionStorageFactory](./kibana-plugin-core-server.sessionstoragefactory.md) | | [createRouter](./kibana-plugin-core-server.httpservicesetup.createrouter.md) | () => IRouter | Provides ability to declare a handler function for a particular path and HTTP request method. | | [csp](./kibana-plugin-core-server.httpservicesetup.csp.md) | ICspConfig | The CSP config used for Kibana. | | [getServerInfo](./kibana-plugin-core-server.httpservicesetup.getserverinfo.md) | () => HttpServerInfo | Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running http server. | -| [isTlsEnabled](./kibana-plugin-core-server.httpservicesetup.istlsenabled.md) | boolean | Flag showing whether a server was configured to use TLS connection. | | [registerAuth](./kibana-plugin-core-server.httpservicesetup.registerauth.md) | (handler: AuthenticationHandler) => void | To define custom authentication and/or authorization mechanism for incoming requests. | | [registerOnPostAuth](./kibana-plugin-core-server.httpservicesetup.registeronpostauth.md) | (handler: OnPostAuthHandler) => void | To define custom logic to perform for incoming requests. | | [registerOnPreAuth](./kibana-plugin-core-server.httpservicesetup.registeronpreauth.md) | (handler: OnPreAuthHandler) => void | To define custom logic to perform for incoming requests. | diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicestart.islistening.md b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.auth.md similarity index 50% rename from docs/development/core/server/kibana-plugin-core-server.httpservicestart.islistening.md rename to docs/development/core/server/kibana-plugin-core-server.httpservicestart.auth.md index bf2922c62c15f..f7dffee2e125c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicestart.islistening.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.auth.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) > [isListening](./kibana-plugin-core-server.httpservicestart.islistening.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) > [auth](./kibana-plugin-core-server.httpservicestart.auth.md) -## HttpServiceStart.isListening property +## HttpServiceStart.auth property -Indicates if http server is listening on a given port +Auth status. See [HttpAuth](./kibana-plugin-core-server.httpauth.md) Signature: ```typescript -isListening: (port: number) => boolean; +auth: HttpAuth; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicestart.basepath.md b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.basepath.md new file mode 100644 index 0000000000000..e8b2a0fc2cbaa --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.basepath.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) > [basePath](./kibana-plugin-core-server.httpservicestart.basepath.md) + +## HttpServiceStart.basePath property + +Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-core-server.ibasepath.md). + +Signature: + +```typescript +basePath: IBasePath; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicestart.getserverinfo.md b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.getserverinfo.md new file mode 100644 index 0000000000000..a95c8da64fdb0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.getserverinfo.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) > [getServerInfo](./kibana-plugin-core-server.httpservicestart.getserverinfo.md) + +## HttpServiceStart.getServerInfo property + +Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running http server. + +Signature: + +```typescript +getServerInfo: () => HttpServerInfo; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicestart.md b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.md index 53239da516b25..bc99c1217f72b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicestart.md @@ -15,5 +15,7 @@ export interface HttpServiceStart | Property | Type | Description | | --- | --- | --- | -| [isListening](./kibana-plugin-core-server.httpservicestart.islistening.md) | (port: number) => boolean | Indicates if http server is listening on a given port | +| [auth](./kibana-plugin-core-server.httpservicestart.auth.md) | HttpAuth | Auth status. See [HttpAuth](./kibana-plugin-core-server.httpauth.md) | +| [basePath](./kibana-plugin-core-server.httpservicestart.basepath.md) | IBasePath | Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-core-server.ibasepath.md). | +| [getServerInfo](./kibana-plugin-core-server.httpservicestart.getserverinfo.md) | () => HttpServerInfo | Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running http server. | diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 147a72016b235..0f1bbbe7176e5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -85,6 +85,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [EnvironmentMode](./kibana-plugin-core-server.environmentmode.md) | | | [ErrorHttpResponseOptions](./kibana-plugin-core-server.errorhttpresponseoptions.md) | HTTP response parameters | | [FakeRequest](./kibana-plugin-core-server.fakerequest.md) | Fake request object created manually by Kibana plugins. | +| [HttpAuth](./kibana-plugin-core-server.httpauth.md) | | | [HttpResources](./kibana-plugin-core-server.httpresources.md) | HttpResources service is responsible for serving static & dynamic assets for Kibana application via HTTP. Provides API allowing plug-ins to respond with: - a pre-configured HTML page bootstrapping Kibana client app - custom HTML page - custom JS script file. | | [HttpResourcesRenderOptions](./kibana-plugin-core-server.httpresourcesrenderoptions.md) | Allows to configure HTTP response parameters | | [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) | Extended set of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) helpers used to respond with HTML or JS resource. | @@ -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 a1b1a7a056206..4ed069d1598fe 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 adad0dd2b1176..7a91367f6ef0b 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 0000000000000..e455074a7d11b --- /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 0000000000000..c6646df6ee470 --- /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/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts index 2eb6c6cc5aac6..861ea0988692c 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 ea4159de55749..2aba2be56f277 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 e671061b34352..7334c6353debf 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 22baf4a330416..4f203e73620f3 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 dbfa87e70032b..6ee2d3bfe59b0 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 29ec28175a851..e9aeee87f1a3b 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 c43efabb4b747..909c94098cf5d 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/src/core/TESTING.md b/src/core/TESTING.md index bed41ab583496..a62922d9b5d64 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 5c8eca4a33ec5..cb279b2cc4c8f 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 7d2e7391aa8d4..42dc1604281b8 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 e7dab3807733a..8bf0df74186a9 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 1798c3a921da4..9a5deb9b45562 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 8089ee901fa65..d4615dd4744e5 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 0788a8f2af7a1..02ae6f5d95a87 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 ae9d53f9fd3db..c2fd653918171 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 651d1712100ee..f85f187164c92 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 77e0d6b61692d..7f2e70545d015 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 e6f129ba12d78..80afddc166570 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 658c24f835020..0da7e5d66cf2a 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 09ec772a41756..f68ab633dcbe6 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 2ae5e3a3fd1e8..67f2f433d4570 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 d9a0ac5e4ecff..fb9dc0776716a 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 2ced8b4762406..cfc53b10d91f0 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 01a7429745cda..b3cc06ffca1d2 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 559588db60a42..9e76895b14578 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 b6e9ffef6f3f1..f3ae5462f1631 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 f0db3a25e313d..31e36db49223a 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 3e668b3f26ab5..ce2eea119d1bb 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 32485f461f59b..5da2235828b5c 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 cafaa5a3147db..6e985c25aeaef 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 31bda1d6b9cbd..33e12dd4e517d 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 9fba2728003d2..e8b2cf0b583b1 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 d631ef9cb353c..ea749235cbb41 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 03538f2394845..40c5282a77e49 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 8780f07cc3091..e15a92c92772f 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 ecfa09fbd37f3..9dc3ac9b94d96 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 6ca580083648f..ae1a02cf71b88 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -202,10 +202,12 @@ export class Server { }); const capabilitiesStart = this.capabilities.start(); const uiSettingsStart = await this.uiSettings.start(); + const httpStart = this.http.getStartContract(); this.coreStart = { capabilities: capabilitiesStart, elasticsearch: elasticsearchStart, + http: httpStart, savedObjects: savedObjectsStart, uiSettings: uiSettingsStart, }; @@ -221,6 +223,7 @@ export class Server { }); await this.http.start(); + await this.rendering.start({ legacy: this.legacy, }); diff --git a/src/core/server/ui_settings/ui_settings_service.test.ts b/src/core/server/ui_settings/ui_settings_service.test.ts index ebcb0cf1d762f..096ca347e6f4b 100644 --- a/src/core/server/ui_settings/ui_settings_service.test.ts +++ b/src/core/server/ui_settings/ui_settings_service.test.ts @@ -49,7 +49,7 @@ describe('uiSettings', () => { beforeEach(() => { const coreContext = mockCoreContext.create(); coreContext.configService.atPath.mockReturnValue(new BehaviorSubject({ overrides })); - const httpSetup = httpServiceMock.createSetupContract(); + const httpSetup = httpServiceMock.createInternalSetupContract(); const savedObjectsSetup = savedObjectsServiceMock.createInternalSetupContract(); setupDeps = { http: httpSetup, savedObjects: savedObjectsSetup }; savedObjectsClient = savedObjectsClientMock.create(); diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 2f785896da8d5..85bfd4a7a4d26 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 f36cbb3f516b9..e335652195b86 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/embeddable/docs/README.md b/src/plugins/embeddable/docs/README.md index 1b6c7be13b1d4..ce5e76d54a046 100644 --- a/src/plugins/embeddable/docs/README.md +++ b/src/plugins/embeddable/docs/README.md @@ -2,4 +2,5 @@ ## Reference -- [Embeddable containers and inherited input state](./containers_and_inherited_state.md) +- [Input and output state](./input_and_output_state.md) +- [Common mistakes with embeddable containers and inherited input state](./containers_and_inherited_state.md) diff --git a/src/plugins/embeddable/docs/containers_and_inherited_state.md b/src/plugins/embeddable/docs/containers_and_inherited_state.md index c950bef96002a..35e399f89c131 100644 --- a/src/plugins/embeddable/docs/containers_and_inherited_state.md +++ b/src/plugins/embeddable/docs/containers_and_inherited_state.md @@ -1,4 +1,4 @@ -## Embeddable containers and inherited input state +## Common mistakes with embeddable containers and inherited input state `updateInput` is typed as `updateInput(input: Partial)`. Notice it's _partial_. This is to support the use case of inherited state when an embeddable is inside a container. diff --git a/src/plugins/embeddable/docs/input_and_output_state.md b/src/plugins/embeddable/docs/input_and_output_state.md new file mode 100644 index 0000000000000..810dc72664f96 --- /dev/null +++ b/src/plugins/embeddable/docs/input_and_output_state.md @@ -0,0 +1,282 @@ +## Input and output state + +### What's the difference? + +Input vs Output State + +| Input | Output | +| ----------- | ----------- | +| Public, on the IEmbeddable interface. `embeddable.updateInput(changedInput)` | Protected inside the Embeddable class. `this.updateOutput(changedOutput)` | +| Serializable representation of the embeddable | Does not need to be serializable | +| Can be updated throughout the lifecycle of an Embeddable | Often derived from input state | + +Non-real examples to showcase the difference: + +| Input | Output | +| ----------- | ----------- | +| savedObjectId | savedObjectAttributes | +| esQueryRequest | esQueryResponse | +| props | renderComplete | + +### Types of input state + +#### Inherited input state + +The only reason we have different types of input state is to support embeddable containers, and children embeddables _inheriting_ state from the container. +For example, when the dashboard time range changes, so does +the time range of all children embeddables. Dashboard passes down time range as _inherited_ input state. From the viewpoint of the child Embeddable, +time range is just input state. It doesn't care where it gets this data from. + + +For example, imagine a container with this input: + +```js +{ + gridData: {...}, + timeRange: 'now-15m to now', + + // Every embeddable container has a panels mapping. It's how the base container class manages common changes like children being + // added, removed or edited. + panels: { + ['1']: { + // `type` is used to grab the right embeddable factory. Every PanelState must specify one. + type: 'clock', + + // `explicitInput` is combined with `inheritedInput` to create `childInput`, and is use like: + // `embeddableFactories.get(type).create(childInput)`. + explicitInput: { + + // All explicitInput is required to have an id. This is used as a way for the + // embeddable to know where it exists in the panels array if it's living in a container. + // Note, this is NOT THE SAVED OBJECT ID! Even though it's sometimes used to store the saved object id. + id: '1', + } + } + } +} +``` + +That could result in the following input being passed to a child: + +```js +{ + timeRange: 'now-15m to now', + id: '1', +} +``` + +Notice that `gridData` is not passed down, but `timeRange` is. What ends up as _inherited_ state, that is passed down to a child, is up to the specific +implementation of a container and +determined by the abstract function `Container.getInheritedInput()` + +#### Overridding inherited input + +We wanted to support _overriding_ this inherited state, to support the "Per panel time range" feature. The _inherited_ `timeRange` input can be +overridden by the _explicit_ `timeRange` input. + +Take this example dashboard container input: + +```js +{ + gridData: {...}, + timeRange: 'now-15m to now', + panels: { + ['1']: { + type: 'clock', + explicitInput: { + timeRange: 'now-30m to now', + id: '1', + } + }, + ['2']: { + type: 'clock', + explicitInput: { + id: '2', + } + }, +} +``` + +The first child embeddable will get passed input state: + +```js +{ + timeRange: 'now-30m to now', + id: '1', +} +``` + +This override wouldn't affect other children, so the second child would receive: + +```js +{ + timeRange: 'now-15m to now', + id: '2', +} +``` + +#### EmbeddableInput.id and some technical debt + +Above I said: + +> From the viewpoint of the child Embeddable, +> time range is just input state. It doesn't care where it gets this data from. + +and this is mostly true, however, the primary reason EmbeddableInput.id exists is to support the +case where the custom time range badge action needs to look up a child's explicit input on the +parent. It does this to determine whether or not to show the badge. The logic is something like: + +```ts + // If there is no explicit input defined on the parent then this embeddable inherits the + // time range from whatever the time range of the parent is. + return parent.getInput().panels[embeddable.id].explicitInput.timeRange === undefined; +``` + +It doesn't just compare the timeRange input on the parent (`embeddable.parent?.getInput().timeRange` )because even if they happen to match, +we still want the badge showing to indicate the time range is "locked" on this particular panel. + +Note that `parent` can be retrieved from either `embeddabble.parent` or `embeddable.getRoot()`. The +`getRoot` variety will walk up to find the root parent, even though we have no tested or used +nested containers, it is theoretically possible. + +This EmbeddableInput.id parameter is marked as required on the `EmbeddableInput` interface, even though it's only used +when an embeddable is inside a parent. There is also no +typescript safety to ensure the id matches the panel id in the parents json: + +```js + ['2']: { + type: 'clock', + explicitInput: { + id: '3', // No! Should be 2! + } + }, +``` + +It should probably be something that the parent passes down to the child specifically, based on the panel mapping key, +and renamed to something like `panelKeyInParent`. + +Note that this has nothing to do with a saved object id, even though in dashboard app, the saved object happens to be +used as the dashboard container id. Another reason this should probably not be required for embeddables not +inside containers. + +#### A container can pass down any information to the children + +It doesn't have to be part of it's own input. It's possible for a container input like: + + +```js +{ + timeRange: 'now-15m to now', + panels: { + ['1']: { + type: 'clock', + explicitInput: { + timeRange: 'now-30m to now', + id: '1', + } + } +} +``` + +to pass down this input: + +```js +{ + timeRange: 'now-30m to now', + id: '1', + zed: 'bar', // <-- Where did this come from?? +} +``` + +I don't have a realistic use case for this, just noting it's possible in any containers implementation of `getInheritedInput`. Note this is still considered +inherited input because it's coming from the container. + +#### Explicit input stored on behalf of the container + +It's possible for a container to store explicit input state on behalf of an embeddable, without knowing what that state is. For example, a container could +have input state like: + +```js +{ + timeRange: 'now-15m to now', + panels: { + ['1']: { + type: 'clock', + explicitInput: { + display: 'analog', + id: '1', + } + } +} +``` + +And what gets passed to the child is: + +```js +{ + timeRange: 'now-15m to now', + id: '1', + display: 'analog' +} +``` + +even if a container has no idea about this `clock` embeddable implementation, nor this `explicitInput.display` field. + +There are two ways for this kind of state to end up in `panels[id].explicitInput`. + +1. `ClockEmbeddableFactory.getExplicitInput` returns it. +2. `ClockEmbeddableFactory.getDefaultInput` returns it. (This function is largely unused. We may be able to get rid of it.) +3. Someone called `embeddable.updateInput({ display: 'analog' })`, when the embeddable is a child in a container. + +#### Containers can pass down too much information + +Lets say our container state is: + +```js +{ + timeRange: 'now-15m to now', + panels: { + ['1']: { + type: 'helloWorld', + explicitInput: { + id: '1', + } + } +} +``` + +What gets passed to the child is: + +```js +{ + timeRange: 'now-15m to now', + id: '1', +} +``` + +It doesn't matter if the embeddable does not require, nor use, `timeRange`. The container passes down inherited input state to every child. +This could present problems with trying to figure out which embeddables support +different types of actions. For example, it'd be great if "Customize time range" action only showed up on embeddables that actually did something +with the `timeRange`. You can't check at runtime whether `input.timeRange === undefined` to do so though, because it will be passed in by the container +regardless. + + +#### Tech debt warnings + +`EmbeddableFactory.getExplicitInput` was intended as a way for an embeddable to retrieve input state it needs, that will not +be provided by a container. However, an embeddable won't know where it will be rendered, so how will the factory know which +required data to ask from the user and which will be inherited from the container? I believe `getDefaultInput` was meant to solve this. +`getDefaultInput` would provide default values, only if the container didn't supply them through inheritance. Explicit input would +always provide these values, and would always be stored in a containers `panel[id].explicitInput`, even if the container _did_ provide +them. + +There are no real life examples showcasing this, it may not even be really needed by current use cases. Containers were built as an abstraction, with +the thinking being that it would support any type of rendering of child embeddables - whether in a "snap to grid" style like dashboard, +or in a free form layout like canvas. + +The only real implementation of a container in production code at the time this is written is Dashboard however, with no plans to migrate +Canvas over to use it (this was the original impetus for an abstraction). The container code is quite complicated with child management, +so it makes creating a new container very easy, as you can see in the developer examples of containers. But, it's possible this layer was + an over abstraction without a real prod use case (I can say that because I wrote it, I'm only insulting myself!) :). + +Be sure to read [Common mistakes with embeddable containers and inherited input state](./containers_and_inherited_state.md) next! \ No newline at end of file diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index 8ba7be7880a7b..7b66f29cc2726 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/home/public/application/components/__snapshots__/welcome.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap index 1c67f332a12ab..bdf1f075967cb 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 1332e03ffdc81..701fab3af7539 100644 --- a/src/plugins/home/public/application/components/welcome.test.tsx +++ b/src/plugins/home/public/application/components/welcome.test.tsx @@ -30,56 +30,39 @@ jest.mock('../kibana_services', () => ({ })); test('should render a Welcome screen with the telemetry disclaimer', () => { - const telemetry = telemetryPluginMock.createSetupContract(); - const component = shallow( - // @ts-ignore - {}} telemetry={telemetry} /> - ); + const telemetry = telemetryPluginMock.createStartContract(); + const component = shallow( {}} telemetry={telemetry} />); expect(component).toMatchSnapshot(); }); test('should render a Welcome screen with the telemetry disclaimer when optIn is true', () => { - const telemetry = telemetryPluginMock.createSetupContract(); + const telemetry = telemetryPluginMock.createStartContract(); telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); - const component = shallow( - // @ts-ignore - {}} telemetry={telemetry} /> - ); + const component = shallow( {}} telemetry={telemetry} />); expect(component).toMatchSnapshot(); }); test('should render a Welcome screen with the telemetry disclaimer when optIn is false', () => { - const telemetry = telemetryPluginMock.createSetupContract(); + const telemetry = telemetryPluginMock.createStartContract(); telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false); - const component = shallow( - // @ts-ignore - {}} telemetry={telemetry} /> - ); + const component = shallow( {}} telemetry={telemetry} />); expect(component).toMatchSnapshot(); }); test('should render a Welcome screen with no telemetry disclaimer', () => { - // @ts-ignore - const component = shallow( - // @ts-ignore - {}} telemetry={null} /> - ); + const component = shallow( {}} />); expect(component).toMatchSnapshot(); }); test('fires opt-in seen when mounted', () => { - const telemetry = telemetryPluginMock.createSetupContract(); + const telemetry = telemetryPluginMock.createStartContract(); const mockSetOptedInNoticeSeen = jest.fn(); - // @ts-ignore telemetry.telemetryNotifications.setOptedInNoticeSeen = mockSetOptedInNoticeSeen; - shallow( - // @ts-ignore - {}} telemetry={telemetry} /> - ); + shallow( {}} telemetry={telemetry} />); expect(mockSetOptedInNoticeSeen).toHaveBeenCalled(); }); diff --git a/src/plugins/home/public/application/components/welcome.tsx b/src/plugins/home/public/application/components/welcome.tsx index d4dcaca317806..f82bd024b80b8 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 2c8997c9af21a..e18a45d9bdf44 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 9ec4a3ae86cc7..dd7e5a4cc4ce3 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 a363953978d79..3846e7cb96a19 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/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js index 7a57d182bc812..7cb5955e4a43d 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 e15a9e989d21f..4d9f1c1658139 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 16039d6fee833..4a251cca044d3 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 dfc6ff9b164e5..caa5549a70f0c 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 cc2fa23825498..7951d4b5b47b2 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 6ad0933a2a5a2..fae4c9198cab6 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 ae02127043234..58c4d0f1cf34e 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/services/common/browser.ts b/test/functional/services/common/browser.ts index 3297f6e094f7c..d6a4fc91481de 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 5a3a775cae0c5..99643929c4682 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 9fbbf28bbf42c..27814060e70c1 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 fdaee76cafa9d..ae924a5e10552 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/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 21b2df3ba12f8..278968cb47231 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 f3d1b9164e976..0000000000000 --- 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 1aec3e8081708..7cae2a85dc4ca 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 b4a9ac8a07479..32e1d81451c65 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 d77ad92298699..7d71ea5ad8256 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 a7125795a5c7d..7e0e39e12e60a 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 afae87c490158..0000000000000 --- 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 ea8a75c92fad2..0000000000000 --- 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 460fc412e9491..0000000000000 --- 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 369c2e1056211..0000000000000 --- 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 0a06c3dcc6412..90519840af213 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( @@ -39,15 +39,6 @@ export interface DatabaseAdapter { putTemplate(name: string, template: any): Promise; } -export interface DatabaseKbnESCluster { - callWithInternalUser(esMethod: string, options: {}): Promise; - callWithRequest(req: FrameworkRequest, esMethod: string, options: {}): Promise; -} - -export interface DatabaseKbnESPlugin { - getCluster(clusterName: string): DatabaseKbnESCluster; -} - export interface DatabaseSearchParams extends DatabaseGenericParams { analyzer?: string; analyzeWildcard?: boolean; diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts index 1ca3bcae8bfca..baccbe416f398 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts @@ -3,6 +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 { ElasticsearchServiceStart, IClusterClient } from 'src/core/server'; import { FrameworkUser } from '../framework/adapter_types'; import { internalAuthData } from './../framework/adapter_types'; import { @@ -15,8 +16,6 @@ import { DatabaseGetDocumentResponse, DatabaseGetParams, DatabaseIndexDocumentParams, - DatabaseKbnESCluster, - DatabaseKbnESPlugin, DatabaseMGetParams, DatabaseMGetResponse, DatabaseSearchParams, @@ -24,75 +23,67 @@ import { } from './adapter_types'; export class KibanaDatabaseAdapter implements DatabaseAdapter { - private es: DatabaseKbnESCluster; + private es: IClusterClient; - constructor(kbnElasticSearch: DatabaseKbnESPlugin) { - this.es = kbnElasticSearch.getCluster('admin'); + constructor(elasticsearch: ElasticsearchServiceStart) { + this.es = elasticsearch.legacy.client; } public async get( user: FrameworkUser, params: DatabaseGetParams ): Promise> { - const result = await this.callWithUser(user, 'get', params); - return result; - // todo + return await this.callWithUser(user, 'get', params); } public async mget( user: FrameworkUser, params: DatabaseMGetParams ): Promise> { - const result = await this.callWithUser(user, 'mget', params); - return result; - // todo + return await this.callWithUser(user, 'mget', params); } public async bulk(user: FrameworkUser, params: DatabaseBulkIndexDocumentsParams): Promise { - const result = await this.callWithUser(user, 'bulk', params); - return result; + return await this.callWithUser(user, 'bulk', params); } public async create( user: FrameworkUser, params: DatabaseCreateDocumentParams ): Promise { - const result = await this.callWithUser(user, 'create', params); - return result; + return await this.callWithUser(user, 'create', params); } + public async index(user: FrameworkUser, params: DatabaseIndexDocumentParams): Promise { - const result = await this.callWithUser(user, 'index', params); - return result; + return await this.callWithUser(user, 'index', params); } + public async delete( user: FrameworkUser, params: DatabaseDeleteDocumentParams ): Promise { - const result = await this.callWithUser(user, 'delete', params); - return result; + return await this.callWithUser(user, 'delete', params); } public async deleteByQuery( user: FrameworkUser, params: DatabaseSearchParams ): Promise { - const result = await this.callWithUser(user, 'deleteByQuery', params); - return result; + return await this.callWithUser(user, 'deleteByQuery', params); } public async search( user: FrameworkUser, params: DatabaseSearchParams ): Promise> { - const result = await this.callWithUser(user, 'search', params); - return result; + return await this.callWithUser(user, 'search', params); } public async searchAll( user: FrameworkUser, params: DatabaseSearchParams ): Promise> { - const result = await this.callWithUser(user, 'search', { + return await this.callWithUser(user, 'search', { scroll: '1m', ...params, body: { @@ -100,29 +91,24 @@ export class KibanaDatabaseAdapter implements DatabaseAdapter { ...params.body, }, }); - return result; } public async putTemplate(name: string, template: any): Promise { - const result = await this.callWithUser({ kind: 'internal' }, 'indices.putTemplate', { + return await this.callWithUser({ kind: 'internal' }, 'indices.putTemplate', { name, body: template, }); - - return result; } private callWithUser(user: FrameworkUser, esMethod: string, options: any = {}): any { if (user.kind === 'authenticated') { - return this.es.callWithRequest( - { + return this.es + .asScoped({ headers: user[internalAuthData], - } as any, - esMethod, - options - ); + }) + .callAsCurrentUser(esMethod, options); } else if (user.kind === 'internal') { - return this.es.callWithInternalUser(esMethod, options); + return this.es.callAsInternalUser(esMethod, options); } else { throw new Error('Invalid user type'); } diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/adapter_types.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/adapter_types.ts deleted file mode 100644 index 4cb38bb3d057b..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/adapter_types.ts +++ /dev/null @@ -1,12 +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 { BeatEvent } from '../../../../common/domain_types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FrameworkUser } from '../../../../../../../plugins/beats_management/public/lib/adapters/framework/adapter_types'; - -export interface BeatEventsAdapter { - bulkInsert(user: FrameworkUser, beatId: string, events: BeatEvent[]): Promise; -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/elasticsearch_beat_events_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/elasticsearch_beat_events_adapter.ts deleted file mode 100644 index b5056140c8b86..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/elasticsearch_beat_events_adapter.ts +++ /dev/null @@ -1,21 +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 { BeatEvent } from '../../../../common/domain_types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FrameworkUser } from '../../../../../../../plugins/beats_management/public/lib/adapters/framework/adapter_types'; -import { DatabaseAdapter } from '../database/adapter_types'; -import { BeatEventsAdapter } from './adapter_types'; - -export class ElasticsearchBeatEventsAdapter implements BeatEventsAdapter { - // @ts-ignore - constructor(private readonly database: DatabaseAdapter) {} - - // eslint-disable-next-line - public bulkInsert = async (user: FrameworkUser, beatId: string, events: BeatEvent[]) => { - // await this.database.putTemplate(INDEX_NAMES.EVENTS_TODAY, beatsIndexTemplate); - }; -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts index 80599f38d982a..e2703cb5786dd 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts @@ -8,6 +8,7 @@ import { Lifecycle, ResponseToolkit } from 'hapi'; import * as t from 'io-ts'; +import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup } from '../../../../../../../plugins/security/server'; import { LicenseType } from '../../../../common/constants/security'; @@ -33,7 +34,6 @@ export interface BackendFrameworkAdapter { log(text: string): void; on(event: 'xpack.status.green' | 'elasticsearch.status.green', cb: () => void): void; getSetting(settingPath: string): any; - exposeStaticDir(urlPath: string, dir: string): void; registerRoute( route: FrameworkRouteOptions ): void; @@ -42,8 +42,12 @@ export interface BackendFrameworkAdapter { export interface KibanaLegacyServer { newPlatform: { setup: { + core: CoreSetup; plugins: { security: SecurityPluginSetup }; }; + start: { + core: CoreStart; + }; }; plugins: { xpack_main: { diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts deleted file mode 100644 index 90500e0283511..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts +++ /dev/null @@ -1,148 +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 { LicenseType } from './../../../../common/constants/security'; -import { KibanaServerRequest } from './adapter_types'; -import { - BackendFrameworkAdapter, - FrameworkInfo, - FrameworkRequest, - FrameworkResponse, - FrameworkRouteOptions, - internalAuthData, - internalUser, -} from './adapter_types'; - -interface TestSettings { - enrollmentTokensTtlInSeconds: number; - encryptionKey: string; -} - -export class HapiBackendFrameworkAdapter implements BackendFrameworkAdapter { - public info: null | FrameworkInfo = null; - public readonly internalUser = internalUser; - - private settings: TestSettings; - private server: any; - - constructor( - settings: TestSettings = { - encryptionKey: 'something_who_cares', - enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes - }, - hapiServer?: any, - license: LicenseType = 'trial', - securityEnabled: boolean = true, - licenseActive: boolean = true - ) { - this.server = hapiServer; - this.settings = settings; - const now = new Date(); - - this.info = { - kibana: { - version: 'unknown', - }, - license: { - type: license, - expired: !licenseActive, - expiry_date_in_millis: new Date(now.getFullYear(), now.getMonth() + 1, 1).getTime(), - }, - security: { - enabled: securityEnabled, - available: securityEnabled, - }, - watcher: { - enabled: true, - available: true, - }, - }; - } - public log(text: string) { - this.server.log(text); - } - public on(event: 'xpack.status.green', cb: () => void) { - cb(); - } - public getSetting(settingPath: string) { - switch (settingPath) { - case 'xpack.beats.enrollmentTokensTtlInSeconds': - return this.settings.enrollmentTokensTtlInSeconds; - case 'xpack.beats.encryptionKey': - return this.settings.encryptionKey; - } - } - - public exposeStaticDir(urlPath: string, dir: string): void { - if (!this.server) { - throw new Error('Must pass a hapi server into the adapter to use exposeStaticDir'); - } - this.server.route({ - handler: { - directory: { - path: dir, - }, - }, - method: 'GET', - path: urlPath, - }); - } - - public registerRoute< - RouteRequest extends FrameworkRequest, - RouteResponse extends FrameworkResponse - >(route: FrameworkRouteOptions) { - if (!this.server) { - throw new Error('Must pass a hapi server into the adapter to use registerRoute'); - } - const wrappedHandler = (licenseRequired: string[]) => (request: any, h: any) => { - return route.handler(this.wrapRequest(request), h); - }; - - this.server.route({ - handler: wrappedHandler(route.licenseRequired || []), - method: route.method, - path: route.path, - config: { - ...route.config, - auth: false, - }, - }); - } - - public async injectRequstForTesting({ method, url, headers, payload }: any) { - return await this.server.inject({ method, url, headers, payload }); - } - - private wrapRequest( - req: InternalRequest - ): FrameworkRequest { - const { params, payload, query, headers, info } = req; - - const isAuthenticated = headers.authorization != null; - - return { - user: isAuthenticated - ? { - kind: 'authenticated', - [internalAuthData]: headers, - username: 'elastic', - roles: ['superuser'], - full_name: null, - email: null, - enabled: true, - } - : { - kind: 'unauthenticated', - }, - headers, - info, - params, - payload, - query, - }; - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/kibana.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/kibana.ts deleted file mode 100644 index 4f0ba01b86082..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/kibana.ts +++ /dev/null @@ -1,38 +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 - -import { camelCase } from 'lodash'; -// @ts-ignore -import * as kbnTestServer from '../../../../../../../../../src/test_utils/kbn_server'; -// @ts-ignore -import { TestKbnServerConfig } from '../../../../../../../test_utils/kbn_server_config'; -import { CONFIG_PREFIX } from '../../../../../common/constants/plugin'; -import { PLUGIN } from './../../../../../common/constants/plugin'; -import { KibanaBackendFrameworkAdapter } from './../kibana_framework_adapter'; -import { contractTests } from './test_contract'; - -let kbnServer: any; -let kbn: any; -let esServer: any; -contractTests('Kibana Framework Adapter', { - async before() { - const servers = kbnTestServer.createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: TestKbnServerConfig, - }); - esServer = await servers.startES(); - kbn = await servers.startKibana(); - kbnServer = kbn.kbnServer; - }, - async after() { - await kbn.stop(); - await esServer.stop(); - }, - adapterSetup: () => { - return new KibanaBackendFrameworkAdapter(camelCase(PLUGIN.ID), kbnServer.server, CONFIG_PREFIX); - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/test_contract.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/test_contract.ts deleted file mode 100644 index 8e21f8cf78ad7..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/test_contract.ts +++ /dev/null @@ -1,29 +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 { BackendFrameworkAdapter } from '../adapter_types'; - -interface ContractConfig { - before(): Promise; - after(): Promise; - adapterSetup(): BackendFrameworkAdapter; -} - -export const contractTests = (testName: string, config: ContractConfig) => { - describe(testName, () => { - let frameworkAdapter: BackendFrameworkAdapter; - beforeAll(config.before); - afterAll(config.after); - beforeEach(async () => { - frameworkAdapter = config.adapterSetup(); - }); - - it('Should have tests here', () => { - expect(frameworkAdapter.info).toHaveProperty('server'); - - expect(frameworkAdapter).toHaveProperty('server'); - }); - }); -}; diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts index 1bf9bbb22b352..3b29e50e4465b 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -68,18 +68,6 @@ export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { this.server.log(text); } - public exposeStaticDir(urlPath: string, dir: string): void { - this.server.route({ - handler: { - directory: { - path: dir, - }, - }, - method: 'GET', - path: urlPath, - }); - } - public registerRoute< RouteRequest extends FrameworkRequest, RouteResponse extends FrameworkResponse diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts deleted file mode 100644 index 66a6c7ebebc2c..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts +++ /dev/null @@ -1,50 +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 { BeatTag } from '../../../../common/domain_types'; -import { FrameworkUser } from '../framework/adapter_types'; -import { CMTagsAdapter } from './adapter_types'; - -export class MemoryTagsAdapter implements CMTagsAdapter { - private tagsDB: BeatTag[] = []; - - constructor(tagsDB: BeatTag[]) { - this.tagsDB = tagsDB; - } - - public async getAll(user: FrameworkUser) { - return this.tagsDB; - } - public async delete(user: FrameworkUser, tagIds: string[]) { - this.tagsDB = this.tagsDB.filter((tag) => !tagIds.includes(tag.id)); - - return true; - } - public async getTagsWithIds(user: FrameworkUser, tagIds: string[]) { - return this.tagsDB.filter((tag) => tagIds.includes(tag.id)); - } - - public async upsertTag(user: FrameworkUser, tag: BeatTag) { - const existingTagIndex = this.tagsDB.findIndex((t) => t.id === tag.id); - if (existingTagIndex !== -1) { - this.tagsDB[existingTagIndex] = tag; - } else { - this.tagsDB.push(tag); - } - return tag.id; - } - - public async getWithoutConfigTypes( - user: FrameworkUser, - blockTypes: string[] - ): Promise { - return this.tagsDB.filter((tag) => tag.hasConfigurationBlocksTypes.includes(blockTypes[0])); - } - - public setDB(tagsDB: BeatTag[]) { - this.tagsDB = tagsDB; - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts deleted file mode 100644 index 431263c808b45..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FrameworkAuthenticatedUser, FrameworkUser } from '../framework/adapter_types'; -import { CMTokensAdapter, TokenEnrollmentData } from './adapter_types'; - -export class MemoryTokensAdapter implements CMTokensAdapter { - private tokenDB: TokenEnrollmentData[]; - - constructor(tokenDB: TokenEnrollmentData[]) { - this.tokenDB = tokenDB; - } - - public async deleteEnrollmentToken(user: FrameworkUser, enrollmentToken: string) { - const index = this.tokenDB.findIndex((token) => token.token === enrollmentToken); - - if (index > -1) { - this.tokenDB.splice(index, 1); - } - } - - public async getEnrollmentToken( - user: FrameworkUser, - tokenString: string - ): Promise { - return new Promise((resolve) => { - return resolve(this.tokenDB.find((token) => token.token === tokenString)); - }); - } - - public async insertTokens(user: FrameworkAuthenticatedUser, tokens: TokenEnrollmentData[]) { - tokens.forEach((token) => { - const existingIndex = this.tokenDB.findIndex((t) => t.token === token.token); - if (existingIndex !== -1) { - this.tokenDB[existingIndex] = token; - } else { - this.tokenDB.push(token); - } - }); - return tokens; - } - - public setDB(tokenDB: TokenEnrollmentData[]) { - this.tokenDB = tokenDB; - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/beat_events.ts b/x-pack/legacy/plugins/beats_management/server/lib/beat_events.ts index f4cb3cb424f6f..54782783f94ca 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/beat_events.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/beat_events.ts @@ -6,13 +6,11 @@ import { PathReporter } from 'io-ts/lib/PathReporter'; import { isLeft } from 'fp-ts/lib/Either'; import { BeatEvent, RuntimeBeatEvent } from '../../common/domain_types'; -import { BeatEventsAdapter } from './adapters/events/adapter_types'; import { FrameworkUser } from './adapters/framework/adapter_types'; import { CMBeatsDomain } from './beats'; export class BeatEventsLib { - // @ts-ignore - constructor(private readonly adapter: BeatEventsAdapter, private readonly beats: CMBeatsDomain) {} + constructor(private readonly beats: CMBeatsDomain) {} public log = async ( user: FrameworkUser, diff --git a/x-pack/legacy/plugins/beats_management/server/lib/beats.ts b/x-pack/legacy/plugins/beats_management/server/lib/beats.ts index 3b9c4d35d8331..6b7053f40550b 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/beats.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/beats.ts @@ -7,7 +7,6 @@ import { uniq } from 'lodash'; import moment from 'moment'; import { CMBeat } from '../../common/domain_types'; -import { findNonExistentItems } from '../utils/find_non_existent_items'; import { BeatsRemovalReturn, BeatsTagAssignment, @@ -249,3 +248,12 @@ function addToResultsToResponse(key: string, response: any, assignmentResults: a }); return response; } + +export function findNonExistentItems(items: Array<{ id: string }>, requestedItems: string[]) { + return requestedItems.reduce((nonExistentItems: string[], requestedItem: string, idx: number) => { + if (items.findIndex((item) => item && item.id === requestedItem) === -1) { + nonExistentItems.push(requestedItems[idx]); + } + return nonExistentItems; + }, []); +} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/beats_management/server/lib/compose/kibana.ts index 2bda2fe85d62f..b6a645ded8164 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/compose/kibana.ts @@ -9,9 +9,7 @@ import { PLUGIN } from '../../../common/constants'; import { CONFIG_PREFIX } from '../../../common/constants/plugin'; import { ElasticsearchBeatsAdapter } from '../adapters/beats/elasticsearch_beats_adapter'; import { ElasticsearchConfigurationBlockAdapter } from '../adapters/configuration_blocks/elasticsearch_configuration_block_adapter'; -import { DatabaseKbnESPlugin } from '../adapters/database/adapter_types'; import { KibanaDatabaseAdapter } from '../adapters/database/kibana_database_adapter'; -import { ElasticsearchBeatEventsAdapter } from '../adapters/events/elasticsearch_beat_events_adapter'; import { KibanaLegacyServer } from '../adapters/framework/adapter_types'; import { KibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; import { ElasticsearchTagsAdapter } from '../adapters/tags/elasticsearch_tags_adapter'; @@ -28,7 +26,7 @@ export function compose(server: KibanaLegacyServer): CMServerLibs { const framework = new BackendFrameworkLib( new KibanaBackendFrameworkAdapter(camelCase(PLUGIN.ID), server, CONFIG_PREFIX) ); - const database = new KibanaDatabaseAdapter(server.plugins.elasticsearch as DatabaseKbnESPlugin); + const database = new KibanaDatabaseAdapter(server.newPlatform.start.core.elasticsearch); const beatsAdapter = new ElasticsearchBeatsAdapter(database); const configAdapter = new ElasticsearchConfigurationBlockAdapter(database); @@ -46,7 +44,7 @@ export function compose(server: KibanaLegacyServer): CMServerLibs { tokens, framework, }); - const beatEvents = new BeatEventsLib(new ElasticsearchBeatEventsAdapter(database), beats); + const beatEvents = new BeatEventsLib(beats); const libs: CMServerLibs = { beatEvents, diff --git a/x-pack/legacy/plugins/beats_management/server/lib/compose/testing.ts b/x-pack/legacy/plugins/beats_management/server/lib/compose/testing.ts deleted file mode 100644 index b5fe6195fc7c7..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/compose/testing.ts +++ /dev/null @@ -1,51 +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 { MemoryBeatsAdapter } from '../adapters/beats/memory_beats_adapter'; -import { MemoryConfigurationBlockAdapter } from '../adapters/configuration_blocks/memory_tags_adapter'; -import { HapiBackendFrameworkAdapter } from '../adapters/framework/hapi_framework_adapter'; -import { MemoryTagsAdapter } from '../adapters/tags/memory_tags_adapter'; -import { MemoryTokensAdapter } from '../adapters/tokens/memory_tokens_adapter'; -import { BeatEventsLib } from '../beat_events'; -import { CMBeatsDomain } from '../beats'; -import { ConfigurationBlocksLib } from '../configuration_blocks'; -import { BackendFrameworkLib } from '../framework'; -import { CMTagsDomain } from '../tags'; -import { CMTokensDomain } from '../tokens'; -import { CMServerLibs } from '../types'; - -export function compose(server: any): CMServerLibs { - const framework = new BackendFrameworkLib(new HapiBackendFrameworkAdapter(undefined, server)); - - const beatsAdapter = new MemoryBeatsAdapter(server.beatsDB || []); - const configAdapter = new MemoryConfigurationBlockAdapter(server.configsDB || []); - const tags = new CMTagsDomain( - new MemoryTagsAdapter(server.tagsDB || []), - configAdapter, - beatsAdapter - ); - const configurationBlocks = new ConfigurationBlocksLib(configAdapter, tags); - const tokens = new CMTokensDomain(new MemoryTokensAdapter(server.tokensDB || []), { - framework, - }); - const beats = new CMBeatsDomain(beatsAdapter, { - tags, - tokens, - framework, - }); - const beatEvents = new BeatEventsLib({} as any, beats); - - const libs: CMServerLibs = { - beatEvents, - framework, - beats, - tags, - tokens, - configurationBlocks, - }; - - return libs; -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/framework.ts b/x-pack/legacy/plugins/beats_management/server/lib/framework.ts index 1a6f84a6979c6..96a06929073e5 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/framework.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/framework.ts @@ -16,7 +16,6 @@ import { export class BackendFrameworkLib { public log = this.adapter.log; public on = this.adapter.on.bind(this.adapter); - public exposeStaticDir = this.adapter.exposeStaticDir; public internalUser = this.adapter.internalUser; constructor(private readonly adapter: BackendFrameworkAdapter) { this.validateConfig(); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts deleted file mode 100644 index 156304443431d..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts +++ /dev/null @@ -1,249 +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 { CMServerLibs } from '../../lib/types'; -import { HapiBackendFrameworkAdapter } from './../../lib/adapters/framework/hapi_framework_adapter'; -import { testHarnes } from './test_harnes'; - -describe('assign_tags_to_beats', () => { - let serverLibs: CMServerLibs; - - beforeAll(async () => { - jest.setTimeout(100000); // 1 second - - serverLibs = await testHarnes.getServerLibs(); - }); - beforeEach(async () => await testHarnes.loadData()); - - it('should add a single tag to a single beat', async () => { - const { result, statusCode } = await ((serverLibs.framework as any) - .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ - method: 'POST', - url: '/api/beats/agents_tags/assignments', - headers: { - 'kbn-xsrf': 'xxx', - authorization: 'loggedin', - }, - payload: { - assignments: [{ beatId: 'bar', tag: 'production' }], - }, - }); - - expect(statusCode).toEqual(200); - expect(result.results).toEqual([{ success: true, result: { message: 'updated' } }]); - }); - - it('should not re-add an existing tag to a beat', async () => { - const { result, statusCode } = await ((serverLibs.framework as any) - .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ - method: 'POST', - url: '/api/beats/agents_tags/assignments', - headers: { - 'kbn-xsrf': 'xxx', - authorization: 'loggedin', - }, - payload: { - assignments: [{ beatId: 'foo', tag: 'production' }], - }, - }); - - expect(statusCode).toEqual(200); - - expect(result.results).toEqual([{ success: true, result: { message: 'updated' } }]); - - const beat = await serverLibs.beats.getById( - { - kind: 'internal', - }, - 'foo' - ); - expect(beat!.tags).toEqual(['production', 'qa']); // as - }); - - it('should add a single tag to a multiple beats', async () => { - const { result, statusCode } = await ((serverLibs.framework as any) - .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ - method: 'POST', - url: '/api/beats/agents_tags/assignments', - headers: { - 'kbn-xsrf': 'xxx', - authorization: 'loggedin', - }, - payload: { - assignments: [ - { beatId: 'foo', tag: 'development' }, - { beatId: 'bar', tag: 'development' }, - ], - }, - }); - - expect(statusCode).toEqual(200); - - expect(result.results).toEqual([ - { success: true, result: { message: 'updated' } }, - { success: true, result: { message: 'updated' } }, - ]); - - let beat; - - beat = await serverLibs.beats.getById( - { - kind: 'internal', - }, - 'foo' - ); - expect(beat!.tags).toEqual(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it - - // Beat bar - beat = await serverLibs.beats.getById( - { - kind: 'internal', - }, - 'bar' - ); - - expect(beat!.tags).toEqual(['development']); - }); - - it('should add multiple tags to a single beat', async () => { - const { result, statusCode } = await ((serverLibs.framework as any) - .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ - method: 'POST', - url: '/api/beats/agents_tags/assignments', - headers: { - 'kbn-xsrf': 'xxx', - authorization: 'loggedin', - }, - payload: { - assignments: [ - { beatId: 'bar', tag: 'development' }, - { beatId: 'bar', tag: 'production' }, - ], - }, - }); - - expect(statusCode).toEqual(200); - - expect(result.results).toEqual([ - { success: true, result: { message: 'updated' } }, - { success: true, result: { message: 'updated' } }, - ]); - - const beat = await serverLibs.beats.getById( - { - kind: 'internal', - }, - 'bar' - ); - - expect(beat!.tags).toEqual(['development', 'production']); - }); - - // it('should add multiple tags to a multiple beats', async () => { - // const { body: apiResponse } = await supertest - // .post('/api/beats/agents_tags/assignments') - // .set('kbn-xsrf', 'xxx') - // .send({ - // assignments: [{ beatId: 'foo', tag: 'development' }, { beatId: 'bar', tag: 'production' }], - // }) - // .expect(200); - - // expect(apiResponse.assignments).to.eql([ - // { status: 200, result: 'updated' }, - // { status: 200, result: 'updated' }, - // ]); - - // let esResponse; - // let beat; - - // // Beat foo - // esResponse = await es.get({ - // index: ES_INDEX_NAME, - // type: ES_TYPE_NAME, - // id: `beat:foo`, - // }); - - // beat = esResponse._source.beat; - // expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it - - // // Beat bar - // esResponse = await es.get({ - // index: ES_INDEX_NAME, - // type: ES_TYPE_NAME, - // id: `beat:bar`, - // }); - - // beat = esResponse._source.beat; - // expect(beat.tags).to.eql(['production']); - // }); - - // it('should return errors for non-existent beats', async () => { - // const nonExistentBeatId = chance.word(); - - // const { body: apiResponse } = await supertest - // .post('/api/beats/agents_tags/assignments') - // .set('kbn-xsrf', 'xxx') - // .send({ - // assignments: [{ beatId: nonExistentBeatId, tag: 'production' }], - // }) - // .expect(200); - - // expect(apiResponse.assignments).to.eql([ - // { status: 404, result: `Beat ${nonExistentBeatId} not found` }, - // ]); - // }); - - // it('should return errors for non-existent tags', async () => { - // const nonExistentTag = chance.word(); - - // const { body: apiResponse } = await supertest - // .post('/api/beats/agents_tags/assignments') - // .set('kbn-xsrf', 'xxx') - // .send({ - // assignments: [{ beatId: 'bar', tag: nonExistentTag }], - // }) - // .expect(200); - - // expect(apiResponse.assignments).to.eql([ - // { status: 404, result: `Tag ${nonExistentTag} not found` }, - // ]); - - // const esResponse = await es.get({ - // index: ES_INDEX_NAME, - // type: ES_TYPE_NAME, - // id: `beat:bar`, - // }); - - // const beat = esResponse._source.beat; - // expect(beat).to.not.have.property('tags'); - // }); - - // it('should return errors for non-existent beats and tags', async () => { - // const nonExistentBeatId = chance.word(); - // const nonExistentTag = chance.word(); - - // const { body: apiResponse } = await supertest - // .post('/api/beats/agents_tags/assignments') - // .set('kbn-xsrf', 'xxx') - // .send({ - // assignments: [{ beatID: nonExistentBeatId, tag: nonExistentTag }], - // }) - // .expect(200); - - // expect(apiResponse.assignments).to.eql([ - // { status: 404, result: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found` }, - // ]); - - // const esResponse = await es.get({ - // index: ES_INDEX_NAME, - // type: ES_TYPE_NAME, - // id: `beat:bar`, - // }); - - // const beat = esResponse._source.beat; - // expect(beat).to.not.have.property('tags'); - // }); -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/data.json b/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/data.json deleted file mode 100644 index 4ee5a4a7e2d55..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/data.json +++ /dev/null @@ -1,178 +0,0 @@ -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "beat:qux", - "source": { - "type": "beat", - "beat": { - "type": "filebeat", - "active": true, - "host_ip": "1.2.3.4", - "host_name": "foo.bar.com", - "id": "qux", - "name": "qux_filebeat", - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "beat:baz", - "source": { - "type": "beat", - "beat": { - "type": "metricbeat", - "active": true, - "host_ip": "22.33.11.44", - "host_name": "baz.bar.com", - "id": "baz", - "name": "baz_metricbeat", - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "beat:foo", - "source": { - "type": "beat", - "beat": { - "type": "metricbeat", - "active": true, - "host_ip": "1.2.3.4", - "host_name": "foo.bar.com", - "id": "foo", - "name": "foo_metricbeat", - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI", - "verified_on": "2018-05-15T16:25:38.924Z", - "tags": [ - "production", - "qa" - ] - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "beat:bar", - "source": { - "type": "beat", - "beat": { - "type": "filebeat", - "active": true, - "host_ip": "11.22.33.44", - "host_name": "foo.com", - "id": "bar", - "name": "bar_filebeat", - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "tag:production", - "source": { - "type": "tag", - "tag": { - "color": "blue" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "tag:development", - "source": { - "type": "tag", - "tag": { - "color": "red" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "tag:qa", - "source": { - "type": "tag", - "tag": { - "color": "green" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "configuration_block:SDfsdfIBdsfsf50zbta", - "source": { - "type": "configuration_block", - "configuration_block": { - "type": "output", - "description": "some description", - "tag": "production", - "last_updated": "2018-05-15T16:25:38.924Z", - "config": "{ \"username\": \"some-username\", \"hosts\": [\"localhost:11211\"] }" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "configuration_block:W0tpsmIBdsfsf50zbta", - "source": { - "type": "configuration_block", - "configuration_block": { - "type": "metricbeat.modules", - "tag": "production", - "last_updated": "2018-05-15T16:25:38.924Z", - "config": "{ \"module\": \"memcached\", \"hosts\": [\"localhost:11211\"] }" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "configuration_block:W0tpsmIBdwcYyG50zbta", - "source": { - "type": "configuration_block", - "configuration_block": { - "type": "metricbeat.modules", - "tag": "qa", - "last_updated": "2018-05-15T16:25:38.924Z", - "config": "{\"module\": \"memcached\", \"node.namespace\": \"node\", \"hosts\": [\"localhost:4949\"] }" - } - } - } -} \ No newline at end of file diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts deleted file mode 100644 index 590ce0bd7b287..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts +++ /dev/null @@ -1,102 +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 { badRequest } from 'boom'; -import { readFile } from 'fs'; -// @ts-ignore -import Hapi from 'hapi'; -import { resolve } from 'path'; -import { promisify } from 'util'; -import { BeatTag, CMBeat } from '../../../common/domain_types'; -import { TokenEnrollmentData } from '../../lib/adapters/tokens/adapter_types'; -import { compose } from '../../lib/compose/testing'; -import { CMServerLibs } from '../../lib/types'; -import { initManagementServer } from './../../management_server'; - -const readFileAsync = promisify(readFile); -let serverLibs: CMServerLibs; - -export const testHarnes = { - description: 'API Development Tests', - loadData: async () => { - if (!serverLibs) { - throw new Error('Server libs not composed yet...'); - } - const contents = await readFileAsync(resolve(__dirname, './data.json'), 'utf8'); - const database = contents.split(/\n\n/); - - // @ts-ignore the private access - serverLibs.beats.adapter.setDB( - database.reduce((inserts: CMBeat[], source) => { - const type = 'beat'; - const data = JSON.parse(source); - - if (data.value.source.type === type) { - inserts.push({ - id: data.value.id.substring(data.value.id.indexOf(':') + 1), - ...data.value.source[type], - }); - } - return inserts; - }, []) - ); - - // @ts-ignore the private access - serverLibs.tags.adapter.setDB( - database.reduce((inserts: BeatTag[], source) => { - const type = 'tag'; - const data = JSON.parse(source); - - if (data.value.source.type === type) { - inserts.push({ - id: data.value.id.substring(data.value.id.indexOf(':') + 1), - ...data.value.source[type], - }); - } - return inserts; - }, []) - ); - - // @ts-ignore the private access - serverLibs.tokens.adapter.setDB( - database.reduce((inserts: TokenEnrollmentData[], source) => { - const type = 'token'; - const data = JSON.parse(source); - - if (data.value.source.type === type) { - inserts.push({ - id: data.value.id.substring(data.value.id.indexOf(':') + 1), - ...data.value.source[type], - }); - } - return inserts; - }, []) - ); - }, - getServerLibs: async () => { - if (!serverLibs) { - const server = new Hapi.Server({ port: 111111 }); - const versionHeader = 'kbn-version'; - const xsrfHeader = 'kbn-xsrf'; - - server.ext('onPostAuth', (req: any, h: any) => { - const isSafeMethod = req.method === 'get' || req.method === 'head'; - const hasVersionHeader = versionHeader in req.headers; - const hasXsrfHeader = xsrfHeader in req.headers; - - if (!isSafeMethod && !hasVersionHeader && !hasXsrfHeader) { - throw badRequest(`Request must contain a ${xsrfHeader} header.`); - } - - return h.continue; - }); - - serverLibs = compose(server); - initManagementServer(serverLibs); - } - return serverLibs; - }, -}; diff --git a/x-pack/legacy/plugins/beats_management/server/utils/README.md b/x-pack/legacy/plugins/beats_management/server/utils/README.md deleted file mode 100644 index 8a6a27aa29867..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/README.md +++ /dev/null @@ -1 +0,0 @@ -Utils should be data processing functions and other tools.... all in all utils is basicly everything that is not an adaptor, or presenter and yet too much to put in a lib. \ No newline at end of file diff --git a/x-pack/legacy/plugins/beats_management/server/utils/find_non_existent_items.ts b/x-pack/legacy/plugins/beats_management/server/utils/find_non_existent_items.ts deleted file mode 100644 index 0e9b4f0b6fa5e..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/find_non_existent_items.ts +++ /dev/null @@ -1,19 +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. - */ - -interface RandomItem { - id: string; - [key: string]: any; -} - -export function findNonExistentItems(items: RandomItem[], requestedItems: any) { - return requestedItems.reduce((nonExistentItems: string[], requestedItem: string, idx: number) => { - if (items.findIndex((item: RandomItem) => item && item.id === requestedItem) === -1) { - nonExistentItems.push(requestedItems[idx]); - } - return nonExistentItems; - }, []); -} diff --git a/x-pack/legacy/plugins/beats_management/server/utils/helper_types.ts b/x-pack/legacy/plugins/beats_management/server/utils/helper_types.ts deleted file mode 100644 index 96f7b7bc79b62..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/helper_types.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export type InterfaceExcept = Pick>; - -export function arrayFromEnum(e: any): T[] { - return Object.keys(e) - .filter((key) => isNaN(+key)) - .map((name) => e[name]) as T[]; -} diff --git a/x-pack/legacy/plugins/beats_management/server/utils/index_templates/beats_template.json b/x-pack/legacy/plugins/beats_management/server/utils/index_templates/beats_template.json deleted file mode 100644 index ba3a0aba6c256..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/index_templates/beats_template.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "index_patterns": [".management-beats"], - "version": 66000, - "settings": { - "index": { - "number_of_shards": 1, - "auto_expand_replicas": "0-1", - "codec": "best_compression" - } - }, - "mappings": { - "_doc": { - "dynamic": "strict", - "properties": { - "type": { - "type": "keyword" - }, - "configuration_block": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "tag": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "config": { - "type": "keyword" - }, - "last_updated": { - "type": "date" - } - } - }, - "enrollment_token": { - "properties": { - "token": { - "type": "keyword" - }, - "expires_on": { - "type": "date" - } - } - }, - "tag": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "color": { - "type": "keyword" - }, - "hasConfigurationBlocksTypes": { - "type": "keyword" - } - } - }, - "beat": { - "properties": { - "id": { - "type": "keyword" - }, - "config_status": { - "type": "keyword" - }, - "active": { - "type": "boolean" - }, - "last_checkin": { - "type": "date" - }, - "enrollment_token": { - "type": "keyword" - }, - "access_token": { - "type": "keyword" - }, - "verified_on": { - "type": "date" - }, - "type": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "host_ip": { - "type": "ip" - }, - "host_name": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "metadata": { - "dynamic": "true", - "type": "object" - }, - "name": { - "type": "keyword" - } - } - } - } - } - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/utils/polyfills.ts b/x-pack/legacy/plugins/beats_management/server/utils/polyfills.ts deleted file mode 100644 index 5291e2c72be7d..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/polyfills.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const entries = (obj: any) => { - const ownProps = Object.keys(obj); - let i = ownProps.length; - const resArray = new Array(i); // preallocate the Array - - while (i--) { - resArray[i] = [ownProps[i], obj[ownProps[i]]]; - } - - return resArray; -}; diff --git a/x-pack/legacy/plugins/beats_management/server/utils/wrap_request.ts b/x-pack/legacy/plugins/beats_management/server/utils/wrap_request.ts deleted file mode 100644 index 57cf70a99a296..0000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/wrap_request.ts +++ /dev/null @@ -1,36 +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 { - FrameworkRequest, - internalAuthData, - KibanaServerRequest, -} from '../lib/adapters/framework/adapter_types'; - -export function wrapRequest( - req: InternalRequest -): FrameworkRequest { - const { params, payload, query, headers, info } = req; - - const isAuthenticated = headers.authorization != null; - - return { - // @ts-ignore -- partial applucation, adapter adds other user data - user: isAuthenticated - ? { - kind: 'authenticated', - [internalAuthData]: headers, - } - : { - kind: 'unauthenticated', - }, - headers, - info, - params, - payload, - query, - }; -} diff --git a/x-pack/legacy/plugins/beats_management/types/eui.d.ts b/x-pack/legacy/plugins/beats_management/types/eui.d.ts deleted file mode 100644 index 636d0a2f7b51e..0000000000000 --- a/x-pack/legacy/plugins/beats_management/types/eui.d.ts +++ /dev/null @@ -1,16 +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. - */ - -/** - * /!\ These type definitions are temporary until the upstream @elastic/eui - * package includes them. - */ - -import * as eui from '@elastic/eui'; -import { Moment } from 'moment'; -import { ChangeEventHandler, MouseEventHandler, ReactType, Ref, FC } from 'react'; - -declare module '@elastic/eui' {} diff --git a/x-pack/legacy/plugins/beats_management/wallaby.js b/x-pack/legacy/plugins/beats_management/wallaby.js deleted file mode 100644 index 823f63b15bcb3..0000000000000 --- a/x-pack/legacy/plugins/beats_management/wallaby.js +++ /dev/null @@ -1,62 +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. - */ -const path = require('path'); -process.env.NODE_PATH = path.resolve(__dirname, '..', '..', '..', 'node_modules'); - -module.exports = function (wallaby) { - return { - debug: true, - files: [ - './tsconfig.json', - //'plugins/beats/public/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', - 'server/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', - 'common/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', - 'public/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', - '!**/*.test.ts', - ], - - tests: ['**/*.test.ts', '**/*.test.tsx'], - env: { - type: 'node', - runner: 'node', - }, - testFramework: { - type: 'jest', - //path: jestPath, - }, - compilers: { - '**/*.ts?(x)': wallaby.compilers.typeScript({ - typescript: require('typescript'), // eslint-disable-line - }), - '**/*.js': wallaby.compilers.babel({ - babelrc: false, - presets: [require.resolve('@kbn/babel-preset/node_preset')], - }), - }, - - setup: (wallaby) => { - const path = require('path'); - - const kibanaDirectory = path.resolve(wallaby.localProjectDir, '..', '..', '..'); - wallaby.testFramework.configure({ - rootDir: wallaby.localProjectDir, - moduleNameMapper: { - '^ui/(.*)': `${kibanaDirectory}/src/legacy/ui/public/$1`, - // eslint-disable-next-line - '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `${kibanaDirectory}/src/dev/jest/mocks/file_mock.js`, - '\\.(css|less|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/style_mock.js`, - }, - testURL: 'http://localhost', - setupFiles: [`${kibanaDirectory}/x-pack/dev-tools/jest/setup/enzyme.js`], - snapshotSerializers: [`${kibanaDirectory}/node_modules/enzyme-to-json/serializer`], - transform: { - '^.+\\.js$': `${kibanaDirectory}/src/dev/jest/babel_transform.js`, - //"^.+\\.tsx?$": `${kibanaDirectory}/src/dev/jest/ts_transform.js`, - }, - }); - }, - }; -}; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 2865bbbe1d944..69fab828e63de 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -386,6 +386,7 @@ describe('getAll()', () => { foo: 'bar', }, }, + score: 1, references: [], }, ], diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 9685f58b8fb31..f494f1358980d 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -1667,6 +1667,7 @@ describe('find()', () => { }, ], }, + score: 1, references: [ { name: 'action_0', diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 796f2992236f9..d71d5f2cb480d 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -5,9 +5,12 @@ */ import { EuiTitle } from '@elastic/eui'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; -import { scaleUtc } from 'd3-scale'; import d3 from 'd3'; +import { scaleUtc } from 'd3-scale'; +import mean from 'lodash.mean'; import React from 'react'; import { asRelativeDateTimeRange } from '../../../../utils/formatters'; import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs'; @@ -17,7 +20,7 @@ import { EmptyMessage } from '../../../shared/EmptyMessage'; interface IBucket { key: number; - count: number; + count: number | undefined; } // TODO: cleanup duplication of this in distribution/get_distribution.ts (ErrorDistributionAPIResponse) and transactions/distribution/index.ts (TransactionDistributionAPIResponse) @@ -30,7 +33,7 @@ interface IDistribution { interface FormattedBucket { x0: number; x: number; - y: number; + y: number | undefined; } export function getFormattedBuckets( @@ -64,7 +67,7 @@ export function ErrorDistribution({ distribution, title }: Props) { distribution.bucketSize ); - if (!buckets || distribution.noHits) { + if (!buckets) { return ( bucket.y)) || 0; const xMin = d3.min(buckets, (d) => d.x0); const xMax = d3.max(buckets, (d) => d.x); const tickFormat = scaleUtc().domain([xMin, xMax]).tickFormat(); @@ -84,6 +88,7 @@ export function ErrorDistribution({ distribution, title }: Props) { {title} bucket.x} xType="time-utc" @@ -105,6 +110,17 @@ export function ErrorDistribution({ distribution, title }: Props) { values: { occCount: value }, }) } + legends={[ + { + color: theme.euiColorVis1, + // 0a abbreviates large whole numbers with metric prefixes like: 1000 = 1k, 32000 = 32k, 1000000 = 1m + legendValue: numeral(averageValue).format('0a'), + title: i18n.translate('xpack.apm.errorGroupDetails.avgLabel', { + defaultMessage: 'Avg.', + }), + legendClickDisabled: true, + }, + ]} /> ); diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index d8885ec11c511..225e5ef2f6ca2 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -26,6 +26,9 @@ import { ErrorDistribution } from './Distribution'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; +import { ErrorRateChart } from '../../shared/charts/ErrorRateChart'; +import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; const Titles = styled.div` margin-bottom: ${px(units.plus)}; @@ -61,49 +64,43 @@ export function ErrorGroupDetails() { const { urlParams, uiFilters } = useUrlParams(); const { serviceName, start, end, errorGroupId } = urlParams; - const { data: errorGroupData } = useFetcher( - (callApmApi) => { - if (serviceName && start && end && errorGroupId) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/{groupId}', - params: { - path: { - serviceName, - groupId: errorGroupId, - }, - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters), - }, + const { data: errorGroupData } = useFetcher(() => { + if (serviceName && start && end && errorGroupId) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/{groupId}', + params: { + path: { + serviceName, + groupId: errorGroupId, }, - }); - } - }, - [serviceName, start, end, errorGroupId, uiFilters] - ); - - const { data: errorDistributionData } = useFetcher( - (callApmApi) => { - if (serviceName && start && end && errorGroupId) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/distribution', - params: { - path: { - serviceName, - }, - query: { - start, - end, - groupId: errorGroupId, - uiFilters: JSON.stringify(uiFilters), - }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), }, - }); - } - }, - [serviceName, start, end, errorGroupId, uiFilters] - ); + }, + }); + } + }, [serviceName, start, end, errorGroupId, uiFilters]); + + const { data: errorDistributionData } = useFetcher(() => { + if (serviceName && start && end && errorGroupId) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/distribution', + params: { + path: { + serviceName, + }, + query: { + start, + end, + groupId: errorGroupId, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + }, [serviceName, start, end, errorGroupId, uiFilters]); useTrackPageview({ app: 'apm', path: 'error_group_details' }); useTrackPageview({ app: 'apm', path: 'error_group_details', delay: 15000 }); @@ -185,16 +182,24 @@ export function ErrorGroupDetails() { )} - - + + + + + + + + + + {showDetails && ( diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index ff031c5a86d11..73474208e26c0 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -13,64 +13,61 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; -import { ErrorGroupList } from './List'; -import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; import { PROJECTION } from '../../../../common/projections/typings'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; +import { ErrorRateChart } from '../../shared/charts/ErrorRateChart'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; +import { ErrorGroupList } from './List'; +import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; const ErrorGroupOverview: React.FC = () => { const { urlParams, uiFilters } = useUrlParams(); const { serviceName, start, end, sortField, sortDirection } = urlParams; - const { data: errorDistributionData } = useFetcher( - (callApmApi) => { - if (serviceName && start && end) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/distribution', - params: { - path: { - serviceName, - }, - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters), - }, + const { data: errorDistributionData } = useFetcher(() => { + if (serviceName && start && end) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/distribution', + params: { + path: { + serviceName, }, - }); - } - }, - [serviceName, start, end, uiFilters] - ); + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + }, [serviceName, start, end, uiFilters]); - const { data: errorGroupListData } = useFetcher( - (callApmApi) => { - const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; + const { data: errorGroupListData } = useFetcher(() => { + const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; - if (serviceName && start && end) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors', - params: { - path: { - serviceName, - }, - query: { - start, - end, - sortField, - sortDirection: normalizedSortDirection, - uiFilters: JSON.stringify(uiFilters), - }, + if (serviceName && start && end) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors', + params: { + path: { + serviceName, }, - }); - } - }, - [serviceName, start, end, sortField, sortDirection, uiFilters] - ); + query: { + start, + end, + sortField, + sortDirection: normalizedSortDirection, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + }, [serviceName, start, end, sortField, sortDirection, uiFilters]); useTrackPageview({ app: 'apm', @@ -102,20 +99,27 @@ const ErrorGroupOverview: React.FC = () => { - - - - - - + + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx index 2199349952d8d..8775cebc0af55 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx @@ -56,18 +56,64 @@ function doZoom(cy: cytoscape.Core | undefined, increment: number) { } } +function useDebugDownloadUrl(cy?: cytoscape.Core) { + const [downloadUrl, setDownloadUrl] = useState(undefined); + const debug = sessionStorage.getItem('apm_debug') === 'true'; + + // Handle elements changes to update the download URL + useEffect(() => { + const elementsHandler: cytoscape.EventHandler = (event) => { + // @ts-ignore The `true` argument to `cy.json` is to flatten the elements + // (instead of having them broken into nodes/edges.) DefinitelyTyped has + // this wrong. + const elementsJson = event.cy.json(true)?.elements.map((element) => ({ + data: element.data, + })); + setDownloadUrl( + elementsJson.length > 0 && debug + ? `data:application/json;charset=utf-8,${encodeURIComponent( + JSON.stringify({ elements: elementsJson }, null, ' ') + )}` + : undefined + ); + }; + + if (cy) { + cy.on('add remove', elementsHandler); + } + + return () => { + if (cy) { + cy.off('add remove', undefined, elementsHandler); + } + }; + }, [cy, debug]); + + return downloadUrl; +} + export function Controls() { const cy = useContext(CytoscapeContext); const { urlParams } = useUrlParams(); const currentSearch = urlParams.kuery ?? ''; const [zoom, setZoom] = useState((cy && cy.zoom()) || 1); + const downloadUrl = useDebugDownloadUrl(cy); + // Handle zoom events useEffect(() => { + const zoomHandler: cytoscape.EventHandler = (event) => { + setZoom(event.cy.zoom()); + }; + if (cy) { - cy.on('zoom', (event) => { - setZoom(event.cy.zoom()); - }); + cy.on('zoom', zoomHandler); } + + return () => { + if (cy) { + cy.off('zoom', undefined, zoomHandler); + } + }; }, [cy]); function center() { @@ -102,6 +148,9 @@ export function Controls() { const centerLabel = i18n.translate('xpack.apm.serviceMap.center', { defaultMessage: 'Center', }); + const downloadLabel = i18n.translate('xpack.apm.serviceMap.download', { + defaultMessage: 'Download', + }); const viewFullMapLabel = i18n.translate('xpack.apm.serviceMap.viewFullMap', { defaultMessage: 'View full service map', }); @@ -165,6 +214,22 @@ export function Controls() { )} + {downloadUrl && ( + + +