diff --git a/.eslintrc.js b/.eslintrc.js index c9f9d96f9ddae..b3d29c9866411 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -529,6 +529,7 @@ module.exports = { 'x-pack/test_utils/**/*', 'x-pack/gulpfile.js', 'x-pack/plugins/apm/public/utils/testHelpers.js', + 'x-pack/plugins/canvas/shareable_runtime/postcss.config.js', ], rules: { 'import/no-extraneous-dependencies': [ diff --git a/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md b/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md index 9bc24f4d1d366..4778c98493b5b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md @@ -4,6 +4,9 @@ ## AssistanceAPIResponse interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md b/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md index 8be7a9edde363..6d3f8df2fa518 100644 --- a/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md @@ -4,6 +4,9 @@ ## AssistantAPIClientParams interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md index e545cf42d3c26..ed64d61e75fab 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md @@ -4,6 +4,9 @@ ## DeprecationAPIClientParams interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md index 1f6e1f9988fc2..1d837d9b4705d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md @@ -4,6 +4,9 @@ ## DeprecationAPIResponse interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md b/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md index bd343f5bc7476..8eeb5ef638a82 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md @@ -4,6 +4,9 @@ ## DeprecationInfo interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md new file mode 100644 index 0000000000000..1ba359e81b9c6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchClientConfig](./kibana-plugin-core-server.elasticsearchclientconfig.md) + +## ElasticsearchClientConfig type + +Configuration options to be used to create a [cluster client](./kibana-plugin-core-server.iclusterclient.md) using the [createClient API](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) + +Signature: + +```typescript +export declare type ElasticsearchClientConfig = Pick & { + pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; + requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; + ssl?: Partial; + keepAlive?: boolean; +}; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.client.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.client.md new file mode 100644 index 0000000000000..591f126c423e3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.client.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) > [client](./kibana-plugin-core-server.elasticsearchservicestart.client.md) + +## ElasticsearchServiceStart.client property + +A pre-configured [Elasticsearch client](./kibana-plugin-core-server.iclusterclient.md) + +Signature: + +```typescript +readonly client: IClusterClient; +``` + +## Example + + +```js +const client = core.elasticsearch.client; + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.createclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.createclient.md new file mode 100644 index 0000000000000..d4a13812ab533 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.createclient.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) > [createClient](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) + +## ElasticsearchServiceStart.createClient property + +Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). + +Signature: + +```typescript +readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; +``` + +## Example + + +```js +const client = elasticsearch.createClient('my-app-name', config); +const data = await client.asInternalUser.search(); + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md index e059acdbd52fa..860867d654435 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md @@ -15,5 +15,7 @@ export interface ElasticsearchServiceStart | Property | Type | Description | | --- | --- | --- | +| [client](./kibana-plugin-core-server.elasticsearchservicestart.client.md) | IClusterClient | A pre-configured [Elasticsearch client](./kibana-plugin-core-server.iclusterclient.md) | +| [createClient](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient | Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). | | [legacy](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) | {
readonly createClient: (type: string, clientConfig?: Partial<LegacyElasticsearchClientConfig>) => ILegacyCustomClusterClient;
readonly client: ILegacyClusterClient;
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md index 8958b49d98b0c..b0dc4d44f7559 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md @@ -21,7 +21,7 @@ registerRouteHandlerContext: (contextName 'myApp', (context, req) => { async function search (id: string) { - return await context.elasticsearch.legacy.client.callAsInternalUser('endpoint', id); + return await context.elasticsearch.client.asCurrentUser.find(id); } return { search }; } diff --git a/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asinternaluser.md b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asinternaluser.md new file mode 100644 index 0000000000000..c7adc345af5a3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asinternaluser.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) > [asInternalUser](./kibana-plugin-core-server.iclusterclient.asinternaluser.md) + +## IClusterClient.asInternalUser property + +A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the ES cluster on behalf of the Kibana internal user + +Signature: + +```typescript +readonly asInternalUser: ElasticsearchClient; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asscoped.md b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asscoped.md new file mode 100644 index 0000000000000..301fcbfee5858 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.asscoped.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) > [asScoped](./kibana-plugin-core-server.iclusterclient.asscoped.md) + +## IClusterClient.asScoped property + +Creates a [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md) bound to given [request](./kibana-plugin-core-server.scopeablerequest.md) + +Signature: + +```typescript +asScoped: (request: ScopeableRequest) => IScopedClusterClient; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.md new file mode 100644 index 0000000000000..f6bacee322538 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iclusterclient.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) + +## IClusterClient interface + +Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). + +Signature: + +```typescript +export interface IClusterClient +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [asInternalUser](./kibana-plugin-core-server.iclusterclient.asinternaluser.md) | ElasticsearchClient | A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the ES cluster on behalf of the Kibana internal user | +| [asScoped](./kibana-plugin-core-server.iclusterclient.asscoped.md) | (request: ScopeableRequest) => IScopedClusterClient | Creates a [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md) bound to given [request](./kibana-plugin-core-server.scopeablerequest.md) | + diff --git a/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.close.md b/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.close.md new file mode 100644 index 0000000000000..5fa2e93cca75b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.close.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md) > [close](./kibana-plugin-core-server.icustomclusterclient.close.md) + +## ICustomClusterClient.close property + +Closes the cluster client. After that client cannot be used and one should create a new client instance to be able to interact with Elasticsearch API. + +Signature: + +```typescript +close: () => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.md new file mode 100644 index 0000000000000..189a50b5d6c20 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.icustomclusterclient.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md) + +## ICustomClusterClient interface + +See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) + +Signature: + +```typescript +export interface ICustomClusterClient extends IClusterClient +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [close](./kibana-plugin-core-server.icustomclusterclient.close.md) | () => Promise<void> | Closes the cluster client. After that client cannot be used and one should create a new client instance to be able to interact with Elasticsearch API. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md index c70a5ac07c6ad..b5fbb3d54b972 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md @@ -4,6 +4,11 @@ ## ILegacyClusterClient type +> Warning: This API is now obsolete. +> +> Use [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). +> + Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). See [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md). diff --git a/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md index a3cb8f1315021..4da121984d084 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md @@ -4,6 +4,11 @@ ## ILegacyCustomClusterClient type +> Warning: This API is now obsolete. +> +> Use [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md). +> + Represents an Elasticsearch cluster API client created by a plugin. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). See [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md). diff --git a/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md index 1263b85acb498..51d0b2e4882cb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md @@ -4,6 +4,11 @@ ## ILegacyScopedClusterClient type +> Warning: This API is now obsolete. +> +> Use [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md). +> + Serves the same purpose as "normal" `ClusterClient` but exposes additional `callAsCurrentUser` method that doesn't use credentials of the Kibana internal user (as `callAsInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API. See [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md). diff --git a/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md b/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md index 00f1659549078..706898c4ad9aa 100644 --- a/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md +++ b/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md @@ -4,6 +4,9 @@ ## IndexSettingsDeprecationInfo interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md new file mode 100644 index 0000000000000..ddc6357bb8835 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) > [asCurrentUser](./kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md) + +## IScopedClusterClient.asCurrentUser property + +A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the user that initiated the request to the Kibana server. + +Signature: + +```typescript +readonly asCurrentUser: ElasticsearchClient; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md new file mode 100644 index 0000000000000..f7f308aa13161 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) > [asInternalUser](./kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md) + +## IScopedClusterClient.asInternalUser property + +A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the internal Kibana user. + +Signature: + +```typescript +readonly asInternalUser: ElasticsearchClient; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md new file mode 100644 index 0000000000000..f39db268288a6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) + +## IScopedClusterClient interface + +Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. + +Signature: + +```typescript +export interface IScopedClusterClient +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [asCurrentUser](./kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md) | ElasticsearchClient | A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the user that initiated the request to the Kibana server. | +| [asInternalUser](./kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md) | ElasticsearchClient | A [client](./kibana-plugin-core-server.elasticsearchclient.md) to be used to query the elasticsearch cluster on behalf of the internal Kibana user. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md b/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md index e6c2878d2b355..168209659046e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md @@ -4,6 +4,9 @@ ## LegacyAPICaller interface +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md b/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md index 9ebe2fc57a54b..40def157114ef 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md @@ -4,6 +4,10 @@ ## LegacyCallAPIOptions interface +> Warning: This API is now obsolete. +> +> + The set of options that defines how API call should be made and result be processed. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md index c51f1858c97a5..668d0b2866a26 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md @@ -4,6 +4,12 @@ ## LegacyClusterClient class +> Warning: This API is now obsolete. +> +> Use [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). +> + +Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md index 62b0f216c863c..78f7bf582d355 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md @@ -4,6 +4,9 @@ ## LegacyElasticsearchClientConfig type +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md index f760780504e55..40fc1a8e05a68 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md @@ -4,6 +4,7 @@ ## LegacyElasticsearchError interface +@deprecated. The new elasticsearch client doesn't wrap errors anymore. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md index c4a94d8661c47..7f752d70921ba 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md @@ -4,6 +4,12 @@ ## LegacyScopedClusterClient class +> Warning: This API is now obsolete. +> +> Use [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md). +> + +Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 95b7627398b45..5347f0b55e19b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -20,9 +20,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [CspConfig](./kibana-plugin-core-server.cspconfig.md) | CSP configuration for use in Kibana. | | [ElasticsearchConfig](./kibana-plugin-core-server.elasticsearchconfig.md) | Wrapper of config schema. | | [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | -| [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md) | | +| [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | | [LegacyElasticsearchErrorHelpers](./kibana-plugin-core-server.legacyelasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | -| [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md) | | +| [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md) | Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional asCurrentUser method that doesn't use credentials of the Kibana internal user (as asInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. | | [RouteValidationError](./kibana-plugin-core-server.routevalidationerror.md) | Error to return when the validation is not successful. | | [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) | | | [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | | @@ -98,20 +98,23 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpServerInfo](./kibana-plugin-core-server.httpserverinfo.md) | | | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | 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, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. | | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | | +| [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | | [IContextContainer](./kibana-plugin-core-server.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | | [ICspConfig](./kibana-plugin-core-server.icspconfig.md) | CSP configuration for use in Kibana. | +| [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md) | See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) | | [IKibanaResponse](./kibana-plugin-core-server.ikibanaresponse.md) | A response data object, expected to returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution | | [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) | A tiny abstraction for TCP socket. | | [ImageValidation](./kibana-plugin-core-server.imagevalidation.md) | | | [IndexSettingsDeprecationInfo](./kibana-plugin-core-server.indexsettingsdeprecationinfo.md) | | | [IRenderOptions](./kibana-plugin-core-server.irenderoptions.md) | | | [IRouter](./kibana-plugin-core-server.irouter.md) | Registers route handlers for specified resource path and method. See [RouteConfig](./kibana-plugin-core-server.routeconfig.md) and [RequestHandler](./kibana-plugin-core-server.requesthandler.md) for more information about arguments to route registrations. | +| [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) | Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional asCurrentUser method that doesn't use credentials of the Kibana internal user (as asInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. | | [IUiSettingsClient](./kibana-plugin-core-server.iuisettingsclient.md) | Server-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. | | [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) | Request events. | | [KibanaRequestRoute](./kibana-plugin-core-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | | [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md) | | | [LegacyCallAPIOptions](./kibana-plugin-core-server.legacycallapioptions.md) | The set of options that defines how API call should be made and result be processed. | -| [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | | +| [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | @deprecated. The new elasticsearch client doesn't wrap errors anymore. | | [LegacyRequest](./kibana-plugin-core-server.legacyrequest.md) | | | [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) | | | [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) | | @@ -137,7 +140,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | | [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | -| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | +| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request - [uiSettings.auditor](./kibana-plugin-core-server.auditor.md) - AuditTrail client scoped to the incoming request | | [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md) | Additional route options. | | [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md) | Additional body options for a route | @@ -236,6 +239,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ConfigPath](./kibana-plugin-core-server.configpath.md) | | | [DestructiveRouteMethod](./kibana-plugin-core-server.destructiveroutemethod.md) | Set of HTTP methods changing the state of the server. | | [ElasticsearchClient](./kibana-plugin-core-server.elasticsearchclient.md) | Client used to query the elasticsearch cluster. | +| [ElasticsearchClientConfig](./kibana-plugin-core-server.elasticsearchclientconfig.md) | Configuration options to be used to create a [cluster client](./kibana-plugin-core-server.iclusterclient.md) using the [createClient API](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) | | [Freezable](./kibana-plugin-core-server.freezable.md) | | | [GetAuthHeaders](./kibana-plugin-core-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. | | [GetAuthState](./kibana-plugin-core-server.getauthstate.md) | Gets authentication state for a request. Returned by auth interceptor. | diff --git a/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md b/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md index 69fb573d36530..a924f0cea6b6b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md +++ b/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md @@ -4,6 +4,9 @@ ## MIGRATION\_ASSISTANCE\_INDEX\_ACTION type +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md b/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md index c3256eaa78331..0fcae8c847cb4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md +++ b/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md @@ -4,6 +4,9 @@ ## MIGRATION\_DEPRECATION\_LEVEL type +> Warning: This API is now obsolete. +> +> Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md index 2d31c24a077cb..5b8492ec5ece1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md @@ -13,6 +13,7 @@ core: { typeRegistry: ISavedObjectTypeRegistry; }; elasticsearch: { + client: IScopedClusterClient; legacy: { client: ILegacyScopedClusterClient; }; diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index 07e6dcbdae125..4e530973f9d50 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -6,7 +6,7 @@ Plugin specific context passed to a route handler. -Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request +Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request - [uiSettings.auditor](./kibana-plugin-core-server.auditor.md) - AuditTrail client scoped to the incoming request Signature: @@ -18,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
auditor: Auditor;
} | | +| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
client: IScopedClusterClient;
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
auditor: Auditor;
} | | diff --git a/docs/visualize/images/vega_tutorial_getting_help.png b/docs/visualize/images/vega_tutorial_getting_help.png new file mode 100644 index 0000000000000..698a4eb889c8c Binary files /dev/null and b/docs/visualize/images/vega_tutorial_getting_help.png differ diff --git a/docs/visualize/images/vega_tutorial_inspect_data_sets.png b/docs/visualize/images/vega_tutorial_inspect_data_sets.png new file mode 100644 index 0000000000000..027841af934d6 Binary files /dev/null and b/docs/visualize/images/vega_tutorial_inspect_data_sets.png differ diff --git a/docs/visualize/images/vega_tutorial_inspect_requests.png b/docs/visualize/images/vega_tutorial_inspect_requests.png new file mode 100644 index 0000000000000..8b9093be9b18d Binary files /dev/null and b/docs/visualize/images/vega_tutorial_inspect_requests.png differ diff --git a/docs/visualize/vega.asciidoc b/docs/visualize/vega.asciidoc index 3a1c57da93f07..9b8c32d7e41f0 100644 --- a/docs/visualize/vega.asciidoc +++ b/docs/visualize/vega.asciidoc @@ -1505,6 +1505,46 @@ Vega can load data from any URL, but this is disabled by default in {kib}. To change this, set `vis_type_vega.enableExternalUrls: true` in `kibana.yml`, then restart {kib}. +[[vega-inspector]] +==== Vega Inspector +Use the contextual *Inspect* tool to gain insights into different elements. +For Vega visualizations, there are two different views: *Request* and *Vega debug*. + +===== Inspect Elasticsearch requests + +Vega uses the {ref}/search-search.html[{es} search API] to get documents and aggregation +results from {es}. To troubleshoot these requests, click *Inspect*, which shows the most recent requests. +In case your specification has more than one request, you can switch between the views using the *View* dropdown. + +[role="screenshot"] +image::visualize/images/vega_tutorial_inspect_requests.png[] + +===== Vega debugging + +With the *Vega debug* view, you can inspect the *Data sets* and *Signal Values* runtime data. + +The runtime data is read from the +https://vega.github.io/vega/docs/api/debugging/#scope[runtime scope]. + +[role="screenshot"] +image::visualize/images/vega_tutorial_inspect_data_sets.png[] + +To debug more complex specs, access to the `view` variable. For more information, refer to +the <>. + +===== Asking for help with a Vega spec + +Because of the dynamic nature of the data in {es}, it is hard to help you with +Vega specs unless you can share a dataset. To do this, click *Inspect*, select the *Vega debug* view, +then select the *Spec* tab: + +[role="screenshot"] +image::visualize/images/vega_tutorial_getting_help.png[] + +To copy the response, click *Copy to clipboard*. Paste the copied data to +https://gist.github.com/[gist.github.com], possibly with a .json extension. Use the [raw] button, +and share that when asking for help. + [[vega-browser-debugging-console]] ==== Browser debugging console @@ -1522,31 +1562,6 @@ of Vega-Lite, this is the output of the Vega-Lite compiler. * `vegalite_spec` — If this is a Vega-Lite graph, JSON specification of the graph before Vega-Lite compilation. -[[vega-data]] -==== Debugging data - -experimental[] If you are using an {es} query, make sure your resulting data is -what you expected. The easiest way to view it is by using the "networking" -tab in the browser debugging tools (for example, F12). Modify the graph slightly -so that it makes a search request, and view the response from the -server. Another approach is to use -https://www.elastic.co/guide/en/kibana/current/console-kibana.html[Dev Tools]. Place the index name into the first line: -`GET /_search`, then add your query as the following lines -(just the value of the `"query"` field). - -[[vega-getting-help]] -==== Asking for help with a Vega spec - -Because of the dynamic nature of the data in {es}, it is hard to help you with -Vega specs unless you can share a dataset. To do this, use the browser developer -tools and type: - -`JSON.stringify(VEGA_DEBUG.vegalite_spec, null, 2)` - -Copy the response to https://gist.github.com/[gist.github.com], possibly -with a `.json` extension, use the `[raw]` button, and share that when -asking for help. - [float] [[vega-expression-functions]] ==== (Vega only) Expression functions which can update the time range and dashboard filters diff --git a/package.json b/package.json index 51a41cbbab9ff..880534997cff0 100644 --- a/package.json +++ b/package.json @@ -480,7 +480,7 @@ "pixelmatch": "^5.1.0", "pkg-up": "^2.0.0", "pngjs": "^3.4.0", - "postcss": "^7.0.26", + "postcss": "^7.0.32", "postcss-url": "^8.0.0", "prettier": "^2.0.5", "proxyquire": "1.8.0", diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index c11bd1b646933..4fbbc920c4447 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -36,6 +36,7 @@ "loader-utils": "^1.2.3", "node-sass": "^4.13.0", "normalize-path": "^3.0.0", + "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "resolve-url-loader": "^3.1.1", diff --git a/packages/kbn-optimizer/src/worker/postcss.config.js b/packages/kbn-optimizer/postcss.config.js similarity index 100% rename from packages/kbn-optimizer/src/worker/postcss.config.js rename to packages/kbn-optimizer/postcss.config.js diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json new file mode 100644 index 0000000000000..10602d2e7981a --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json @@ -0,0 +1,4 @@ +{ + "id": "baz", + "ui": true +} diff --git a/packages/kbn-ui-framework/doc_site/postcss.config.js b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/public/index.ts similarity index 91% rename from packages/kbn-ui-framework/doc_site/postcss.config.js rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/public/index.ts index 571bae86dee37..7313de07be04c 100644 --- a/packages/kbn-ui-framework/doc_site/postcss.config.js +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/public/index.ts @@ -17,6 +17,5 @@ * under the License. */ -module.exports = { - plugins: [require('autoprefixer')()], -}; +// eslint-disable-next-line no-console +console.log('plugin in an x-pack dir'); diff --git a/packages/kbn-optimizer/src/common/bundle.test.ts b/packages/kbn-optimizer/src/common/bundle.test.ts index 6197a08485854..b8f9b94379f20 100644 --- a/packages/kbn-optimizer/src/common/bundle.test.ts +++ b/packages/kbn-optimizer/src/common/bundle.test.ts @@ -48,6 +48,7 @@ it('creates cache keys', () => { "/foo/bar/c": 789, }, "spec": Object { + "banner": undefined, "contextDir": "/foo/bar", "id": "bar", "manifestPath": undefined, @@ -80,6 +81,7 @@ it('parses bundles from JSON specs', () => { expect(bundles).toMatchInlineSnapshot(` Array [ Bundle { + "banner": undefined, "cache": BundleCache { "path": "/foo/bar/target/.kbn-optimizer-cache", "state": undefined, diff --git a/packages/kbn-optimizer/src/common/bundle.ts b/packages/kbn-optimizer/src/common/bundle.ts index a354da7a21521..25b37ace09a8f 100644 --- a/packages/kbn-optimizer/src/common/bundle.ts +++ b/packages/kbn-optimizer/src/common/bundle.ts @@ -43,6 +43,8 @@ export interface BundleSpec { readonly sourceRoot: string; /** Absolute path to the directory where output should be written */ readonly outputDir: string; + /** Banner that should be written to all bundle JS files */ + readonly banner?: string; /** Absolute path to a kibana.json manifest file, if omitted we assume there are not dependenices */ readonly manifestPath?: string; } @@ -64,6 +66,8 @@ export class Bundle { public readonly sourceRoot: BundleSpec['sourceRoot']; /** Absolute path to the output directory for this bundle */ public readonly outputDir: BundleSpec['outputDir']; + /** Banner that should be written to all bundle JS files */ + public readonly banner: BundleSpec['banner']; /** * Absolute path to a manifest file with "requiredBundles" which will be * used to allow bundleRefs from this bundle to the exports of another bundle. @@ -81,6 +85,7 @@ export class Bundle { this.sourceRoot = spec.sourceRoot; this.outputDir = spec.outputDir; this.manifestPath = spec.manifestPath; + this.banner = spec.banner; this.cache = new BundleCache(Path.resolve(this.outputDir, '.kbn-optimizer-cache')); } @@ -112,6 +117,7 @@ export class Bundle { sourceRoot: this.sourceRoot, outputDir: this.outputDir, manifestPath: this.manifestPath, + banner: this.banner, }; } @@ -220,6 +226,13 @@ export function parseBundles(json: string) { } } + const { banner } = spec; + if (banner !== undefined) { + if (!(typeof banner === 'string')) { + throw new Error('`bundles[]` must have a string `banner` property'); + } + } + return new Bundle({ type, id, @@ -227,6 +240,7 @@ export function parseBundles(json: string) { contextDir, sourceRoot, outputDir, + banner, manifestPath, }); } diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 109188e163d06..5f44d8068e694 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -4,6 +4,7 @@ exports[`builds expected bundles, saves bundle counts to metadata: OptimizerConf OptimizerConfig { "bundles": Array [ Bundle { + "banner": undefined, "cache": BundleCache { "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public/.kbn-optimizer-cache, "state": undefined, @@ -19,6 +20,7 @@ OptimizerConfig { "type": "plugin", }, Bundle { + "banner": undefined, "cache": BundleCache { "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public/.kbn-optimizer-cache, "state": undefined, @@ -33,6 +35,24 @@ OptimizerConfig { "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "type": "plugin", }, + Bundle { + "banner": "/*! 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. */ +", + "cache": BundleCache { + "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/target/public/.kbn-optimizer-cache, + "state": undefined, + }, + "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz, + "id": "baz", + "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, + "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/target/public, + "publicDirNames": Array [ + "public", + ], + "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, + "type": "plugin", + }, ], "cache": true, "dist": false, @@ -60,6 +80,13 @@ OptimizerConfig { "isUiPlugin": false, "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/nested/baz/kibana.json, }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz, + "extraPublicDirs": Array [], + "id": "baz", + "isUiPlugin": true, + "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, + }, ], "profileWebpack": false, "repoRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, @@ -73,6 +100,11 @@ OptimizerConfig { exports[`prepares assets for distribution: bar bundle 1`] = `"(function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!==\\"undefined\\"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"})}Object.defineProperty(exports,\\"__esModule\\",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value===\\"object\\"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,\\"default\\",{enumerable:true,value:value});if(mode&2&&typeof value!=\\"string\\")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module[\\"default\\"]}:function getModuleExports(){return module};__webpack_require__.d(getter,\\"a\\",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p=\\"\\";return __webpack_require__(__webpack_require__.s=5)})([function(module,exports,__webpack_require__){\\"use strict\\";var isOldIE=function isOldIE(){var memo;return function memorize(){if(typeof memo===\\"undefined\\"){memo=Boolean(window&&document&&document.all&&!window.atob)}return memo}}();var getTarget=function getTarget(){var memo={};return function memorize(target){if(typeof memo[target]===\\"undefined\\"){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement){try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}}memo[target]=styleTarget}return memo[target]}}();var stylesInDom=[];function getIndexByIdentifier(identifier){var result=-1;for(var i=0;i { it('builds expected bundles, saves bundle counts to metadata', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, - pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], + pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins'), Path.resolve(MOCK_REPO_DIR, 'x-pack')], maxWorkerCount: 1, dist: false, }); @@ -100,7 +100,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { (msg.event?.type === 'bundle cached' || msg.event?.type === 'bundle not cached') && msg.state.phase === 'initializing' ); - assert('produce two bundle cache events while initializing', bundleCacheStates.length === 2); + assert('produce three bundle cache events while initializing', bundleCacheStates.length === 3); const initializedStates = msgs.filter((msg) => msg.state.phase === 'initialized'); assert('produce at least one initialized event', initializedStates.length >= 1); @@ -110,17 +110,17 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { const runningStates = msgs.filter((msg) => msg.state.phase === 'running'); assert( - 'produce two or three "running" states', - runningStates.length === 2 || runningStates.length === 3 + 'produce three to five "running" states', + runningStates.length >= 3 && runningStates.length <= 5 ); const bundleNotCachedEvents = msgs.filter((msg) => msg.event?.type === 'bundle not cached'); - assert('produce two "bundle not cached" events', bundleNotCachedEvents.length === 2); + assert('produce three "bundle not cached" events', bundleNotCachedEvents.length === 3); const successStates = msgs.filter((msg) => msg.state.phase === 'success'); assert( - 'produce one or two "compiler success" states', - successStates.length === 1 || successStates.length === 2 + 'produce one to three "compiler success" states', + successStates.length >= 1 && successStates.length <= 3 ); const otherStates = msgs.filter( @@ -161,6 +161,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { Array [ /node_modules/css-loader/package.json, /node_modules/style-loader/package.json, + /packages/kbn-optimizer/postcss.config.js, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, @@ -171,7 +172,20 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/styles/_globals_v7dark.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/styles/_globals_v7light.scss, /packages/kbn-optimizer/target/worker/entry_point_creator.js, - /packages/kbn-optimizer/target/worker/postcss.config.js, + /packages/kbn-ui-shared-deps/public_path_module_creator.js, + ] + `); + + const baz = config.bundles.find((b) => b.id === 'baz')!; + expect(baz).toBeTruthy(); + baz.cache.refresh(); + expect(baz.cache.getModuleCount()).toBe(3); + + expect(baz.cache.getReferencedFiles()).toMatchInlineSnapshot(` + Array [ + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/public/index.ts, + /packages/kbn-optimizer/target/worker/entry_point_creator.js, /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); @@ -180,7 +194,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { it('uses cache on second run and exist cleanly', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, - pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], + pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins'), Path.resolve(MOCK_REPO_DIR, 'x-pack')], maxWorkerCount: 1, dist: false, }); @@ -202,6 +216,7 @@ it('uses cache on second run and exist cleanly', async () => { "initializing", "initializing", "initializing", + "initializing", "initialized", "success", ] @@ -211,7 +226,7 @@ it('uses cache on second run and exist cleanly', async () => { it('prepares assets for distribution', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, - pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], + pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins'), Path.resolve(MOCK_REPO_DIR, 'x-pack')], maxWorkerCount: 1, dist: true, }); @@ -224,6 +239,7 @@ it('prepares assets for distribution', async () => { 'foo async bundle' ); expectFileMatchesSnapshotWithCompression('plugins/bar/target/public/bar.plugin.js', 'bar bundle'); + expectFileMatchesSnapshotWithCompression('x-pack/baz/target/public/baz.plugin.js', 'baz bundle'); }); /** diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts index a70cfc759dd55..a823f66cf767b 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts @@ -48,12 +48,20 @@ it('returns a bundle for core and each plugin', () => { extraPublicDirs: [], manifestPath: '/outside/of/repo/plugins/baz/kibana.json', }, + { + directory: '/repo/x-pack/plugins/box', + id: 'box', + isUiPlugin: true, + extraPublicDirs: [], + manifestPath: '/repo/x-pack/plugins/box/kibana.json', + }, ], '/repo' ).map((b) => b.toSpec()) ).toMatchInlineSnapshot(` Array [ Object { + "banner": undefined, "contextDir": /plugins/foo, "id": "foo", "manifestPath": /plugins/foo/kibana.json, @@ -65,6 +73,7 @@ it('returns a bundle for core and each plugin', () => { "type": "plugin", }, Object { + "banner": undefined, "contextDir": "/outside/of/repo/plugins/baz", "id": "baz", "manifestPath": "/outside/of/repo/plugins/baz/kibana.json", @@ -75,6 +84,20 @@ it('returns a bundle for core and each plugin', () => { "sourceRoot": , "type": "plugin", }, + Object { + "banner": "/*! 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. */ + ", + "contextDir": /x-pack/plugins/box, + "id": "box", + "manifestPath": /x-pack/plugins/box/kibana.json, + "outputDir": /x-pack/plugins/box/target/public, + "publicDirNames": Array [ + "public", + ], + "sourceRoot": , + "type": "plugin", + }, ] `); }); diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts index 04ab992addeec..9350b9464242a 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts @@ -24,6 +24,8 @@ import { Bundle } from '../common'; import { KibanaPlatformPlugin } from './kibana_platform_plugins'; export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: string) { + const xpackDirSlash = Path.resolve(repoRoot, 'x-pack') + Path.sep; + return plugins .filter((p) => p.isUiPlugin) .map( @@ -36,6 +38,10 @@ export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: stri contextDir: p.directory, outputDir: Path.resolve(p.directory, 'target/public'), manifestPath: p.manifestPath, + banner: p.directory.startsWith(xpackDirSlash) + ? `/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements.\n` + + ` * Licensed under the Elastic License; you may not use this file except in compliance with the Elastic License. */\n` + : undefined, }) ); } diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts index c7be943d65a48..d78eb8214f607 100644 --- a/packages/kbn-optimizer/src/worker/run_compilers.ts +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -110,7 +110,7 @@ const observeCompiler = ( const bundleRefExportIds: string[] = []; const referencedFiles = new Set(); - let normalModuleCount = 0; + let moduleCount = 0; let workUnits = stats.compilation.fileDependencies.size; if (bundle.manifestPath) { @@ -119,7 +119,7 @@ const observeCompiler = ( for (const module of stats.compilation.modules) { if (isNormalModule(module)) { - normalModuleCount += 1; + moduleCount += 1; const path = getModulePath(module); const parsedPath = parseFilePath(path); @@ -154,7 +154,12 @@ const observeCompiler = ( continue; } - if (isExternalModule(module) || isIgnoredModule(module) || isConcatenatedModule(module)) { + if (isConcatenatedModule(module)) { + moduleCount += module.modules.length; + continue; + } + + if (isExternalModule(module) || isIgnoredModule(module)) { continue; } @@ -180,13 +185,13 @@ const observeCompiler = ( bundleRefExportIds, optimizerCacheKey: workerConfig.optimizerCacheKey, cacheKey: bundle.createCacheKey(files, mtimes), - moduleCount: normalModuleCount, + moduleCount, workUnits, files, }); return compilerMsgs.compilerSuccess({ - moduleCount: normalModuleCount, + moduleCount, }); }) ); diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 271ad49aee351..ae5d2b5fb3292 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -72,6 +72,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: new CleanWebpackPlugin(), new DisallowedSyntaxPlugin(), new BundleRefsPlugin(bundle, bundleRefs), + ...(bundle.banner ? [new webpack.BannerPlugin({ banner: bundle.banner, raw: true })] : []), ], module: { @@ -151,7 +152,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: options: { sourceMap: !worker.dist, config: { - path: require.resolve('./postcss.config'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/packages/kbn-optimizer/src/worker/webpack_helpers.ts b/packages/kbn-optimizer/src/worker/webpack_helpers.ts index e30920b960144..a1f97c4314774 100644 --- a/packages/kbn-optimizer/src/worker/webpack_helpers.ts +++ b/packages/kbn-optimizer/src/worker/webpack_helpers.ts @@ -155,6 +155,7 @@ export interface WebpackConcatenatedModule { id: number; dependencies: Dependency[]; usedExports: string[]; + modules: unknown[]; } export function isConcatenatedModule(module: any): module is WebpackConcatenatedModule { diff --git a/packages/kbn-storybook/lib/webpack.dll.config.js b/packages/kbn-storybook/lib/webpack.dll.config.js index 740ee3819c36f..661312b9a0581 100644 --- a/packages/kbn-storybook/lib/webpack.dll.config.js +++ b/packages/kbn-storybook/lib/webpack.dll.config.js @@ -127,7 +127,7 @@ module.exports = { loader: 'postcss-loader', options: { config: { - path: path.resolve(REPO_ROOT, 'src/optimize/postcss.config.js'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js index b2df4f40d4fbe..0a9977463aee8 100644 --- a/packages/kbn-storybook/storybook_config/webpack.config.js +++ b/packages/kbn-storybook/storybook_config/webpack.config.js @@ -91,7 +91,7 @@ module.exports = async ({ config }) => { loader: 'postcss-loader', options: { config: { - path: resolve(REPO_ROOT, 'src/optimize/'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/packages/kbn-ui-framework/Gruntfile.js b/packages/kbn-ui-framework/Gruntfile.js index b7ba1e87b2f00..bb8e7b72cb7bd 100644 --- a/packages/kbn-ui-framework/Gruntfile.js +++ b/packages/kbn-ui-framework/Gruntfile.js @@ -19,7 +19,7 @@ const sass = require('node-sass'); const postcss = require('postcss'); -const postcssConfig = require('../../src/optimize/postcss.config'); +const postcssConfig = require('@kbn/optimizer/postcss.config.js'); const chokidar = require('chokidar'); const { debounce } = require('lodash'); diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index abf64906e0253..7933ce06d6847 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -33,7 +33,7 @@ "@babel/core": "^7.10.2", "@elastic/eui": "0.0.55", "@kbn/babel-preset": "1.0.0", - "autoprefixer": "^9.7.4", + "@kbn/optimizer": "1.0.0", "babel-loader": "^8.0.6", "brace": "0.11.1", "chalk": "^2.4.2", @@ -54,7 +54,7 @@ "keymirror": "0.1.1", "moment": "^2.24.0", "node-sass": "^4.13.1", - "postcss": "^7.0.26", + "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "react-dom": "^16.12.0", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 8398d1c081da6..3c03a52383f77 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -21,6 +21,7 @@ "custom-event-polyfill": "^0.3.0", "elasticsearch-browser": "^16.7.0", "jquery": "^3.5.0", + "mini-css-extract-plugin": "0.8.0", "moment": "^2.24.0", "moment-timezone": "^0.5.27", "react": "^16.12.0", diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 3fdab481dc750..4facbe1ffbb07 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -20,7 +20,7 @@ // eslint-disable-next-line no-restricted-syntax const alwaysImportedTests = [ require.resolve('../test/functional/config.js'), - require.resolve('../test/plugin_functional/config.js'), + require.resolve('../test/plugin_functional/config.ts'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), require.resolve('../test/new_visualize_flow/config.js'), ]; diff --git a/src/core/server/elasticsearch/client/mocks.ts b/src/core/server/elasticsearch/client/mocks.ts index c93294404b52f..2f2ca08fee6f2 100644 --- a/src/core/server/elasticsearch/client/mocks.ts +++ b/src/core/server/elasticsearch/client/mocks.ts @@ -70,15 +70,14 @@ const createInternalClientMock = (): DeeplyMockedKeys => { return (mock as unknown) as DeeplyMockedKeys; }; -// TODO fix naming ElasticsearchClientMock -export type ElasticSearchClientMock = DeeplyMockedKeys; +export type ElasticsearchClientMock = DeeplyMockedKeys; -const createClientMock = (): ElasticSearchClientMock => - (createInternalClientMock() as unknown) as ElasticSearchClientMock; +const createClientMock = (): ElasticsearchClientMock => + (createInternalClientMock() as unknown) as ElasticsearchClientMock; interface ScopedClusterClientMock { - asInternalUser: ElasticSearchClientMock; - asCurrentUser: ElasticSearchClientMock; + asInternalUser: ElasticsearchClientMock; + asCurrentUser: ElasticsearchClientMock; } const createScopedClusterClientMock = () => { @@ -91,7 +90,7 @@ const createScopedClusterClientMock = () => { }; export interface ClusterClientMock { - asInternalUser: ElasticSearchClientMock; + asInternalUser: ElasticsearchClientMock; asScoped: jest.MockedFunction<() => ScopedClusterClientMock>; } @@ -157,7 +156,7 @@ export const elasticsearchClientMock = { createClusterClient: createClusterClientMock, createCustomClusterClient: createCustomClusterClientMock, createScopedClusterClient: createScopedClusterClientMock, - createElasticSearchClient: createClientMock, + createElasticsearchClient: createClientMock, createInternalClient: createInternalClientMock, createSuccessTransportRequestPromise, createErrorTransportRequestPromise, diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts index 3aa47e8b40e24..c9366c575ba74 100644 --- a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts +++ b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts @@ -27,10 +27,10 @@ const createErrorReturn = (err: any) => elasticsearchClientMock.createErrorTransportRequestPromise(err); describe('retryCallCluster', () => { - let client: ReturnType; + let client: ReturnType; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); }); it('returns response from ES API call in case of success', async () => { @@ -91,11 +91,11 @@ describe('retryCallCluster', () => { }); describe('migrationRetryCallCluster', () => { - let client: ReturnType; + let client: ReturnType; let logger: ReturnType; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); logger = loggingSystemMock.createLogger(); }); diff --git a/src/core/server/elasticsearch/client/scoped_cluster_client.test.ts b/src/core/server/elasticsearch/client/scoped_cluster_client.test.ts index 78ca8fcbd3c07..4288c6bf6421d 100644 --- a/src/core/server/elasticsearch/client/scoped_cluster_client.test.ts +++ b/src/core/server/elasticsearch/client/scoped_cluster_client.test.ts @@ -22,8 +22,8 @@ import { ScopedClusterClient } from './scoped_cluster_client'; describe('ScopedClusterClient', () => { it('uses the internal client passed in the constructor', () => { - const internalClient = elasticsearchClientMock.createElasticSearchClient(); - const scopedClient = elasticsearchClientMock.createElasticSearchClient(); + const internalClient = elasticsearchClientMock.createElasticsearchClient(); + const scopedClient = elasticsearchClientMock.createElasticsearchClient(); const scopedClusterClient = new ScopedClusterClient(internalClient, scopedClient); @@ -31,8 +31,8 @@ describe('ScopedClusterClient', () => { }); it('uses the scoped client passed in the constructor', () => { - const internalClient = elasticsearchClientMock.createElasticSearchClient(); - const scopedClient = elasticsearchClientMock.createElasticSearchClient(); + const internalClient = elasticsearchClientMock.createElasticsearchClient(); + const scopedClient = elasticsearchClientMock.createElasticsearchClient(); const scopedClusterClient = new ScopedClusterClient(internalClient, scopedClient); diff --git a/src/core/server/elasticsearch/client/scoped_cluster_client.ts b/src/core/server/elasticsearch/client/scoped_cluster_client.ts index 1af7948a65e16..05ab67073f9e1 100644 --- a/src/core/server/elasticsearch/client/scoped_cluster_client.ts +++ b/src/core/server/elasticsearch/client/scoped_cluster_client.ts @@ -20,7 +20,7 @@ import { ElasticsearchClient } from './types'; /** - * Serves the same purpose as the normal {@link ClusterClient | cluster client} but exposes + * Serves the same purpose as the normal {@link IClusterClient | cluster client} but exposes * an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal * user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers * extracted from the current user request to the API instead. diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index b97f6df6b0afc..501ab619316c2 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -24,6 +24,7 @@ import { ClusterClientMock, CustomClusterClientMock, } from './client/mocks'; +import { ElasticsearchClientConfig } from './client'; import { legacyClientMock } from './legacy/mocks'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; @@ -38,12 +39,12 @@ interface MockedElasticSearchServiceSetup { }; } -type MockedElasticSearchServiceStart = MockedElasticSearchServiceSetup; - -interface MockedInternalElasticSearchServiceStart extends MockedElasticSearchServiceStart { +type MockedElasticSearchServiceStart = MockedElasticSearchServiceSetup & { client: ClusterClientMock; - createClient: jest.MockedFunction<() => CustomClusterClientMock>; -} + createClient: jest.MockedFunction< + (name: string, config?: Partial) => CustomClusterClientMock + >; +}; const createSetupContractMock = () => { const setupContract: MockedElasticSearchServiceSetup = { @@ -61,6 +62,8 @@ const createSetupContractMock = () => { const createStartContractMock = () => { const startContract: MockedElasticSearchServiceStart = { + client: elasticsearchClientMock.createClusterClient(), + createClient: jest.fn(), legacy: { createClient: jest.fn(), client: legacyClientMock.createClusterClient(), @@ -70,20 +73,13 @@ const createStartContractMock = () => { startContract.legacy.client.asScoped.mockReturnValue( legacyClientMock.createScopedClusterClient() ); + startContract.createClient.mockImplementation(() => + elasticsearchClientMock.createCustomClusterClient() + ); return startContract; }; -const createInternalStartContractMock = () => { - const startContract: MockedInternalElasticSearchServiceStart = { - ...createStartContractMock(), - client: elasticsearchClientMock.createClusterClient(), - createClient: jest.fn(), - }; - - startContract.createClient.mockReturnValue(elasticsearchClientMock.createCustomClusterClient()); - - return startContract; -}; +const createInternalStartContractMock = createStartContractMock; type MockedInternalElasticSearchServiceSetup = jest.Mocked< InternalElasticsearchServiceSetup & { @@ -136,4 +132,6 @@ export const elasticsearchServiceMock = { createLegacyCustomClusterClient: legacyClientMock.createCustomClusterClient, createLegacyScopedClusterClient: legacyClientMock.createScopedClusterClient, createLegacyElasticsearchClient: legacyClientMock.createElasticsearchClient, + + ...elasticsearchClientMock, }; diff --git a/src/core/server/elasticsearch/legacy/api_types.ts b/src/core/server/elasticsearch/legacy/api_types.ts index b9699ab290e3f..896a58e085d49 100644 --- a/src/core/server/elasticsearch/legacy/api_types.ts +++ b/src/core/server/elasticsearch/legacy/api_types.ts @@ -150,6 +150,7 @@ import { * processed. * * @public + * @deprecated */ export interface LegacyCallAPIOptions { /** @@ -165,7 +166,10 @@ export interface LegacyCallAPIOptions { signal?: AbortSignal; } -/** @public */ +/** + * @deprecated + * @public + * */ export interface LegacyAPICaller { /* eslint-disable */ (endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: LegacyCallAPIOptions): ReturnType; @@ -317,18 +321,30 @@ export interface LegacyAPICaller { /* eslint-enable */ } -/** @public */ +/** + * @deprecated + * @public + * */ export interface AssistantAPIClientParams extends GenericParams { path: '/_migration/assistance'; method: 'GET'; } -/** @public */ +/** + * @deprecated + * @public + * */ export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; -/** @public */ +/** + * @deprecated + * @public + * */ export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; -/** @public */ +/** + * @deprecated + * @public + * */ export interface AssistanceAPIResponse { indices: { [indexName: string]: { @@ -337,13 +353,19 @@ export interface AssistanceAPIResponse { }; } -/** @public */ +/** + * @deprecated + * @public + * */ export interface DeprecationAPIClientParams extends GenericParams { path: '/_migration/deprecations'; method: 'GET'; } -/** @public */ +/** + * @deprecated + * @public + * */ export interface DeprecationInfo { level: MIGRATION_DEPRECATION_LEVEL; message: string; @@ -351,12 +373,18 @@ export interface DeprecationInfo { details?: string; } -/** @public */ +/** + * @deprecated + * @public + * */ export interface IndexSettingsDeprecationInfo { [indexName: string]: DeprecationInfo[]; } -/** @public */ +/** + * @deprecated + * @public + * */ export interface DeprecationAPIResponse { cluster_settings: DeprecationInfo[]; ml_settings: DeprecationInfo[]; diff --git a/src/core/server/elasticsearch/legacy/cluster_client.ts b/src/core/server/elasticsearch/legacy/cluster_client.ts index 7a39113d25a14..f8b2d39a4251c 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.ts @@ -88,6 +88,7 @@ const callAPI = async ( * * See {@link LegacyClusterClient}. * + * @deprecated Use {@link IClusterClient}. * @public */ export type ILegacyClusterClient = Pick; @@ -98,7 +99,7 @@ export type ILegacyClusterClient = Pick & diff --git a/src/core/server/elasticsearch/legacy/errors.ts b/src/core/server/elasticsearch/legacy/errors.ts index 3b3b8da51a907..de4d2739977bb 100644 --- a/src/core/server/elasticsearch/legacy/errors.ts +++ b/src/core/server/elasticsearch/legacy/errors.ts @@ -26,7 +26,10 @@ enum ErrorCode { NOT_AUTHORIZED = 'Elasticsearch/notAuthorized', } -/** @public */ +/** + * @deprecated. The new elasticsearch client doesn't wrap errors anymore. + * @public + * */ export interface LegacyElasticsearchError extends Boom { [code]?: string; } diff --git a/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts b/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts index 9edb73645f0e2..aee7a1daa8166 100644 --- a/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts @@ -30,6 +30,7 @@ import { LegacyAPICaller, LegacyCallAPIOptions } from './api_types'; * * See {@link LegacyScopedClusterClient}. * + * @deprecated Use {@link IScopedClusterClient}. * @public */ export type ILegacyScopedClusterClient = Pick< @@ -39,6 +40,7 @@ export type ILegacyScopedClusterClient = Pick< /** * {@inheritDoc IScopedClusterClient} + * @deprecated Use {@link IScopedClusterClient | scoped cluster client}. * @public */ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 40399aecbc446..88094af8047e7 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -95,6 +95,37 @@ export interface InternalElasticsearchServiceSetup { * @public */ export interface ElasticsearchServiceStart { + /** + * A pre-configured {@link IClusterClient | Elasticsearch client} + * + * @example + * ```js + * const client = core.elasticsearch.client; + * ``` + */ + readonly client: IClusterClient; + /** + * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. + * + * @param type Unique identifier of the client + * @param clientConfig A config consists of Elasticsearch JS client options and + * valid sub-set of Elasticsearch service config. + * We fill all the missing properties in the `clientConfig` using the default + * Elasticsearch config so that we don't depend on default values set and + * controlled by underlying Elasticsearch JS client. + * We don't run validation against the passed config and expect it to be valid. + * + * @example + * ```js + * const client = elasticsearch.createClient('my-app-name', config); + * const data = await client.asInternalUser.search(); + * ``` + */ + readonly createClient: ( + type: string, + clientConfig?: Partial + ) => ICustomClusterClient; + /** * @deprecated * Provided for the backward compatibility. @@ -138,38 +169,7 @@ export interface ElasticsearchServiceStart { /** * @internal */ -export interface InternalElasticsearchServiceStart extends ElasticsearchServiceStart { - /** - * A pre-configured {@link IClusterClient | Elasticsearch client} - * - * @example - * ```js - * const client = core.elasticsearch.client; - * ``` - */ - readonly client: IClusterClient; - /** - * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. - * - * @param type Unique identifier of the client - * @param clientConfig A config consists of Elasticsearch JS client options and - * valid sub-set of Elasticsearch service config. - * We fill all the missing properties in the `clientConfig` using the default - * Elasticsearch config so that we don't depend on default values set and - * controlled by underlying Elasticsearch JS client. - * We don't run validation against the passed config and expect it to be valid. - * - * @example - * ```js - * const client = elasticsearch.createClient('my-app-name', config); - * const data = await client.asInternalUser().search(); - * ``` - */ - readonly createClient: ( - type: string, - clientConfig?: Partial - ) => ICustomClusterClient; -} +export type InternalElasticsearchServiceStart = ElasticsearchServiceStart; /** @public */ export interface ElasticsearchStatusMeta { diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts index 3df098a1df00d..4345783e46e11 100644 --- a/src/core/server/http/types.ts +++ b/src/core/server/http/types.ts @@ -250,7 +250,7 @@ export interface HttpServiceSetup { * 'myApp', * (context, req) => { * async function search (id: string) { - * return await context.elasticsearch.legacy.client.callAsInternalUser('endpoint', id); + * return await context.elasticsearch.client.asCurrentUser.find(id); * } * return { search }; * } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index f46b41d6b8793..382318ea86a34 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -44,6 +44,7 @@ import { ILegacyScopedClusterClient, configSchema as elasticsearchConfigSchema, ElasticsearchServiceStart, + IScopedClusterClient, } from './elasticsearch'; import { HttpServiceSetup, HttpServiceStart } from './http'; @@ -110,6 +111,10 @@ export { FakeRequest, ScopeableRequest, ElasticsearchClient, + IClusterClient, + ICustomClusterClient, + ElasticsearchClientConfig, + IScopedClusterClient, SearchResponse, CountResponse, ShardsInfo, @@ -367,10 +372,13 @@ export { * which uses the credentials of the incoming request * - {@link ISavedObjectTypeRegistry | savedObjects.typeRegistry} - Type registry containing * all the registered types. - * - {@link LegacyScopedClusterClient | elasticsearch.legacy.client} - Elasticsearch + * - {@link IScopedClusterClient | elasticsearch.client} - Elasticsearch + * data client which uses the credentials of the incoming request + * - {@link LegacyScopedClusterClient | elasticsearch.legacy.client} - The legacy Elasticsearch * data client which uses the credentials of the incoming request * - {@link IUiSettingsClient | uiSettings.client} - uiSettings client * which uses the credentials of the incoming request + * - {@link Auditor | uiSettings.auditor} - AuditTrail client scoped to the incoming request * * @public */ @@ -381,6 +389,7 @@ export interface RequestHandlerContext { typeRegistry: ISavedObjectTypeRegistry; }; elasticsearch: { + client: IScopedClusterClient; legacy: { client: ILegacyScopedClusterClient; }; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 84e4b4741b717..bf9dcc4abe01c 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -193,6 +193,7 @@ function createCoreRequestHandlerContextMock() { typeRegistry: savedObjectsTypeRegistryMock.create(), }, elasticsearch: { + client: elasticsearchServiceMock.createScopedClusterClient(), legacy: { client: elasticsearchServiceMock.createLegacyScopedClusterClient(), }, diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index c17b8df8bb52c..5235f3ee6d580 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -212,6 +212,8 @@ export function createPluginStartContext( resolveCapabilities: deps.capabilities.resolveCapabilities, }, elasticsearch: { + client: deps.elasticsearch.client, + createClient: deps.elasticsearch.createClient, legacy: deps.elasticsearch.legacy, }, http: { diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts index fb8fb4ef95081..0b3ad1b6e3cc8 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts @@ -22,10 +22,10 @@ import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import * as Index from './elastic_index'; describe('ElasticIndex', () => { - let client: ReturnType; + let client: ReturnType; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); }); describe('fetchInfo', () => { test('it handles 404', async () => { diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 78601d033f8d8..b0669774207dd 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -27,13 +27,13 @@ import { loggingSystemMock } from '../../../logging/logging_system.mock'; describe('IndexMigrator', () => { let testOpts: jest.Mocked & { - client: ReturnType; + client: ReturnType; }; beforeEach(() => { testOpts = { batchSize: 10, - client: elasticsearchClientMock.createElasticSearchClient(), + client: elasticsearchClientMock.createElasticsearchClient(), index: '.kibana', log: loggingSystemMock.create().get(), mappingProperties: {}, @@ -366,7 +366,7 @@ describe('IndexMigrator', () => { }); function withIndex( - client: ReturnType, + client: ReturnType, opts: any = {} ) { const defaultIndex = { diff --git a/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts b/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts index 40c06677c4a5a..a6da62095060c 100644 --- a/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts +++ b/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts @@ -24,11 +24,11 @@ import { loggerMock } from '../../../logging/logger.mock'; import { SavedObjectsErrorHelpers } from '../../service/lib/errors'; describe('MigrationEsClient', () => { - let client: ReturnType; + let client: ReturnType; let migrationEsClient: MigrationEsClient; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); migrationEsClient = createMigrationEsClient(client, loggerMock.create()); migrationRetryCallClusterMock.mockClear(); }); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index c3ed97a89af80..cc443093e30a3 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -127,7 +127,7 @@ describe('KibanaMigrator', () => { }); type MockedOptions = KibanaMigratorOptions & { - client: ReturnType; + client: ReturnType; }; const mockOptions = () => { @@ -170,7 +170,7 @@ const mockOptions = () => { scrollDuration: '10m', skip: false, }, - client: elasticsearchClientMock.createElasticSearchClient(), + client: elasticsearchClientMock.createElasticsearchClient(), }; return options; }; 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 b902179b012ff..4a9fceb9bf357 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -201,7 +201,7 @@ describe('SavedObjectsRepository', () => { }; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); migrator = { migrateDocument: jest.fn().mockImplementation(documentMigrator.migrate), runMigrations: async () => ({ status: 'skipped' }), diff --git a/src/core/server/saved_objects/service/lib/repository_es_client.test.ts b/src/core/server/saved_objects/service/lib/repository_es_client.test.ts index 86a984fb67124..61df94fb6bfe2 100644 --- a/src/core/server/saved_objects/service/lib/repository_es_client.test.ts +++ b/src/core/server/saved_objects/service/lib/repository_es_client.test.ts @@ -23,11 +23,11 @@ import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import { SavedObjectsErrorHelpers } from './errors'; describe('RepositoryEsClient', () => { - let client: ReturnType; + let client: ReturnType; let repositoryClient: RepositoryEsClient; beforeEach(() => { - client = elasticsearchClientMock.createElasticSearchClient(); + client = elasticsearchClientMock.createElasticsearchClient(); repositoryClient = createRepositoryEsClient(client); retryCallClusterMock.mockClear(); }); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index c94151f8cee17..c1054c27d084e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -158,7 +158,7 @@ export type AppenderConfigType = TypeOf; // @public export function assertNever(x: never): never; -// @public (undocumented) +// @public @deprecated (undocumented) export interface AssistanceAPIResponse { // (undocumented) indices: { @@ -168,7 +168,7 @@ export interface AssistanceAPIResponse { }; } -// @public (undocumented) +// @public @deprecated (undocumented) export interface AssistantAPIClientParams extends GenericParams { // (undocumented) method: 'GET'; @@ -622,7 +622,7 @@ export interface DeleteDocumentResponse { _version: number; } -// @public (undocumented) +// @public @deprecated (undocumented) export interface DeprecationAPIClientParams extends GenericParams { // (undocumented) method: 'GET'; @@ -630,7 +630,7 @@ export interface DeprecationAPIClientParams extends GenericParams { path: '/_migration/deprecations'; } -// @public (undocumented) +// @public @deprecated (undocumented) export interface DeprecationAPIResponse { // (undocumented) cluster_settings: DeprecationInfo[]; @@ -642,7 +642,7 @@ export interface DeprecationAPIResponse { node_settings: DeprecationInfo[]; } -// @public (undocumented) +// @public @deprecated (undocumented) export interface DeprecationInfo { // (undocumented) details?: string; @@ -679,6 +679,14 @@ export type ElasticsearchClient = Omit & { + pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; + requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; + ssl?: Partial; + keepAlive?: boolean; +}; + // @public export class ElasticsearchConfig { constructor(rawConfig: ElasticsearchConfigType); @@ -715,6 +723,8 @@ export interface ElasticsearchServiceSetup { // @public (undocumented) export interface ElasticsearchServiceStart { + readonly client: IClusterClient; + readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; // @deprecated (undocumented) legacy: { readonly createClient: (type: string, clientConfig?: Partial) => ILegacyCustomClusterClient; @@ -895,6 +905,12 @@ export interface HttpServiceStart { // @public export type IBasePath = Pick; +// @public +export interface IClusterClient { + readonly asInternalUser: ElasticsearchClient; + asScoped: (request: ScopeableRequest) => IScopedClusterClient; +} + // @public export interface IContextContainer> { createHandler(pluginOpaqueId: PluginOpaqueId, handler: THandler): (...rest: HandlerParameters) => ShallowPromise>; @@ -914,6 +930,11 @@ export interface ICspConfig { readonly warnLegacyBrowsers: boolean; } +// @public +export interface ICustomClusterClient extends IClusterClient { + close: () => Promise; +} + // @public export interface IKibanaResponse { // (undocumented) @@ -935,13 +956,13 @@ export interface IKibanaSocket { getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate | null; } -// @public +// @public @deprecated export type ILegacyClusterClient = Pick; -// @public +// @public @deprecated export type ILegacyCustomClusterClient = Pick; -// @public +// @public @deprecated export type ILegacyScopedClusterClient = Pick; // @public (undocumented) @@ -956,7 +977,7 @@ export interface ImageValidation { // @public export function importSavedObjectsFromStream({ readStream, objectLimit, overwrite, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsImportOptions): Promise; -// @public (undocumented) +// @public @deprecated (undocumented) export interface IndexSettingsDeprecationInfo { // (undocumented) [indexName: string]: DeprecationInfo[]; @@ -997,6 +1018,12 @@ export type ISavedObjectsRepository = Pick; +// @public +export interface IScopedClusterClient { + readonly asCurrentUser: ElasticsearchClient; + readonly asInternalUser: ElasticsearchClient; +} + // @public export function isRelativeUrl(candidatePath: string): boolean; @@ -1086,7 +1113,7 @@ export const kibanaResponseFactory: { // @public export type KnownHeaders = KnownKeys; -// @public (undocumented) +// @public @deprecated (undocumented) export interface LegacyAPICaller { // (undocumented) (endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: LegacyCallAPIOptions): ReturnType; @@ -1330,15 +1357,13 @@ export interface LegacyAPICaller { (endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; } -// @public +// @public @deprecated export interface LegacyCallAPIOptions { signal?: AbortSignal; wrap401Errors?: boolean; } -// Warning: (ae-unresolved-inheritdoc-reference) The @inheritDoc reference could not be resolved: The package "kibana" does not have an export "IClusterClient" -// -// @public (undocumented) +// @public @deprecated export class LegacyClusterClient implements ILegacyClusterClient { constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuditorFactory: () => AuditorFactory, getAuthHeaders?: GetAuthHeaders); asScoped(request?: ScopeableRequest): ILegacyScopedClusterClient; @@ -1360,7 +1385,7 @@ export interface LegacyConfig { set(config: LegacyVars): void; } -// @public (undocumented) +// @public @deprecated (undocumented) export type LegacyElasticsearchClientConfig = Pick & Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ConfigOptions['requestTimeout']; @@ -1368,7 +1393,7 @@ export type LegacyElasticsearchClientConfig = Pick; }; -// @public (undocumented) +// @public export interface LegacyElasticsearchError extends Boom { // (undocumented) [code]?: string; @@ -1401,9 +1426,7 @@ export class LegacyInternals implements ILegacyInternals { export interface LegacyRequest extends Request { } -// Warning: (ae-unresolved-inheritdoc-reference) The @inheritDoc reference could not be resolved: The package "kibana" does not have an export "IScopedClusterClient" -// -// @public (undocumented) +// @public @deprecated export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined, auditor?: Auditor | undefined); callAsCurrentUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; @@ -1559,10 +1582,10 @@ export interface LogRecord { export interface MetricsServiceSetup { } -// @public (undocumented) +// @public @deprecated (undocumented) export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; -// @public (undocumented) +// @public @deprecated (undocumented) export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; // @public @@ -1812,6 +1835,7 @@ export interface RequestHandlerContext { typeRegistry: ISavedObjectTypeRegistry; }; elasticsearch: { + client: IScopedClusterClient; legacy: { client: ILegacyScopedClusterClient; }; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 2dae7f8f38f23..aff749ca97534 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -275,6 +275,7 @@ export class Server { typeRegistry: coreStart.savedObjects.getTypeRegistry(), }, elasticsearch: { + client: coreStart.elasticsearch.client.asScoped(req), legacy: { client: coreStart.elasticsearch.legacy.client.asScoped(req), }, diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index bfcc98d6cd9a8..1d41f4c270caa 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -63,17 +63,17 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions await run(Tasks.CopyBinScripts); await run(Tasks.CreateEmptyDirsAndFiles); await run(Tasks.CreateReadme); - await run(Tasks.TranspileBabel); await run(Tasks.BuildPackages); await run(Tasks.CreatePackageJson); await run(Tasks.InstallDependencies); + await run(Tasks.BuildKibanaPlatformPlugins); + await run(Tasks.TranspileBabel); await run(Tasks.RemoveWorkspaces); await run(Tasks.CleanPackages); await run(Tasks.CreateNoticeFile); await run(Tasks.UpdateLicenseFile); await run(Tasks.RemovePackageJsonDeps); await run(Tasks.TranspileScss); - await run(Tasks.BuildKibanaPlatformPlugins); await run(Tasks.OptimizeBuild); await run(Tasks.CleanTypescript); await run(Tasks.CleanExtraFilesFromModules); diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 864bf7515053c..404ad67174681 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -105,6 +105,7 @@ export const IGNORE_DIRECTORY_GLOBS = [ 'test/functional/fixtures/es_archiver/visualize_source-filters', 'packages/kbn-pm/src/utils/__fixtures__/*', 'x-pack/dev-tools', + 'packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack', ]; /** diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 41628a2264193..74973887ae9c1 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -34,7 +34,7 @@ import { IS_KIBANA_DISTRIBUTABLE } from '../legacy/utils'; import { fromRoot } from '../core/server/utils'; import { PUBLIC_PATH_PLACEHOLDER } from './public_path_placeholder'; -const POSTCSS_CONFIG_PATH = require.resolve('./postcss.config'); +const POSTCSS_CONFIG_PATH = require.resolve('./postcss.config.js'); const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); const EMPTY_MODULE_PATH = require.resolve('./intentionally_empty_module.js'); diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 7ad2f9edd3325..d35a6a5bbb9a9 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -22,6 +22,7 @@ import { CatTasksParams } from 'elasticsearch'; import { CatThreadPoolParams } from 'elasticsearch'; import { ClearScrollParams } from 'elasticsearch'; import { Client } from 'elasticsearch'; +import { ClientOptions } from '@elastic/elasticsearch'; import { ClusterAllocationExplainParams } from 'elasticsearch'; import { ClusterGetSettingsParams } from 'elasticsearch'; import { ClusterHealthParams } from 'elasticsearch'; diff --git a/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts b/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts index d598f28a0ad12..0c1a44d7845cf 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts @@ -155,7 +155,7 @@ export function getVisualizeUrl( params: { field: field.name, size: parseInt(aggsTermSize, 10), - orderBy: '2', + orderBy: '1', }, }; } @@ -169,7 +169,7 @@ export function getVisualizeUrl( query: state.query, vis: { type, - aggs: [{ schema: 'metric', type: 'count', id: '2' }, agg], + aggs: [{ schema: 'metric', type: 'count', id: '1' }, agg], }, } as any), }, diff --git a/src/plugins/vis_type_timeseries/common/panel_types.js b/src/plugins/vis_type_timeseries/common/panel_types.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/panel_types.js rename to src/plugins/vis_type_timeseries/common/panel_types.ts diff --git a/src/plugins/vis_type_timeseries/common/ui_restrictions.ts b/src/plugins/vis_type_timeseries/common/ui_restrictions.ts index 4508735f39ff9..e2911eb2d70e3 100644 --- a/src/plugins/vis_type_timeseries/common/ui_restrictions.ts +++ b/src/plugins/vis_type_timeseries/common/ui_restrictions.ts @@ -17,6 +17,8 @@ * under the License. */ +import { PANEL_TYPES } from './panel_types'; + /** * UI Restrictions keys * @constant @@ -56,3 +58,12 @@ export type TimeseriesUIRestrictions = { export const DEFAULT_UI_RESTRICTION: UIRestrictions = { '*': true, }; + +/** limit on the number of series for the panel + * @constant + * @public + */ +export const limitOfSeries = { + [PANEL_TYPES.GAUGE]: 1, + [PANEL_TYPES.METRIC]: 2, +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js index eda49ccdca178..18380680283ef 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js @@ -46,6 +46,9 @@ import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { QueryBarWrapper } from '../query_bar_wrapper'; import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; +import { limitOfSeries } from '../../../../common/ui_restrictions'; +import { PANEL_TYPES } from '../../../../common/panel_types'; + class GaugePanelConfigUi extends Component { constructor(props) { super(props); @@ -110,7 +113,7 @@ class GaugePanelConfigUi extends Component { { + let visibleSeries = panel.series || []; + + if (panel.type in limitOfSeries) { + visibleSeries = visibleSeries.slice(0, limitOfSeries[panel.type]); + } + + // Toogle visibility functionality for 'gauge', 'markdown' is not accessible + const shouldNotApplyFilter = [PANEL_TYPES.GAUGE, PANEL_TYPES.MARKDOWN].includes(panel.type); + + return visibleSeries.filter((series) => !series.hidden || shouldNotApplyFilter); +}; diff --git a/src/plugins/vis_type_vega/public/data_model/ems_file_parser.ts b/src/plugins/vis_type_vega/public/data_model/ems_file_parser.ts index 59256d47de97c..6335aeaf217a1 100644 --- a/src/plugins/vis_type_vega/public/data_model/ems_file_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/ems_file_parser.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore import { bypassExternalUrlCheck } from '../vega_view/vega_base_view'; import { IServiceSettings, FileLayer } from '../../../maps_legacy/public'; -import { Data, UrlObject, Requests } from './types'; +import { Data, UrlObject, EmsQueryRequest } from './types'; /** * This class processes all Vega spec customizations, @@ -53,6 +53,7 @@ export class EmsFileParser { }) ); } + // Optimization: so initiate remote request as early as we know that we will need it if (!this._fileLayersP) { this._fileLayersP = this._serviceSettings.getFileLayers(); @@ -65,7 +66,7 @@ export class EmsFileParser { * @param {object[]} requests each object is generated by parseUrl() * @returns {Promise} */ - async populateData(requests: Requests[]) { + async populateData(requests: EmsQueryRequest[]) { if (requests.length === 0) return; const layers = await this._fileLayersP; diff --git a/src/plugins/vis_type_vega/public/data_model/es_query_parser.ts b/src/plugins/vis_type_vega/public/data_model/es_query_parser.ts index 4fdd68f9e9dbe..1aac8e25d5c73 100644 --- a/src/plugins/vis_type_vega/public/data_model/es_query_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/es_query_parser.ts @@ -23,7 +23,16 @@ import { cloneDeep, isPlainObject } from 'lodash'; import { SearchParams } from 'elasticsearch'; import { TimeCache } from './time_cache'; import { SearchAPI } from './search_api'; -import { Opts, Type, Data, UrlObject, Bool, Requests, Query, ContextVarsObject } from './types'; +import { + Opts, + Type, + Data, + UrlObject, + Bool, + EsQueryRequest, + Query, + ContextVarsObject, +} from './types'; const TIMEFILTER: string = '%timefilter%'; const AUTOINTERVAL: string = '%autointerval%'; @@ -36,6 +45,13 @@ const LEGACY_CONTEXT: string = '%context_query%'; const CONTEXT: string = '%context%'; const TIMEFIELD: string = '%timefield%'; +const getRequestName = (request: EsQueryRequest, index: number) => + request.dataObject.name || + i18n.translate('visTypeVega.esQueryParser.unnamedRequest', { + defaultMessage: 'Unnamed request #{index}', + values: { index }, + }); + /** * This class parses ES requests specified in the data.url objects. */ @@ -196,14 +212,22 @@ export class EsQueryParser { * @param {object[]} requests each object is generated by parseUrl() * @returns {Promise} */ - async populateData(requests: Requests[]) { - const esSearches = requests.map((r: Requests) => r.url); + async populateData(requests: EsQueryRequest[]) { + const esSearches = requests.map((r: EsQueryRequest, index: number) => ({ + ...r.url, + name: getRequestName(r, index), + })); + const data$ = this._searchAPI.search(esSearches); const results = await data$.toPromise(); - results.forEach((data) => { - requests[data.id].dataObject.values = data.rawResponse; + results.forEach((data, index) => { + const requestObject = requests.find((item) => getRequestName(item, index) === data.name); + + if (requestObject) { + requestObject.dataObject.values = data.rawResponse; + } }); } diff --git a/src/plugins/vis_type_vega/public/data_model/search_api.ts b/src/plugins/vis_type_vega/public/data_model/search_api.ts index 18387a6ab0876..a213b59be2ad0 100644 --- a/src/plugins/vis_type_vega/public/data_model/search_api.ts +++ b/src/plugins/vis_type_vega/public/data_model/search_api.ts @@ -48,25 +48,22 @@ export class SearchAPI { const requestResponders: any = {}; return combineLatest( - searchRequests.map((request, index) => { - const requestId: number = index; + searchRequests.map((request) => { + const requestId = request.name; const params = getSearchParamsFromRequest(request, { uiSettings: this.dependencies.uiSettings, injectedMetadata: this.dependencies.injectedMetadata, }); if (this.inspectorAdapters) { - requestResponders[requestId] = this.inspectorAdapters.requests.start( - `#${requestId}`, - request - ); + requestResponders[requestId] = this.inspectorAdapters.requests.start(requestId, request); requestResponders[requestId].json(params.body); } return search({ params }, { signal: this.abortSignal }).pipe( tap((data) => this.inspectSearchResult(data, requestResponders[requestId])), map((data) => ({ - id: requestId, + name: requestId, rawResponse: data.rawResponse, })) ); diff --git a/src/plugins/vis_type_vega/public/data_model/types.ts b/src/plugins/vis_type_vega/public/data_model/types.ts index 9876faf0fc88f..b830b24c92082 100644 --- a/src/plugins/vis_type_vega/public/data_model/types.ts +++ b/src/plugins/vis_type_vega/public/data_model/types.ts @@ -18,6 +18,7 @@ */ import { SearchResponse, SearchParams } from 'elasticsearch'; + import { Filter } from 'src/plugins/data/public'; import { DslQuery } from 'src/plugins/data/common'; import { EsQueryParser } from './es_query_parser'; @@ -75,13 +76,10 @@ interface Projection { } interface RequestDataObject { + name?: string; values: SearchResponse; } -interface RequestObject { - url: string; -} - type ContextVarsObjectProps = | string | { @@ -176,22 +174,22 @@ export interface Data { source?: unknown; } -export interface CacheOptions { - max: number; - maxAge: number; -} - export interface CacheBounds { min: number; max: number; } -export interface Requests extends RequestObject { - obj: RequestObject; +interface Requests { + url: TUrlData; name: string; - dataObject: RequestDataObject; + dataObject: TRequestDataObject; } +export type EsQueryRequest = Requests; +export type EmsQueryRequest = Requests & { + obj: UrlObject; +}; + export interface ContextVarsObject { [index: string]: any; prop: ContextVarsObjectProps; diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index e29e16e3212f4..62563dce2a18d 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -87,7 +87,7 @@ describe('VegaParser._resolveEsQueries', () => { let searchApiStub; const data = [ { - id: 0, + name: 'requestId', rawResponse: [42], }, ]; @@ -119,16 +119,25 @@ describe('VegaParser._resolveEsQueries', () => { test('no data', check({}, {})); test('no data2', check({ a: 1 }, { a: 1 })); test('non-es data', check({ data: { a: 10 } }, { data: { a: 10 } })); - test('es', check({ data: { url: { index: 'a' }, x: 1 } }, { data: { values: [42], x: 1 } })); + test( + 'es', + check( + { data: { name: 'requestId', url: { index: 'a' }, x: 1 } }, + { data: { name: 'requestId', values: [42], x: 1 } } + ) + ); test( 'es 2', - check({ data: { url: { '%type%': 'elasticsearch', index: 'a' } } }, { data: { values: [42] } }) + check( + { data: { name: 'requestId', url: { '%type%': 'elasticsearch', index: 'a' } } }, + { data: { name: 'requestId', values: [42] } } + ) ); test( 'es arr', check( - { arr: [{ data: { url: { index: 'a' }, x: 1 } }] }, - { arr: [{ data: { values: [42], x: 1 } }] } + { arr: [{ data: { name: 'requestId', url: { index: 'a' }, x: 1 } }] }, + { arr: [{ data: { name: 'requestId', values: [42], x: 1 } }] } ) ); test( diff --git a/tasks/config/run.js b/tasks/config/run.js index 9ac8f72d56d4a..132b51765b3ed 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -210,7 +210,7 @@ module.exports = function () { args: [ 'scripts/functional_tests', '--config', - 'test/plugin_functional/config.js', + 'test/plugin_functional/config.ts', '--bail', '--debug', ], diff --git a/test/api_integration/services/index.ts b/test/api_integration/services/index.ts index 782ea271869ba..d024943bef792 100644 --- a/test/api_integration/services/index.ts +++ b/test/api_integration/services/index.ts @@ -19,7 +19,6 @@ import { services as commonServices } from '../../common/services'; -// @ts-ignore not TS yet import { KibanaSupertestProvider, ElasticsearchSupertestProvider } from './supertest'; export const services = { diff --git a/test/functional/apps/discover/_field_visualize.ts b/test/functional/apps/discover/_field_visualize.ts index e202dcb7e2af7..b0db6c149e41a 100644 --- a/test/functional/apps/discover/_field_visualize.ts +++ b/test/functional/apps/discover/_field_visualize.ts @@ -27,7 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const log = getService('log'); const queryBar = getService('queryBar'); - const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker', 'visualize']); const defaultSettings = { defaultIndex: 'logstash-*', }; @@ -48,6 +48,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); }); + it('should be able to visualize a field and save the visualization', async () => { + await PageObjects.discover.findFieldByName('type'); + log.debug('visualize a type field'); + await PageObjects.discover.clickFieldListItemVisualize('type'); + await PageObjects.visualize.saveVisualizationExpectSuccess('Top 5 server types'); + }); + it('should visualize a field in area chart', async () => { await PageObjects.discover.findFieldByName('phpmemory'); log.debug('visualize a phpmemory field'); diff --git a/test/plugin_functional/README.md b/test/plugin_functional/README.md index 476c08408658c..075d321917c39 100644 --- a/test/plugin_functional/README.md +++ b/test/plugin_functional/README.md @@ -17,9 +17,9 @@ To run these tests during development you can use the following commands: ``` # Start the test server (can continue running) -node scripts/functional_tests_server.js --config test/plugin_functional/config.js +node scripts/functional_tests_server.js --config test/plugin_functional/config.ts # Start a test run -node scripts/functional_test_runner.js --config test/plugin_functional/config.js +node scripts/functional_test_runner.js --config test/plugin_functional/config.ts ``` ## Run Kibana with a test plugin @@ -42,7 +42,7 @@ If you wish to load up specific es archived data for your test, you can do so vi Another option, which will automatically use any specific settings the test environment may rely on, is to boot up the functional test server pointing to the plugin configuration file. ``` -node scripts/functional_tests_server --config test/plugin_functional/config.js +node scripts/functional_tests_server --config test/plugin_functional/config.ts ``` *Note:* you may still need to use the es_archiver script to boot up any required data. diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.ts similarity index 94% rename from test/plugin_functional/config.js rename to test/plugin_functional/config.ts index f51fb5e1bade4..c611300eade10 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.ts @@ -16,12 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import path from 'path'; import fs from 'fs'; -import { services } from './services'; -export default async function ({ readConfigFile }) { +export default async function ({ readConfigFile }: FtrConfigProviderContext) { const functionalConfig = await readConfigFile(require.resolve('../functional/config')); // Find all folders in ./plugins since we treat all them as plugin folder @@ -42,7 +41,6 @@ export default async function ({ readConfigFile }) { ], services: { ...functionalConfig.get('services'), - ...services, }, pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/kibana.json b/test/plugin_functional/plugins/elasticsearch_client_plugin/kibana.json new file mode 100644 index 0000000000000..a7674881e8ba0 --- /dev/null +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "elasticsearch_client_plugin", + "version": "0.0.1", + "kibanaVersion": "kibana", + "server": true, + "ui": false +} diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/package.json b/test/plugin_functional/plugins/elasticsearch_client_plugin/package.json new file mode 100644 index 0000000000000..02c2955f26c86 --- /dev/null +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/package.json @@ -0,0 +1,15 @@ +{ + "name": "elasticsearch_client_plugin", + "version": "1.0.0", + "kibana": { + "version": "kibana" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.9.5" + } +} diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_active_series.js b/test/plugin_functional/plugins/elasticsearch_client_plugin/server/index.ts similarity index 79% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_active_series.js rename to test/plugin_functional/plugins/elasticsearch_client_plugin/server/index.ts index 10b60df0aa35c..3801e33a2cf3e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_active_series.js +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/server/index.ts @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -export const getActiveSeries = (panel) => { - const shouldNotApplyFilter = ['gauge', 'markdown'].includes(panel.type); - return (panel.series || []).filter((series) => !series.hidden || shouldNotApplyFilter); -}; +import { ElasticsearchClientPlugin } from './plugin'; + +export const plugin = () => new ElasticsearchClientPlugin(); diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/server/plugin.ts b/test/plugin_functional/plugins/elasticsearch_client_plugin/server/plugin.ts new file mode 100644 index 0000000000000..5e018ca7818d3 --- /dev/null +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/server/plugin.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Plugin, CoreSetup, CoreStart, ICustomClusterClient } from 'src/core/server'; + +export class ElasticsearchClientPlugin implements Plugin { + private client?: ICustomClusterClient; + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + router.get( + { path: '/api/elasticsearch_client_plugin/context/ping', validate: false }, + async (context, req, res) => { + const { body } = await context.core.elasticsearch.client.asInternalUser.ping(); + return res.ok({ body }); + } + ); + router.get( + { path: '/api/elasticsearch_client_plugin/contract/ping', validate: false }, + async (context, req, res) => { + const [coreStart] = await core.getStartServices(); + const { body } = await coreStart.elasticsearch.client.asInternalUser.ping(); + return res.ok({ body }); + } + ); + router.get( + { path: '/api/elasticsearch_client_plugin/custom_client/ping', validate: false }, + async (context, req, res) => { + const { body } = await this.client!.asInternalUser.ping(); + return res.ok({ body }); + } + ); + } + + public start(core: CoreStart) { + this.client = core.elasticsearch.createClient('my-custom-client-test'); + } + public stop() {} +} diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json b/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json new file mode 100644 index 0000000000000..d0751f31ecc5e --- /dev/null +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "server/**/*.ts", + "../../../../typings/**/*" + ], + "exclude": [] +} diff --git a/test/plugin_functional/services/index.ts b/test/plugin_functional/services/index.ts index dd2b25e14fe17..453cfc5a8636f 100644 --- a/test/plugin_functional/services/index.ts +++ b/test/plugin_functional/services/index.ts @@ -16,14 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; -import { FtrProviderContext } from 'test/functional/ftr_provider_context'; -import { KibanaSupertestProvider } from './supertest'; +import { FtrProviderContext } from '../../functional/ftr_provider_context'; -export const services = { - supertest: KibanaSupertestProvider, -}; - -export type PluginFunctionalProviderContext = FtrProviderContext & - GenericFtrProviderContext; +export type PluginFunctionalProviderContext = FtrProviderContext; diff --git a/test/plugin_functional/test_suites/core_plugins/application_status.ts b/test/plugin_functional/test_suites/core_plugins/application_status.ts index a4c2db733b894..f56a6e8d62fb1 100644 --- a/test/plugin_functional/test_suites/core_plugins/application_status.ts +++ b/test/plugin_functional/test_suites/core_plugins/application_status.ts @@ -17,7 +17,7 @@ * under the License. */ -import url from 'url'; +import Url from 'url'; import expect from '@kbn/expect'; import { AppNavLinkStatus, @@ -28,7 +28,7 @@ import { PluginFunctionalProviderContext } from '../../services'; import '../../plugins/core_app_status/public/types'; const getKibanaUrl = (pathname?: string, search?: string) => - url.format({ + Url.format({ protocol: 'http:', hostname: process.env.TEST_KIBANA_HOST || 'localhost', port: process.env.TEST_KIBANA_PORT || '5620', @@ -115,7 +115,8 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide await navigateToApp('app_status'); expect(await testSubjects.exists('appStatusApp')).to.eql(true); - expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/app_status/arbitrary/path')); + const currentUrl = await browser.getCurrentUrl(); + expect(Url.parse(currentUrl).pathname).to.eql('/app/app_status/arbitrary/path'); }); it('can change the state of the currently mounted app', async () => { diff --git a/test/plugin_functional/test_suites/core_plugins/elasticsearch_client.ts b/test/plugin_functional/test_suites/core_plugins/elasticsearch_client.ts new file mode 100644 index 0000000000000..9b9efc261126f --- /dev/null +++ b/test/plugin_functional/test_suites/core_plugins/elasticsearch_client.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { PluginFunctionalProviderContext } from '../../services'; +import '../../plugins/core_provider_plugin/types'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + describe('elasticsearch client', () => { + it('server plugins have access to elasticsearch client via request context', async () => { + await supertest.get('/api/elasticsearch_client_plugin/context/ping').expect(200, 'true'); + }); + it('server plugins have access to elasticsearch client via core contract', async () => { + await supertest.get('/api/elasticsearch_client_plugin/contract/ping').expect(200, 'true'); + }); + it('server plugins can create a custom elasticsearch client', async () => { + await supertest + .get('/api/elasticsearch_client_plugin/custom_client/ping') + .expect(200, 'true'); + }); + }); +} diff --git a/test/plugin_functional/test_suites/core_plugins/index.ts b/test/plugin_functional/test_suites/core_plugins/index.ts index 8f54ec6c0f4cd..99ac6dc9b8474 100644 --- a/test/plugin_functional/test_suites/core_plugins/index.ts +++ b/test/plugin_functional/test_suites/core_plugins/index.ts @@ -22,6 +22,7 @@ import { PluginFunctionalProviderContext } from '../../services'; export default function ({ loadTestFile }: PluginFunctionalProviderContext) { describe('core plugins', () => { loadTestFile(require.resolve('./applications')); + loadTestFile(require.resolve('./elasticsearch_client')); loadTestFile(require.resolve('./legacy_plugins')); loadTestFile(require.resolve('./server_plugins')); loadTestFile(require.resolve('./ui_plugins')); diff --git a/test/plugin_functional/test_suites/core_plugins/top_nav.js b/test/plugin_functional/test_suites/core_plugins/top_nav.ts similarity index 88% rename from test/plugin_functional/test_suites/core_plugins/top_nav.js rename to test/plugin_functional/test_suites/core_plugins/top_nav.ts index 5c46e3d7f76db..6d2c6b7f85d28 100644 --- a/test/plugin_functional/test_suites/core_plugins/top_nav.js +++ b/test/plugin_functional/test_suites/core_plugins/top_nav.ts @@ -17,8 +17,10 @@ * under the License. */ import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; -export default function ({ getService, getPageObjects }) { +// eslint-disable-next-line import/no-default-export +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { const PageObjects = getPageObjects(['common']); const browser = getService('browser'); diff --git a/x-pack/package.json b/x-pack/package.json index 3a9b3ca606de6..2d7cb148c43b0 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -121,8 +121,10 @@ "@types/pretty-ms": "^5.0.0", "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", + "autoprefixer": "^9.7.4", "axios": "^0.19.0", "babel-jest": "^25.5.1", + "babel-loader": "^8.0.6", "babel-plugin-require-context-hook": "npm:babel-plugin-require-context-hook-babel7@1.0.0", "base64-js": "^1.3.1", "base64url": "^3.0.1", @@ -159,6 +161,7 @@ "loader-utils": "^1.2.3", "madge": "3.4.4", "marge": "^1.0.1", + "mini-css-extract-plugin": "0.8.0", "mocha": "^7.1.1", "mocha-junit-reporter": "^1.23.1", "mochawesome": "^4.1.0", @@ -168,6 +171,9 @@ "node-fetch": "^2.6.0", "null-loader": "^3.0.0", "pixelmatch": "^5.1.0", + "postcss": "^7.0.32", + "postcss-loader": "^3.0.0", + "postcss-prefix-selector": "^1.7.2", "proxyquire": "1.8.0", "react-docgen-typescript-loader": "^3.1.1", "react-is": "^16.8.0", @@ -308,7 +314,6 @@ "pluralize": "3.1.0", "pngjs": "3.4.0", "polished": "^1.9.2", - "postcss-prefix-selector": "^1.7.2", "prop-types": "^15.6.0", "proper-lockfile": "^3.2.0", "puid": "1.0.7", diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index 9e0a3e3d0d889..07270b572a4be 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -13,19 +13,18 @@ export interface ServiceAnomalyStats { jobId?: string; } -export const MLErrorMessages: Record = { - INSUFFICIENT_LICENSE: i18n.translate( - 'xpack.apm.anomaly_detection.error.insufficient_license', +export const ML_ERRORS = { + INVALID_LICENSE: i18n.translate( + 'xpack.apm.anomaly_detection.error.invalid_license', { - defaultMessage: - 'You must have a platinum license to use Anomaly Detection', + defaultMessage: `To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning.`, } ), MISSING_READ_PRIVILEGES: i18n.translate( 'xpack.apm.anomaly_detection.error.missing_read_privileges', { defaultMessage: - 'You must have "read" privileges to Machine Learning in order to view Anomaly Detection jobs', + 'You must have "read" privileges to Machine Learning and APM in order to view Anomaly Detection jobs', } ), MISSING_WRITE_PRIVILEGES: i18n.translate( @@ -47,16 +46,4 @@ export const MLErrorMessages: Record = { defaultMessage: 'Machine learning is not available in the selected space', } ), - UNEXPECTED: i18n.translate('xpack.apm.anomaly_detection.error.unexpected', { - defaultMessage: 'An unexpected error occurred', - }), }; - -export enum ErrorCode { - INSUFFICIENT_LICENSE = 'INSUFFICIENT_LICENSE', - MISSING_READ_PRIVILEGES = 'MISSING_READ_PRIVILEGES', - MISSING_WRITE_PRIVILEGES = 'MISSING_WRITE_PRIVILEGES', - ML_NOT_AVAILABLE = 'ML_NOT_AVAILABLE', - ML_NOT_AVAILABLE_IN_SPACE = 'ML_NOT_AVAILABLE_IN_SPACE', - UNEXPECTED = 'UNEXPECTED', -} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx index 3c323cf8fa161..a594edb32b083 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx @@ -20,7 +20,7 @@ import { EuiEmptyPrompt, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { MLErrorMessages } from '../../../../../common/anomaly_detection'; +import { ML_ERRORS } from '../../../../../common/anomaly_detection'; import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; import { createJobs } from './create_jobs'; @@ -65,7 +65,7 @@ export function AddEnvironments({ {MLErrorMessages.MISSING_WRITE_PRIVILEGES}} + body={<>{ML_ERRORS.MISSING_WRITE_PRIVILEGES}} /> ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts index acea38732b40a..2e2c2ccbad7cf 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'kibana/public'; -import { MLErrorMessages } from '../../../../../common/anomaly_detection'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; const errorToastTitle = i18n.translate( @@ -27,7 +26,7 @@ export async function createJobs({ toasts: NotificationsStart['toasts']; }) { try { - const res = await callApmApi({ + await callApmApi({ pathname: '/api/apm/settings/anomaly-detection/jobs', method: 'POST', params: { @@ -35,23 +34,11 @@ export async function createJobs({ }, }); - // a known error occurred - if (res?.errorCode) { - toasts.addDanger({ - title: errorToastTitle, - text: MLErrorMessages[res.errorCode], - }); - return false; - } - - // job created successfully toasts.addSuccess({ title: successToastTitle, text: getSuccessToastMessage(environments), }); return true; - - // an unknown/unexpected error occurred } catch (error) { toasts.addDanger({ title: errorToastTitle, diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index 11d3b766d2293..9c04caf61022a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -8,7 +8,7 @@ import React, { useState } from 'react'; import { EuiTitle, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiPanel, EuiEmptyPrompt } from '@elastic/eui'; -import { MLErrorMessages } from '../../../../../common/anomaly_detection'; +import { ML_ERRORS } from '../../../../../common/anomaly_detection'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; import { JobsList } from './jobs_list'; import { AddEnvironments } from './add_environments'; @@ -25,7 +25,6 @@ export type AnomalyDetectionApiResponse = APIReturnType< const DEFAULT_VALUE: AnomalyDetectionApiResponse = { jobs: [], hasLegacyJobs: false, - errorCode: undefined, }; export function AnomalyDetection() { @@ -49,15 +48,7 @@ export function AnomalyDetection() { if (!hasValidLicense) { return ( - + ); } @@ -67,7 +58,7 @@ export function AnomalyDetection() { {MLErrorMessages.MISSING_READ_PRIVILEGES}} + body={<>{ML_ERRORS.MISSING_READ_PRIVILEGES}} /> ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 8494004ae5639..05ea585108c69 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -16,10 +16,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - MLErrorMessages, - ErrorCode, -} from '../../../../../common/anomaly_detection'; import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; @@ -66,7 +62,7 @@ interface Props { onAddEnvironments: () => void; } export function JobsList({ data, status, onAddEnvironments }: Props) { - const { jobs, hasLegacyJobs, errorCode } = data; + const { jobs, hasLegacyJobs } = data; return ( @@ -115,10 +111,7 @@ export function JobsList({ data, status, onAddEnvironments }: Props) { @@ -129,13 +122,7 @@ export function JobsList({ data, status, onAddEnvironments }: Props) { ); } -function getNoItemsMessage({ - status, - errorCode, -}: { - status: FETCH_STATUS; - errorCode?: ErrorCode; -}) { +function getNoItemsMessage({ status }: { status: FETCH_STATUS }) { // loading state const isLoading = status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; @@ -143,11 +130,6 @@ function getNoItemsMessage({ return ; } - // A known error occured. Show specific error message - if (errorCode) { - return MLErrorMessages[errorCode]; - } - // An unexpected error occurred. Show default error message if (status === FETCH_STATUS.FAILURE) { return i18n.translate( diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx index 2149cb676f0d8..585ab22b5fb27 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx @@ -4,51 +4,96 @@ * you may not use this file except in compliance with the Elastic License. */ -import { showAlert } from './AnomalyDetectionSetupLink'; - -const dataWithJobs = { - hasLegacyJobs: false, - jobs: [ - { job_id: 'job1', environment: 'staging' }, - { job_id: 'job2', environment: 'production' }, - ], -}; -const dataWithoutJobs = ({ jobs: [] } as unknown) as any; - -describe('#showAlert', () => { - describe('when an environment is selected', () => { - it('should return true when there are no jobs', () => { - const result = showAlert(dataWithoutJobs, 'testing'); - expect(result).toBe(true); - }); - it('should return true when environment is not included in the jobs', () => { - const result = showAlert(dataWithJobs, 'testing'); - expect(result).toBe(true); +import React from 'react'; +import { render, fireEvent, wait } from '@testing-library/react'; +import { MissingJobsAlert } from './AnomalyDetectionSetupLink'; +import * as hooks from '../../../../hooks/useFetcher'; + +async function renderTooltipAnchor({ + jobs, + environment, +}: { + jobs: Array<{ job_id: string; environment: string }>; + environment?: string; +}) { + // mock api response + jest.spyOn(hooks, 'useFetcher').mockReturnValue({ + data: { jobs }, + status: hooks.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + + const { baseElement, container } = render( + + ); + + // hover tooltip anchor if it exists + const toolTipAnchor = container.querySelector('.euiToolTipAnchor') as any; + if (toolTipAnchor) { + fireEvent.mouseOver(toolTipAnchor); + + // wait for tooltip text to be in the DOM + await wait(() => { + const toolTipText = baseElement.querySelector('.euiToolTipPopover') + ?.textContent; + expect(toolTipText).not.toBe(undefined); }); - it('should return false when environment is included in the jobs', () => { - const result = showAlert(dataWithJobs, 'staging'); - expect(result).toBe(false); + } + + const toolTipText = baseElement.querySelector('.euiToolTipPopover') + ?.textContent; + + return { toolTipText, toolTipAnchor }; +} + +describe('MissingJobsAlert', () => { + describe('when no jobs exist', () => { + it('shows a warning', async () => { + const { toolTipText, toolTipAnchor } = await renderTooltipAnchor({ + jobs: [], + }); + + expect(toolTipAnchor).toBeInTheDocument(); + expect(toolTipText).toBe( + 'Anomaly detection is not yet enabled. Click to continue setup.' + ); }); }); - describe('there is no environment selected (All)', () => { - it('should return true when there are no jobs', () => { - const result = showAlert(dataWithoutJobs, undefined); - expect(result).toBe(true); + describe('when no jobs exists for the selected environment', () => { + it('shows a warning', async () => { + const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ + jobs: [{ environment: 'production', job_id: 'my_job_id' }], + environment: 'staging', + }); + + expect(toolTipAnchor).toBeInTheDocument(); + expect(toolTipText).toBe( + 'Anomaly detection is not yet enabled for the environment "staging". Click to continue setup.' + ); }); - it('should return false when there are any number of jobs', () => { - const result = showAlert(dataWithJobs, undefined); - expect(result).toBe(false); + }); + + describe('when a job exists for the selected environment', () => { + it('does not show a warning', async () => { + const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ + jobs: [{ environment: 'production', job_id: 'my_job_id' }], + environment: 'production', + }); + + expect(toolTipAnchor).not.toBeInTheDocument(); + expect(toolTipText).toBe(undefined); }); }); - describe('when a known error occurred', () => { - it('should return false', () => { - const data = ({ - errorCode: 'MISSING_READ_PRIVILEGES', - } as unknown) as any; - const result = showAlert(data, undefined); - expect(result).toBe(false); + describe('when at least one job exists and no environment is selected', () => { + it('does not show a warning', async () => { + const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ + jobs: [{ environment: 'production', job_id: 'my_job_id' }], + }); + + expect(toolTipAnchor).not.toBeInTheDocument(); + expect(toolTipText).toBe(undefined); }); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx index e989244d43148..a80dcca4a107d 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx @@ -6,43 +6,73 @@ import React from 'react'; import { EuiButtonEmpty, EuiToolTip, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ErrorCode } from '../../../../../common/anomaly_detection'; +import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { APMLink } from './APMLink'; import { getEnvironmentLabel } from '../../../../../common/environment_filter_values'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { useLicense } from '../../../../hooks/useLicense'; export type AnomalyDetectionApiResponse = APIReturnType< '/api/apm/settings/anomaly-detection', 'GET' >; -const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false, errorCode: undefined }; +const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false }; export function AnomalyDetectionSetupLink() { const { uiFilters } = useUrlParams(); const environment = uiFilters.environment; + const plugin = useApmPluginContext(); + const canGetJobs = !!plugin.core.application.capabilities.ml?.canGetJobs; + const license = useLicense(); + const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); + return ( + + + {ANOMALY_DETECTION_LINK_LABEL} + + + {canGetJobs && hasValidLicense ? ( + + ) : null} + + ); +} + +export function MissingJobsAlert({ environment }: { environment?: string }) { const { data = DEFAULT_DATA, status } = useFetcher( (callApmApi) => callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }), [], - { preservePreviousData: false } + { preservePreviousData: false, showToastOnError: false } ); - const isFetchSuccess = status === FETCH_STATUS.SUCCESS; + + if (status !== FETCH_STATUS.SUCCESS) { + return null; + } + + const isEnvironmentSelected = !!environment; + + // there are jobs for at least one environment + if (!isEnvironmentSelected && data.jobs.length > 0) { + return null; + } + + // there are jobs for the selected environment + if ( + isEnvironmentSelected && + data.jobs.some((job) => environment === job.environment) + ) { + return null; + } return ( - - - {ANOMALY_DETECTION_LINK_LABEL} - - {isFetchSuccess && showAlert(data, environment) && ( - - - - )} - + + + ); } @@ -56,7 +86,7 @@ function getTooltipText(environment?: string) { return i18n.translate( 'xpack.apm.anomalyDetectionSetup.notEnabledForEnvironmentText', { - defaultMessage: `Anomaly detection is not yet enabled for the "{currentEnvironment}" environment. Click to continue setup.`, + defaultMessage: `Anomaly detection is not yet enabled for the environment "{currentEnvironment}". Click to continue setup.`, values: { currentEnvironment: getEnvironmentLabel(environment) }, } ); @@ -66,21 +96,3 @@ const ANOMALY_DETECTION_LINK_LABEL = i18n.translate( 'xpack.apm.anomalyDetectionSetup.linkLabel', { defaultMessage: `Anomaly detection` } ); - -export function showAlert( - { jobs = [], errorCode }: AnomalyDetectionApiResponse, - environment: string | undefined -) { - // don't show warning if the user is missing read privileges - if (errorCode === ErrorCode.MISSING_READ_PRIVILEGES) { - return false; - } - - return ( - // No job exists, or - jobs.length === 0 || - // no job exists for the selected environment - (environment !== undefined && - jobs.every((job) => environment !== job.environment)) - ); -} diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/anomaly_detection_error.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/anomaly_detection_error.ts deleted file mode 100644 index 993dcf4c5354b..0000000000000 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/anomaly_detection_error.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. - */ - -import { ErrorCode, MLErrorMessages } from '../../../common/anomaly_detection'; - -export class AnomalyDetectionError extends Error { - constructor(public code: ErrorCode) { - super(MLErrorMessages[code]); - - this.name = this.constructor.name; - Error.captureStackTrace(this, this.constructor); - } -} diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index e5338ac9f5797..7bcd945d890ad 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -7,7 +7,8 @@ import { Logger } from 'kibana/server'; import uuid from 'uuid/v4'; import { snakeCase } from 'lodash'; -import { ErrorCode } from '../../../common/anomaly_detection'; +import Boom from 'boom'; +import { ML_ERRORS } from '../../../common/anomaly_detection'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { Setup } from '../helpers/setup_request'; import { @@ -16,7 +17,6 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; -import { AnomalyDetectionError } from './anomaly_detection_error'; export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType< typeof createAnomalyDetectionJobs @@ -29,16 +29,12 @@ export async function createAnomalyDetectionJobs( const { ml, indices } = setup; if (!ml) { - throw new AnomalyDetectionError(ErrorCode.ML_NOT_AVAILABLE); + throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); } const mlCapabilities = await ml.mlSystem.mlCapabilities(); if (!mlCapabilities.mlFeatureEnabledInSpace) { - throw new AnomalyDetectionError(ErrorCode.ML_NOT_AVAILABLE_IN_SPACE); - } - - if (!mlCapabilities.isPlatinumOrTrialLicense) { - throw new AnomalyDetectionError(ErrorCode.INSUFFICIENT_LICENSE); + throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); } logger.info( @@ -55,13 +51,10 @@ export async function createAnomalyDetectionJobs( const failedJobs = jobResponses.filter(({ success }) => !success); if (failedJobs.length > 0) { - const failedJobIds = failedJobs.map(({ id }) => id).join(', '); - logger.error( - `Failed to create anomaly detection ML jobs for: [${failedJobIds}]:` + const errors = failedJobs.map(({ id, error }) => ({ id, error })); + throw new Error( + `An error occurred while creating ML jobs: ${JSON.stringify(errors)}` ); - failedJobs.forEach(({ error }) => logger.error(JSON.stringify(error))); - - throw new AnomalyDetectionError(ErrorCode.UNEXPECTED); } return jobResponses; diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts index 62d4243a06028..05f41cdfdffd4 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts @@ -5,24 +5,21 @@ */ import { Logger } from 'kibana/server'; -import { ErrorCode } from '../../../common/anomaly_detection'; +import Boom from 'boom'; +import { ML_ERRORS } from '../../../common/anomaly_detection'; import { Setup } from '../helpers/setup_request'; import { getMlJobsWithAPMGroup } from './get_ml_jobs_with_apm_group'; -import { AnomalyDetectionError } from './anomaly_detection_error'; export async function getAnomalyDetectionJobs(setup: Setup, logger: Logger) { const { ml } = setup; + if (!ml) { - return []; + throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); } const mlCapabilities = await ml.mlSystem.mlCapabilities(); if (!mlCapabilities.mlFeatureEnabledInSpace) { - throw new AnomalyDetectionError(ErrorCode.ML_NOT_AVAILABLE_IN_SPACE); - } - - if (!mlCapabilities.isPlatinumOrTrialLicense) { - throw new AnomalyDetectionError(ErrorCode.INSUFFICIENT_LICENSE); + throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); } const response = await getMlJobsWithAPMGroup(ml); diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts index 999d28309121a..ed66236726b9f 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import Boom from 'boom'; +import { ML_ERRORS } from '../../../common/anomaly_detection'; import { Setup } from '../helpers/setup_request'; import { getMlJobsWithAPMGroup } from './get_ml_jobs_with_apm_group'; @@ -12,7 +15,12 @@ export async function hasLegacyJobs(setup: Setup) { const { ml } = setup; if (!ml) { - return false; + throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); + } + + const mlCapabilities = await ml.mlSystem.mlCapabilities(); + if (!mlCapabilities.mlFeatureEnabledInSpace) { + throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); } const response = await getMlJobsWithAPMGroup(ml); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 3e5ef5eb37b02..03716382af859 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -4,14 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ import { Logger } from 'kibana/server'; +import Boom from 'boom'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { PromiseReturnType } from '../../../typings/common'; import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, } from '../../../common/transaction_types'; -import { ServiceAnomalyStats } from '../../../common/anomaly_detection'; -import { APM_ML_JOB_GROUP } from '../anomaly_detection/constants'; +import { + ServiceAnomalyStats, + ML_ERRORS, +} from '../../../common/anomaly_detection'; +import { getMlJobsWithAPMGroup } from '../anomaly_detection/get_ml_jobs_with_apm_group'; export const DEFAULT_ANOMALIES = { mlJobIds: [], serviceAnomalies: {} }; @@ -31,29 +35,15 @@ export async function getServiceAnomalies({ const { ml, start, end } = setup; if (!ml) { - logger.warn('Anomaly detection plugin is not available.'); - return DEFAULT_ANOMALIES; + throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); } + const mlCapabilities = await ml.mlSystem.mlCapabilities(); if (!mlCapabilities.mlFeatureEnabledInSpace) { - logger.warn('Anomaly detection feature is not enabled for the space.'); - return DEFAULT_ANOMALIES; - } - if (!mlCapabilities.isPlatinumOrTrialLicense) { - logger.warn( - 'Unable to create anomaly detection jobs due to insufficient license.' - ); - return DEFAULT_ANOMALIES; - } - - let mlJobIds: string[] = []; - try { - mlJobIds = await getMLJobIds(ml, environment); - } catch (error) { - logger.error(error); - return DEFAULT_ANOMALIES; + throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); } + const mlJobIds = await getMLJobIds(ml, environment); const params = { body: { size: 0, @@ -92,7 +82,9 @@ export async function getServiceAnomalies({ }, }, }; + const response = await ml.mlSystem.mlAnomalySearch(params); + return { mlJobIds, serviceAnomalies: transformResponseToServiceAnomalies( @@ -147,7 +139,7 @@ export async function getMLJobIds( ml: Required['ml'], environment?: string ) { - const response = await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP); + const response = await getMlJobsWithAPMGroup(ml); // to filter out legacy jobs we are filtering by the existence of `apm_ml_version` in `custom_settings` // and checking that it is compatable. const mlJobs = response.jobs.filter( diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index ea2bb14efdfc7..cd125f944f8a5 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -142,11 +142,14 @@ export async function getServiceMap(options: IEnvOptions) { const { logger } = options; const anomaliesPromise: Promise = getServiceAnomalies( options + + // always catch error to avoid breaking service maps if there is a problem with ML ).catch((error) => { logger.warn(`Unable to retrieve anomalies for service maps.`); logger.error(error); return DEFAULT_ANOMALIES; }); + const [connectionData, servicesData, anomalies] = await Promise.all([ getConnectionData(options), getServicesData(options), diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index 218d47fcf9bb4..ac25f22751f2f 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -5,58 +5,38 @@ */ import * as t from 'io-ts'; -import { ErrorCode } from '../../../common/anomaly_detection'; -import { PromiseReturnType } from '../../../typings/common'; -import { InsufficientMLCapabilities } from '../../../../ml/server'; +import Boom from 'boom'; +import { ML_ERRORS } from '../../../common/anomaly_detection'; import { createRoute } from '../create_route'; import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly_detection_jobs'; import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs'; import { setupRequest } from '../../lib/helpers/setup_request'; import { getAllEnvironments } from '../../lib/environments/get_all_environments'; import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs'; -import { AnomalyDetectionError } from '../../lib/anomaly_detection/anomaly_detection_error'; - -type Jobs = PromiseReturnType; - -function getMlErrorCode(e: Error) { - // Missing privileges - if (e instanceof InsufficientMLCapabilities) { - return ErrorCode.MISSING_READ_PRIVILEGES; - } - - if (e instanceof AnomalyDetectionError) { - return e.code; - } - - // unexpected error - return ErrorCode.UNEXPECTED; -} // get ML anomaly detection jobs for each environment export const anomalyDetectionJobsRoute = createRoute(() => ({ method: 'GET', path: '/api/apm/settings/anomaly-detection', + options: { + tags: ['access:apm', 'access:ml:canGetJobs'], + }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - try { - const [jobs, legacyJobs] = await Promise.all([ - getAnomalyDetectionJobs(setup, context.logger), - hasLegacyJobs(setup), - ]); - return { - jobs, - hasLegacyJobs: legacyJobs, - }; - } catch (e) { - const mlErrorCode = getMlErrorCode(e); - context.logger.warn(`Error while retrieving ML jobs: "${e.message}"`); - return { - jobs: [] as Jobs, - hasLegacyJobs: false, - errorCode: mlErrorCode, - }; + const license = context.licensing.license; + if (!license.isActive || !license.hasAtLeast('platinum')) { + throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } + + const [jobs, legacyJobs] = await Promise.all([ + getAnomalyDetectionJobs(setup, context.logger), + hasLegacyJobs(setup), + ]); + return { + jobs, + hasLegacyJobs: legacyJobs, + }; }, })); @@ -65,7 +45,7 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({ method: 'POST', path: '/api/apm/settings/anomaly-detection/jobs', options: { - tags: ['access:apm', 'access:apm_write'], + tags: ['access:apm', 'access:apm_write', 'access:ml:canCreateJob'], }, params: { body: t.type({ @@ -76,15 +56,12 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({ const { environments } = context.params.body; const setup = await setupRequest(context, request); - try { - await createAnomalyDetectionJobs(setup, environments, context.logger); - } catch (e) { - const mlErrorCode = getMlErrorCode(e); - context.logger.warn(`Error while creating ML job: "${e.message}"`); - return { - errorCode: mlErrorCode, - }; + const license = context.licensing.license; + if (!license.isActive || !license.hasAtLeast('platinum')) { + throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } + + await createAnomalyDetectionJobs(setup, environments, context.logger); }, })); diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index b1815e88d2917..c95719da881ea 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -45,7 +45,12 @@ export interface Route< method?: TMethod; params?: TParams; options?: { - tags: Array<'access:apm' | 'access:apm_write'>; + tags: Array< + | 'access:apm' + | 'access:apm_write' + | 'access:ml:canGetJobs' + | 'access:ml:canCreateJob' + >; }; handler: (kibanaContext: { context: APMRequestHandlerContext>; diff --git a/x-pack/plugins/canvas/shareable_runtime/postcss.config.js b/x-pack/plugins/canvas/shareable_runtime/postcss.config.js index 10baaddfc9b05..e1db6e4a64f71 100644 --- a/x-pack/plugins/canvas/shareable_runtime/postcss.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/postcss.config.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line const autoprefixer = require('autoprefixer'); const prefixer = require('postcss-prefix-selector'); diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index 93dc3dbccd549..43e422a161569 100644 --- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js @@ -111,7 +111,7 @@ module.exports = { loader: 'postcss-loader', options: { config: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/x-pack/plugins/canvas/storybook/webpack.config.js b/x-pack/plugins/canvas/storybook/webpack.config.js index 927f71b832ba0..982185a731b14 100644 --- a/x-pack/plugins/canvas/storybook/webpack.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.config.js @@ -77,7 +77,9 @@ module.exports = async ({ config }) => { { loader: 'postcss-loader', options: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + config: { + path: require.resolve('@kbn/optimizer/postcss.config.js'), + }, }, }, { @@ -114,7 +116,9 @@ module.exports = async ({ config }) => { { loader: 'postcss-loader', options: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + config: { + path: require.resolve('@kbn/optimizer/postcss.config.js'), + }, }, }, { diff --git a/x-pack/plugins/canvas/storybook/webpack.dll.config.js b/x-pack/plugins/canvas/storybook/webpack.dll.config.js index 0e9371e4cb5e4..81d19c035075f 100644 --- a/x-pack/plugins/canvas/storybook/webpack.dll.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.dll.config.js @@ -114,7 +114,7 @@ module.exports = { loader: 'postcss-loader', options: { config: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts index 3716e618068a3..02d5a415eec2e 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts @@ -18,15 +18,16 @@ import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/f import { InfraClientCoreSetup, InfraClientStartDeps } from '../types'; interface StatsAggregation { - buckets: Array<{ key: string; doc_count: number }>; -} - -interface SeriesAggregation { buckets: Array<{ - key_as_string: string; - key: number; + key: string; doc_count: number; - dataset: StatsAggregation; + series: { + buckets: Array<{ + key_as_string: string; + key: number; + doc_count: number; + }>; + }; }>; } @@ -131,18 +132,13 @@ function buildLogOverviewAggregations(logParams: LogParams, params: FetchDataPar terms: { field: 'event.dataset', size: 4, - }, - }, - series: { - date_histogram: { - field: logParams.timestampField, - fixed_interval: params.bucketSize, + missing: 'unknown', }, aggs: { - dataset: { - terms: { - field: 'event.dataset', - size: 4, + series: { + date_histogram: { + field: logParams.timestampField, + fixed_interval: params.bucketSize, }, }, }, @@ -152,34 +148,27 @@ function buildLogOverviewAggregations(logParams: LogParams, params: FetchDataPar function processLogsOverviewAggregations(aggregations: { stats: StatsAggregation; - series: SeriesAggregation; }): StatsAndSeries { - const processedStats = aggregations.stats.buckets.reduce( - (result, bucket) => { - result[bucket.key] = { - type: 'number', - label: bucket.key, - value: bucket.doc_count, - }; - - return result; - }, - {} - ); - - const processedSeries = aggregations.series.buckets.reduce( - (result, bucket) => { - const x = bucket.key; // the timestamp of the bucket - bucket.dataset.buckets.forEach((b) => { - const label = b.key; - result[label] = result[label] || { label, coordinates: [] }; - result[label].coordinates.push({ x, y: b.doc_count }); - }); + const processedStats: StatsAndSeries['stats'] = {}; + const processedSeries: StatsAndSeries['series'] = {}; - return result; - }, - {} - ); + aggregations.stats.buckets.forEach((stat) => { + const label = stat.key; + + processedStats[stat.key] = { + type: 'number', + label, + value: stat.doc_count, + }; + + stat.series.buckets.forEach((series) => { + processedSeries[label] = processedSeries[label] || { label, coordinates: [] }; + processedSeries[label].coordinates.push({ + x: series.key, + y: series.doc_count, + }); + }); + }); return { stats: processedStats, diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts index a2b4e162756e3..ea5c374c47d6e 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts @@ -8,18 +8,45 @@ import { CoreStart } from 'kibana/public'; import { coreMock } from 'src/core/public/mocks'; import { dataPluginMock } from 'src/plugins/data/public/mocks'; import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/fetch_log_source_status'; +import { callFetchLogSourceConfigurationAPI } from '../containers/logs/log_source/api/fetch_log_source_configuration'; import { InfraClientStartDeps, InfraClientStartExports } from '../types'; -import { getLogsHasDataFetcher } from './logs_overview_fetchers'; +import { getLogsHasDataFetcher, getLogsOverviewDataFetcher } from './logs_overview_fetchers'; +import { GetLogSourceConfigurationSuccessResponsePayload } from '../../common/http_api/log_sources/get_log_source_configuration'; jest.mock('../containers/logs/log_source/api/fetch_log_source_status'); const mockedCallFetchLogSourceStatusAPI = callFetchLogSourceStatusAPI as jest.MockedFunction< typeof callFetchLogSourceStatusAPI >; +jest.mock('../containers/logs/log_source/api/fetch_log_source_configuration'); +const mockedCallFetchLogSourceConfigurationAPI = callFetchLogSourceConfigurationAPI as jest.MockedFunction< + typeof callFetchLogSourceConfigurationAPI +>; + +const DEFAULT_PARAMS = { + absoluteTime: { start: 1593430680000, end: 1593430800000 }, + relativeTime: { start: 'now-2m', end: 'now' }, // Doesn't matter for the test + bucketSize: '30s', // Doesn't matter for the test +}; + function setup() { const core = coreMock.createStart(); const data = dataPluginMock.createStartContract(); + // `dataResponder.mockReturnValue()` will be the `response` in + // + // const searcher = data.search.getSearchStrategy('sth'); + // searcher.search(...).subscribe((**response**) => {}); + // + const dataResponder = jest.fn(); + + (data.search.search as jest.Mock).mockReturnValue({ + subscribe: (progress: Function, error: Function, finish: Function) => { + progress(dataResponder()); + finish(); + }, + }); + const mockedGetStartServices = jest.fn(() => { const deps = { data }; return Promise.resolve([ @@ -28,7 +55,7 @@ function setup() { void 0 as InfraClientStartExports, ]) as Promise<[CoreStart, InfraClientStartDeps, InfraClientStartExports]>; }); - return { core, mockedGetStartServices }; + return { core, mockedGetStartServices, dataResponder }; } describe('Logs UI Observability Homepage Functions', () => { @@ -80,8 +107,72 @@ describe('Logs UI Observability Homepage Functions', () => { }); describe('getLogsOverviewDataFetcher()', () => { - it.skip('should work', async () => { - // Pending + beforeAll(() => { + mockedCallFetchLogSourceConfigurationAPI.mockResolvedValue({ + data: { + configuration: { + logAlias: 'filebeat-*', + fields: { timestamp: '@timestamp', tiebreaker: '_doc' }, + }, + }, + } as GetLogSourceConfigurationSuccessResponsePayload); + }); + + afterAll(() => { + mockedCallFetchLogSourceConfigurationAPI.mockReset(); + }); + + it('should work', async () => { + const { mockedGetStartServices, dataResponder } = setup(); + + dataResponder.mockReturnValue({ + rawResponse: { + aggregations: { + stats: { + buckets: [ + { + key: 'nginx', + doc_count: 250, // Count is for 2 minutes + series: { + buckets: [ + // Counts are per 30 seconds + { key: 1593430680000, doc_count: 25 }, + { key: 1593430710000, doc_count: 50 }, + { key: 1593430740000, doc_count: 75 }, + { key: 1593430770000, doc_count: 100 }, + ], + }, + }, + ], + }, + }, + }, + }); + + const fetchData = getLogsOverviewDataFetcher(mockedGetStartServices); + const response = await fetchData(DEFAULT_PARAMS); + + expect(response).toMatchObject({ + stats: { + nginx: { + label: 'nginx', + type: 'number', + // Rate is normalized to logs in one minute + value: 125, + }, + }, + series: { + nginx: { + coordinates: [ + // Rates are normalized to logs in one minute + { x: 1593430680000, y: 50 }, + { x: 1593430710000, y: 100 }, + { x: 1593430740000, y: 150 }, + { x: 1593430770000, y: 200 }, + ], + }, + }, + }); }); }); }); diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index ef0cfdf0b4742..f3619fd1bd23e 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -14,9 +14,11 @@ import { Filter, Query, TimeRange } from 'src/plugins/data/public'; import { MapStoreState } from '../reducers/store'; import { getDataFilters, + getFilters, getMapSettings, getWaitingForMapReadyLayerListRaw, getQuery, + getTimeFilters, } from '../selectors/map_selectors'; import { CLEAR_GOTO, @@ -217,13 +219,13 @@ export function setQuery({ dispatch({ type: SET_QUERY, - timeFilters, + timeFilters: timeFilters ? timeFilters : getTimeFilters(getState()), query: { - ...query, + ...(query ? query : getQuery(getState())), // ensure query changes to trigger re-fetch when "Refresh" clicked queryLastTriggeredAt: refresh ? generateQueryTimestamp() : prevTriggeredAt, }, - filters, + filters: filters ? filters : getFilters(getState()), }); if (getMapSettings(getState()).autoFitToDataBounds) { diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index 8fb0ecb50b28b..5b73cd9e77201 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -48,7 +48,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { const { addLayerWithoutDataSync, createMapStore, - getIndexPatternService, + getIndexPatternsFromIds, getQueryableUniqueIndexPatternIds, } = await lazyLoadMapModules(); const store = createMapStore(); @@ -66,17 +66,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { ); } - const promises = queryableIndexPatternIds.map(async (indexPatternId) => { - try { - // @ts-ignore - return await getIndexPatternService().get(indexPatternId); - } catch (error) { - // Unable to load index pattern, better to not throw error so map embeddable can render - // Error will be surfaced by map embeddable since it too will be unable to locate the index pattern - return null; - } - }); - const indexPatterns = await Promise.all(promises); + const indexPatterns = await getIndexPatternsFromIds(queryableIndexPatternIds); return _.compact(indexPatterns) as IIndexPattern[]; } diff --git a/x-pack/plugins/maps/public/index_pattern_util.ts b/x-pack/plugins/maps/public/index_pattern_util.ts index e65e37ef19809..4b4bfb41990b9 100644 --- a/x-pack/plugins/maps/public/index_pattern_util.ts +++ b/x-pack/plugins/maps/public/index_pattern_util.ts @@ -30,11 +30,17 @@ export function getGeoTileAggNotSupportedReason(field: IFieldType): string | nul export async function getIndexPatternsFromIds( indexPatternIds: string[] = [] ): Promise { - const promises: Array> = []; - indexPatternIds.forEach((id) => { - promises.push(getIndexPatternService().get(id)); + const promises: IndexPattern[] = []; + indexPatternIds.forEach(async (indexPatternId) => { + try { + // @ts-ignore + promises.push(getIndexPatternService().get(indexPatternId)); + } catch (error) { + // Unable to load index pattern, better to not throw error so map can render + // Error will be surfaced by layer since it too will be unable to locate the index pattern + return null; + } }); - return await Promise.all(promises); } diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts index 12d6d75ac57ba..b77bf208c5865 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts @@ -8,6 +8,7 @@ import { AnyAction } from 'redux'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IndexPatternsService } from 'src/plugins/data/public/index_patterns'; import { ReactElement } from 'react'; +import { IndexPattern } from 'src/plugins/data/public'; import { Embeddable, IContainer } from '../../../../../src/plugins/embeddable/public'; import { LayerDescriptor } from '../../common/descriptor_types'; import { MapStore, MapStoreState } from '../reducers/store'; @@ -44,8 +45,9 @@ interface LazyLoadedMapModules { indexPatternId: string, indexPatternTitle: string ) => LayerDescriptor[]; - registerLayerWizard(layerWizard: LayerWizard): void; + registerLayerWizard: (layerWizard: LayerWizard) => void; registerSource(entry: SourceRegistryEntry): void; + getIndexPatternsFromIds: (indexPatternIds: string[]) => Promise; } export async function lazyLoadMapModules(): Promise { @@ -71,6 +73,7 @@ export async function lazyLoadMapModules(): Promise { createSecurityLayerDescriptors, registerLayerWizard, registerSource, + getIndexPatternsFromIds, } = await import('./lazy'); resolve({ @@ -88,6 +91,7 @@ export async function lazyLoadMapModules(): Promise { createSecurityLayerDescriptors, registerLayerWizard, registerSource, + getIndexPatternsFromIds, }); }); return loadModulesPromise; diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts index c839122ab90b1..e55160383a8f3 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts @@ -21,3 +21,4 @@ export * from '../../routing/maps_router'; export * from '../../classes/layers/solution_layers/security'; export { registerLayerWizard } from '../../classes/layers/layer_wizard_registry'; export { registerSource } from '../../classes/sources/source_registry'; +export { getIndexPatternsFromIds } from '../../index_pattern_util'; diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js index cf6f51c8aeacf..5d02160fc3eb5 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js @@ -3,7 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import _ from 'lodash'; +import rison from 'rison-node'; +import { i18n } from '@kbn/i18n'; // Import each layer type, even those not used, to init in registry import '../../classes/sources/wms_source'; import '../../classes/sources/ems_file_source'; @@ -16,7 +19,7 @@ import { KibanaTilemapSource } from '../../classes/sources/kibana_tilemap_source import { TileLayer } from '../../classes/layers/tile_layer/tile_layer'; import { EMSTMSSource } from '../../classes/sources/ems_tms_source'; import { VectorTileLayer } from '../../classes/layers/vector_tile_layer/vector_tile_layer'; -import { getIsEmsEnabled } from '../../kibana_services'; +import { getIsEmsEnabled, getToasts } from '../../kibana_services'; import { getKibanaTileMap } from '../../meta'; export function getInitialLayers(layerListJSON, initialLayers = []) { @@ -41,3 +44,33 @@ export function getInitialLayers(layerListJSON, initialLayers = []) { return initialLayers; } + +export function getInitialLayersFromUrlParam() { + const locationSplit = window.location.href.split('?'); + if (locationSplit.length <= 1) { + return []; + } + const mapAppParams = new URLSearchParams(locationSplit[1]); + if (!mapAppParams.has('initialLayers')) { + return []; + } + + try { + let mapInitLayers = mapAppParams.get('initialLayers'); + if (mapInitLayers[mapInitLayers.length - 1] === '#') { + mapInitLayers = mapInitLayers.substr(0, mapInitLayers.length - 1); + } + return rison.decode_array(mapInitLayers); + } catch (e) { + getToasts().addWarning({ + title: i18n.translate('xpack.maps.initialLayers.unableToParseTitle', { + defaultMessage: `Initial layers not added to map`, + }), + text: i18n.translate('xpack.maps.initialLayers.unableToParseMessage', { + defaultMessage: `Unable to parse contents of 'initialLayers' parameter. Error: {errorMsg}`, + values: { errorMsg: e.message }, + }), + }); + return []; + } +} diff --git a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js b/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js index 2575bbb9df920..4692bb1db3477 100644 --- a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js +++ b/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js @@ -10,20 +10,27 @@ import { enableFullScreen, openMapSettings, removePreviewLayers, - setRefreshConfig, setSelectedLayer, updateFlyout, } from '../../../actions'; import { FLYOUT_STATE } from '../../../reducers/ui'; import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; import { getFlyoutDisplay } from '../../../selectors/ui_selectors'; -import { hasDirtyState } from '../../../selectors/map_selectors'; +import { + getQuery, + getRefreshConfig, + getTimeFilters, + hasDirtyState, +} from '../../../selectors/map_selectors'; function mapStateToProps(state = {}) { return { isOpenSettingsDisabled: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, inspectorAdapters: getInspectorAdapters(state), isSaveDisabled: hasDirtyState(state), + query: getQuery(state), + refreshConfig: getRefreshConfig(state), + timeFilters: getTimeFilters(state), }; } @@ -34,7 +41,6 @@ function mapDispatchToProps(dispatch) { dispatch(updateFlyout(FLYOUT_STATE.NONE)); dispatch(removePreviewLayers()); }, - setRefreshStoreConfig: (refreshConfig) => dispatch(setRefreshConfig(refreshConfig)), enableFullScreen: () => dispatch(enableFullScreen()), openMapSettings: () => dispatch(openMapSettings()), }; diff --git a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js b/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js index 2340e3716547b..be474b43da81a 100644 --- a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js +++ b/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js @@ -29,18 +29,16 @@ export function MapsTopNavMenu({ onQuerySaved, onSavedQueryUpdated, savedQuery, - time, + timeFilters, refreshConfig, - setRefreshConfig, - setRefreshStoreConfig, + onRefreshConfigChange, indexPatterns, - updateFiltersAndDispatch, + onFiltersChange, isSaveDisabled, closeFlyout, enableFullScreen, openMapSettings, inspectorAdapters, - syncAppAndGlobalState, setBreadcrumbs, isOpenSettingsDisabled, }) { @@ -75,31 +73,20 @@ export function MapsTopNavMenu({ }); }; - const onRefreshChange = function ({ isPaused, refreshInterval }) { - const newRefreshConfig = { - isPaused, - interval: isNaN(refreshInterval) ? refreshConfig.interval : refreshInterval, - }; - setRefreshConfig(newRefreshConfig, () => { - setRefreshStoreConfig(newRefreshConfig); - syncAppAndGlobalState(); - }); - }; - return ( { return hasUnsavedChanges(state, savedMap, initialLayerListConfig); }, + query: getQuery(state), + timeFilters: getTimeFilters(state), }; } function mapDispatchToProps(dispatch) { return { - dispatchSetQuery: (refresh, filters, query, time) => { + dispatchSetQuery: ({ refresh, filters, query, timeFilters }) => { dispatch( setQuery({ filters, query, - timeFilters: time, + timeFilters, refresh, }) ); diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js index b26c44df25104..d945aa9623b21 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js @@ -9,15 +9,9 @@ import { i18n } from '@kbn/i18n'; import 'mapbox-gl/dist/mapbox-gl.css'; import _ from 'lodash'; import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../reducers/ui'; -import { - getIndexPatternService, - getToasts, - getData, - getCoreChrome, -} from '../../../kibana_services'; +import { getData, getCoreChrome } from '../../../kibana_services'; import { copyPersistentState } from '../../../reducers/util'; -import { getInitialLayers } from '../../bootstrap/get_initial_layers'; -import rison from 'rison-node'; +import { getInitialLayers, getInitialLayersFromUrlParam } from '../../bootstrap/get_initial_layers'; import { getInitialTimeFilters } from '../../bootstrap/get_initial_time_filters'; import { getInitialRefreshConfig } from '../../bootstrap/get_initial_refresh_config'; import { getInitialQuery } from '../../bootstrap/get_initial_query'; @@ -25,13 +19,14 @@ import { MapsTopNavMenu } from '../../page_elements/top_nav_menu'; import { getGlobalState, updateGlobalState, - useGlobalStateSyncing, + startGlobalStateSyncing, } from '../../state_syncing/global_sync'; import { AppStateManager } from '../../state_syncing/app_state_manager'; -import { useAppStateSyncing } from '../../state_syncing/app_sync'; +import { startAppStateSyncing } from '../../state_syncing/app_sync'; import { esFilters } from '../../../../../../../src/plugins/data/public'; import { MapContainer } from '../../../connected_components/map_container'; import { goToSpecifiedPath } from '../../maps_router'; +import { getIndexPatternsFromIds } from '../../../index_pattern_util'; const unsavedChangesWarning = i18n.translate('xpack.maps.breadCrumbs.unsavedChangesWarning', { defaultMessage: 'Your map has unsaved changes. Are you sure you want to leave?', @@ -42,12 +37,12 @@ export class MapsAppView extends React.Component { _globalSyncChangeMonitorSubscription = null; _appSyncUnsubscribe = null; _appStateManager = new AppStateManager(); + _prevIndexPatternIds = null; constructor(props) { super(props); this.state = { indexPatterns: [], - prevIndexPatternIds: [], initialized: false, savedQuery: '', initialLayerListConfig: null, @@ -55,21 +50,15 @@ export class MapsAppView extends React.Component { } componentDidMount() { - // Init sync utils - // eslint-disable-next-line react-hooks/rules-of-hooks - this._globalSyncUnsubscribe = useGlobalStateSyncing(); - // eslint-disable-next-line react-hooks/rules-of-hooks - this._appSyncUnsubscribe = useAppStateSyncing(this._appStateManager); + this._isMounted = true; + + this._globalSyncUnsubscribe = startGlobalStateSyncing(); + this._appSyncUnsubscribe = startAppStateSyncing(this._appStateManager); this._globalSyncChangeMonitorSubscription = getData().query.state$.subscribe( this._updateFromGlobalState ); - // Check app state in case of refresh - const initAppState = this._appStateManager.getAppState(); - this._onQueryChange(initAppState); - if (initAppState.savedQuery) { - this._updateStateFromSavedQuery(initAppState.savedQuery); - } + this._updateStateFromSavedQuery(this._appStateManager.getAppState().savedQuery); this._initMap(); @@ -86,11 +75,12 @@ export class MapsAppView extends React.Component { } componentDidUpdate() { - // TODO: Handle null when converting to TS - this._handleStoreChanges(); + this._updateIndexPatterns(); } componentWillUnmount() { + this._isMounted = false; + if (this._globalSyncUnsubscribe) { this._globalSyncUnsubscribe(); } @@ -101,14 +91,6 @@ export class MapsAppView extends React.Component { this._globalSyncChangeMonitorSubscription.unsubscribe(); } - // Clean up app state filters - const { filterManager } = getData().query; - filterManager.filters.forEach((filter) => { - if (filter.$state.store === esFilters.FilterStateStore.APP_STATE) { - filterManager.removeFilter(filter); - } - }); - getCoreChrome().setBreadcrumbs([]); } @@ -138,183 +120,96 @@ export class MapsAppView extends React.Component { }; _updateFromGlobalState = ({ changes, state: globalState }) => { - if (!changes || !globalState) { + if (!this.state.initialized || !changes || !globalState) { return; } - const newState = {}; - Object.keys(changes).forEach((key) => { - if (changes[key]) { - newState[key] = globalState[key]; - } - }); - this.setState(newState, () => { - this._appStateManager.setQueryAndFilters({ - filters: getData().query.filterManager.getAppFilters(), - }); - const { time, filters, refreshInterval } = globalState; - this.props.dispatchSetQuery(refreshInterval, filters, this.state.query, time); - }); + this._onQueryChange({ time: globalState.time, refresh: true }); }; - _getInitialLayersFromUrlParam() { - const locationSplit = window.location.href.split('?'); - if (locationSplit.length <= 1) { - return []; - } - const mapAppParams = new URLSearchParams(locationSplit[1]); - if (!mapAppParams.has('initialLayers')) { - return []; - } + async _updateIndexPatterns() { + const { nextIndexPatternIds } = this.props; - try { - let mapInitLayers = mapAppParams.get('initialLayers'); - if (mapInitLayers[mapInitLayers.length - 1] === '#') { - mapInitLayers = mapInitLayers.substr(0, mapInitLayers.length - 1); - } - return rison.decode_array(mapInitLayers); - } catch (e) { - getToasts().addWarning({ - title: i18n.translate('xpack.maps.initialLayers.unableToParseTitle', { - defaultMessage: `Initial layers not added to map`, - }), - text: i18n.translate('xpack.maps.initialLayers.unableToParseMessage', { - defaultMessage: `Unable to parse contents of 'initialLayers' parameter. Error: {errorMsg}`, - values: { errorMsg: e.message }, - }), - }); - return []; + if (_.isEqual(nextIndexPatternIds, this._prevIndexPatternIds)) { + return; } - } - async _updateIndexPatterns(nextIndexPatternIds) { - const indexPatterns = []; - const getIndexPatternPromises = nextIndexPatternIds.map(async (indexPatternId) => { - try { - const indexPattern = await getIndexPatternService().get(indexPatternId); - indexPatterns.push(indexPattern); - } catch (err) { - // unable to fetch index pattern - } - }); + this._prevIndexPatternIds = nextIndexPatternIds; - await Promise.all(getIndexPatternPromises); - this.setState({ - indexPatterns, - }); + const indexPatterns = await getIndexPatternsFromIds(nextIndexPatternIds); + if (this._isMounted) { + this.setState({ indexPatterns }); + } } - _handleStoreChanges = () => { - const { prevIndexPatternIds } = this.state; - const { nextIndexPatternIds } = this.props; + _onQueryChange = ({ filters, query, time, refresh = false }) => { + const { filterManager } = getData().query; - if (nextIndexPatternIds !== prevIndexPatternIds) { - this.setState({ prevIndexPatternIds: nextIndexPatternIds }); - this._updateIndexPatterns(nextIndexPatternIds); + if (filters) { + filterManager.setFilters(filters); } - }; - _getAppStateFilters = () => { - return this._appStateManager.getFilters() || []; - }; - - _syncAppAndGlobalState = () => { - const { query, time, initialized } = this.state; - const { refreshConfig } = this.props; - const { filterManager } = getData().query; + this.props.dispatchSetQuery({ + refresh, + filters: filterManager.getFilters(), + query, + timeFilters: time, + }); - // appState + // sync appState this._appStateManager.setQueryAndFilters({ - query: query, filters: filterManager.getAppFilters(), + query, }); - // globalState - const refreshInterval = { - pause: refreshConfig.isPaused, - value: refreshConfig.interval, - }; - updateGlobalState( - { - time: time, - refreshInterval, - filters: filterManager.getGlobalFilters(), - }, - !initialized - ); - this.setState({ refreshInterval }); + // sync globalState + const updatedGlobalState = { filters: filterManager.getGlobalFilters() }; + if (time) { + updatedGlobalState.time = time; + } + updateGlobalState(updatedGlobalState, !this.state.initialized); }; - _onQueryChange = async ({ filters, query, time, refresh }) => { - const { filterManager } = getData().query; - const { dispatchSetQuery } = this.props; - const newState = {}; - let newFilters; - if (filters) { - filterManager.setFilters(filters); // Maps and merges filters - newFilters = filterManager.getFilters(); + _initMapAndLayerSettings() { + const globalState = getGlobalState(); + const mapStateJSON = this.props.savedMap.mapStateJSON; + + let savedObjectFilters = []; + if (mapStateJSON) { + const mapState = JSON.parse(mapStateJSON); + if (mapState.filters) { + savedObjectFilters = mapState.filters; + } } + const appFilters = this._appStateManager.getFilters() || []; + + const query = getInitialQuery({ + mapStateJSON, + appState: this._appStateManager.getAppState(), + }); if (query) { - newState.query = query; - } - if (time) { - newState.time = time; + getData().query.queryString.setQuery(query); } - this.setState(newState, () => { - this._syncAppAndGlobalState(); - dispatchSetQuery( - refresh, - newFilters || this.props.filters, - query || this.state.query, - time || this.state.time - ); - }); - }; - _initQueryTimeRefresh() { - const { setRefreshConfig, savedMap } = this.props; - const { queryString } = getData().query; - // TODO: Handle null when converting to TS - const globalState = getGlobalState(); - const mapStateJSON = savedMap ? savedMap.mapStateJSON : undefined; - const newState = { - query: getInitialQuery({ - mapStateJSON, - appState: this._appStateManager.getAppState(), - }), + this._onQueryChange({ + filters: [..._.get(globalState, 'filters', []), ...appFilters, ...savedObjectFilters], + query, time: getInitialTimeFilters({ mapStateJSON, globalState, }), - refreshConfig: getInitialRefreshConfig({ + }); + + this._onRefreshConfigChange( + getInitialRefreshConfig({ mapStateJSON, globalState, - }), - }; - - if (newState.query) queryString.setQuery(newState.query); - this.setState({ query: newState.query, time: newState.time }); - updateGlobalState( - { - time: newState.time, - refreshInterval: { - value: newState.refreshConfig.interval, - pause: newState.refreshConfig.isPaused, - }, - }, - !this.state.initialized + }) ); - setRefreshConfig(newState.refreshConfig); - } - - _initMapAndLayerSettings() { - const { savedMap } = this.props; - // Get saved map & layer settings - this._initQueryTimeRefresh(); const layerList = getInitialLayers( - savedMap.layerListJSON, - this._getInitialLayersFromUrlParam() + this.props.savedMap.layerListJSON, + getInitialLayersFromUrlParam() ); this.props.replaceLayerList(layerList); this.setState({ @@ -322,20 +217,31 @@ export class MapsAppView extends React.Component { }); } - _updateFiltersAndDispatch = (filters) => { + _onFiltersChange = (filters) => { this._onQueryChange({ filters, }); }; - _onRefreshChange = ({ isPaused, refreshInterval }) => { - const { refreshConfig } = this.props; - const newRefreshConfig = { + // mapRefreshConfig: MapRefreshConfig + _onRefreshConfigChange(mapRefreshConfig) { + this.props.setRefreshConfig(mapRefreshConfig); + updateGlobalState( + { + refreshInterval: { + pause: mapRefreshConfig.isPaused, + value: mapRefreshConfig.interval, + }, + }, + !this.state.initialized + ); + } + + _onTopNavRefreshConfig = ({ isPaused, refreshInterval }) => { + this._onRefreshConfigChange({ isPaused, - interval: isNaN(refreshInterval) ? refreshConfig.interval : refreshInterval, - }; - this.setState({ refreshConfig: newRefreshConfig }, this._syncAppAndGlobalState); - this.props.setRefreshConfig(newRefreshConfig); + interval: refreshInterval, + }); }; _updateStateFromSavedQuery(savedQuery) { @@ -348,98 +254,55 @@ export class MapsAppView extends React.Component { const globalFilters = filterManager.getGlobalFilters(); const allFilters = [...savedQueryFilters, ...globalFilters]; - if (savedQuery.attributes.timefilter) { - if (savedQuery.attributes.timefilter.refreshInterval) { - this._onRefreshChange({ - isPaused: savedQuery.attributes.timefilter.refreshInterval.pause, - refreshInterval: savedQuery.attributes.timefilter.refreshInterval.value, - }); - } - this._onQueryChange({ - filters: allFilters, - query: savedQuery.attributes.query, - time: savedQuery.attributes.timefilter, - }); - } else { - this._onQueryChange({ - filters: allFilters, - query: savedQuery.attributes.query, + const refreshInterval = _.get(savedQuery, 'attributes.timefilter.refreshInterval'); + if (refreshInterval) { + this._onRefreshConfigChange({ + isPaused: refreshInterval.pause, + interval: refreshInterval.value, }); } + this._onQueryChange({ + filters: allFilters, + query: savedQuery.attributes.query, + time: savedQuery.attributes.timefilter, + }); } - _syncStoreAndGetFilters() { - const { - savedMap, - setGotoWithCenter, - setMapSettings, - setIsLayerTOCOpen, - setOpenTOCDetails, - } = this.props; - let savedObjectFilters = []; - if (savedMap.mapStateJSON) { - const mapState = JSON.parse(savedMap.mapStateJSON); - setGotoWithCenter({ + _initMap() { + this._initMapAndLayerSettings(); + + this.props.clearUi(); + + if (this.props.savedMap.mapStateJSON) { + const mapState = JSON.parse(this.props.savedMap.mapStateJSON); + this.props.setGotoWithCenter({ lat: mapState.center.lat, lon: mapState.center.lon, zoom: mapState.zoom, }); - if (mapState.filters) { - savedObjectFilters = mapState.filters; - } if (mapState.settings) { - setMapSettings(mapState.settings); + this.props.setMapSettings(mapState.settings); } } - if (savedMap.uiStateJSON) { - const uiState = JSON.parse(savedMap.uiStateJSON); - setIsLayerTOCOpen(_.get(uiState, 'isLayerTOCOpen', DEFAULT_IS_LAYER_TOC_OPEN)); - setOpenTOCDetails(_.get(uiState, 'openTOCDetails', [])); + if (this.props.savedMap.uiStateJSON) { + const uiState = JSON.parse(this.props.savedMap.uiStateJSON); + this.props.setIsLayerTOCOpen(_.get(uiState, 'isLayerTOCOpen', DEFAULT_IS_LAYER_TOC_OPEN)); + this.props.setOpenTOCDetails(_.get(uiState, 'openTOCDetails', [])); } - return savedObjectFilters; - } - async _initMap() { - const { clearUi, savedMap } = this.props; - // TODO: Handle null when converting to TS - const globalState = getGlobalState(); - this._initMapAndLayerSettings(); - clearUi(); - - const savedObjectFilters = this._syncStoreAndGetFilters(savedMap); - await this._onQueryChange({ - filters: [ - ..._.get(globalState, 'filters', []), - ...this._getAppStateFilters(), - ...savedObjectFilters, - ], - }); this.setState({ initialized: true }); } _renderTopNav() { - const { query, time, savedQuery, indexPatterns } = this.state; - const { savedMap, refreshConfig, isFullScreen } = this.props; - - return !isFullScreen ? ( + return !this.props.isFullScreen ? ( { - this.setState( - { - refreshConfig: newConfig, - }, - callback - ); - }} - indexPatterns={indexPatterns} - updateFiltersAndDispatch={this._updateFiltersAndDispatch} + onRefreshConfigChange={this._onTopNavRefreshConfig} + indexPatterns={this.state.indexPatterns} + onFiltersChange={this._onFiltersChange} onQuerySaved={(query) => { this.setState({ savedQuery: query }); this._appStateManager.setQueryAndFilters({ savedQuery: query }); @@ -450,7 +313,6 @@ export class MapsAppView extends React.Component { this._appStateManager.setQueryAndFilters({ savedQuery: query }); this._updateStateFromSavedQuery(query); }} - syncAppAndGlobalState={this._syncAppAndGlobalState} setBreadcrumbs={this._setBreadcrumbs} /> ) : null; @@ -469,7 +331,7 @@ export class MapsAppView extends React.Component { newFilters.forEach((filter) => { filter.$state = { store: esFilters.FilterStateStore.APP_STATE }; }); - this._updateFiltersAndDispatch([...filters, ...newFilters]); + this._onFiltersChange([...filters, ...newFilters]); }} /> diff --git a/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js b/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js index 19118c6130805..4cdba13bd85d2 100644 --- a/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js +++ b/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js @@ -14,13 +14,13 @@ export class AppStateManager { _updated$ = new Subject(); setQueryAndFilters({ query, savedQuery, filters }) { - if (this._query !== query) { + if (query && this._query !== query) { this._query = query; } - if (this._savedQuery !== savedQuery) { + if (savedQuery && this._savedQuery !== savedQuery) { this._savedQuery = savedQuery; } - if (this._filters !== filters) { + if (filters && this._filters !== filters) { this._filters = filters; } this._updated$.next(); diff --git a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js index 69d6dbbe0c4d3..60e8dc9cd574c 100644 --- a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js +++ b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js @@ -10,11 +10,15 @@ import { map } from 'rxjs/operators'; import { getData } from '../../kibana_services'; import { kbnUrlStateStorage } from '../maps_router'; -export function useAppStateSyncing(appStateManager) { +export function startAppStateSyncing(appStateManager) { // get appStateContainer // sync app filters with app state container from data.query to state container const { query } = getData(); + // Filter manager state persists across applications + // clear app state filters to prevent application filters from other applications being transfered to maps + query.filterManager.setAppFilters([]); + const stateContainer = { get: () => ({ query: appStateManager.getQuery(), diff --git a/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts index b466f254e4d08..4e17241752f53 100644 --- a/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts +++ b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts @@ -9,7 +9,7 @@ import { getData } from '../../kibana_services'; // @ts-ignore import { kbnUrlStateStorage } from '../maps_router'; -export function useGlobalStateSyncing() { +export function startGlobalStateSyncing() { const { stop } = syncQueryStateWithUrl(getData().query, kbnUrlStateStorage); return stop; } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts index 6111974f369c8..49ee5ec6b90b0 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts @@ -167,7 +167,7 @@ export function processResults({ results, overrides }: AnalysisResult) { */ export async function hasImportPermission(indexName: string) { const priv: { cluster: string[]; index?: any } = { - cluster: ['cluster:monitor/nodes/info', 'cluster:admin/ingest/pipeline/put'], + cluster: ['cluster:admin/ingest/pipeline/put'], }; if (indexName !== undefined) { diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 9a92270fc9c14..aa3f0bf287fca 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -8,16 +8,26 @@ import seedrandom from 'seedrandom'; import { AlertEvent, EndpointEvent, + EndpointStatus, Host, HostMetadata, - OSFields, HostPolicyResponse, HostPolicyResponseActionStatus, + OSFields, PolicyData, - EndpointStatus, } from './types'; import { factory as policyFactory } from './models/policy_config'; import { parentEntityId } from './models/event'; +import { + GetAgentConfigsResponseItem, + GetPackagesResponse, +} from '../../../ingest_manager/common/types/rest_spec'; +import { + AgentConfigStatus, + EsAssetReference, + InstallationStatus, + KibanaAssetReference, +} from '../../../ingest_manager/common/types/models'; export type Event = AlertEvent | EndpointEvent; /** @@ -1062,6 +1072,97 @@ export class EndpointDocGenerator { }; } + /** + * Generate an Agent Configuration (ingest) + */ + public generateAgentConfig(): GetAgentConfigsResponseItem { + return { + id: this.seededUUIDv4(), + name: 'Agent Config', + status: AgentConfigStatus.Active, + description: 'Some description', + namespace: 'default', + monitoring_enabled: ['logs', 'metrics'], + revision: 2, + updated_at: '2020-07-22T16:36:49.196Z', + updated_by: 'elastic', + package_configs: ['852491f0-cc39-11ea-bac2-cdbf95b4b41a'], + agents: 0, + }; + } + + /** + * Generate an EPM Package for Endpoint + */ + public generateEpmPackage(): GetPackagesResponse['response'][0] { + return { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + description: 'This is the Elastic Endpoint package.', + type: 'solution', + download: '/epr/endpoint/endpoint-0.5.0.tar.gz', + path: '/package/endpoint/0.5.0', + icons: [ + { + src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg', + size: '16x16', + type: 'image/svg+xml', + }, + ], + status: 'installed' as InstallationStatus, + savedObject: { + type: 'epm-packages', + id: 'endpoint', + attributes: { + installed_kibana: [ + { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' }, + { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' }, + ] as KibanaAssetReference[], + installed_es: [ + { id: 'logs-endpoint.alerts', type: 'index_template' }, + { id: 'events-endpoint', type: 'index_template' }, + { id: 'logs-endpoint.events.file', type: 'index_template' }, + { id: 'logs-endpoint.events.library', type: 'index_template' }, + { id: 'metrics-endpoint.metadata', type: 'index_template' }, + { id: 'metrics-endpoint.metadata_mirror', type: 'index_template' }, + { id: 'logs-endpoint.events.network', type: 'index_template' }, + { id: 'metrics-endpoint.policy', type: 'index_template' }, + { id: 'logs-endpoint.events.process', type: 'index_template' }, + { id: 'logs-endpoint.events.registry', type: 'index_template' }, + { id: 'logs-endpoint.events.security', type: 'index_template' }, + { id: 'metrics-endpoint.telemetry', type: 'index_template' }, + ] as EsAssetReference[], + es_index_patterns: { + alerts: 'logs-endpoint.alerts-*', + events: 'events-endpoint-*', + file: 'logs-endpoint.events.file-*', + library: 'logs-endpoint.events.library-*', + metadata: 'metrics-endpoint.metadata-*', + metadata_mirror: 'metrics-endpoint.metadata_mirror-*', + network: 'logs-endpoint.events.network-*', + policy: 'metrics-endpoint.policy-*', + process: 'logs-endpoint.events.process-*', + registry: 'logs-endpoint.events.registry-*', + security: 'logs-endpoint.events.security-*', + telemetry: 'metrics-endpoint.telemetry-*', + }, + name: 'endpoint', + version: '0.5.0', + internal: false, + removable: false, + }, + references: [], + updated_at: '2020-06-24T14:41:23.098Z', + version: 'Wzc0LDFd', + }, + }; + } + /** * Generates a Host Policy response message */ diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index a982f9ffe8f21..1c24e1abe5a57 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -104,6 +104,8 @@ export interface ResolverChildNode extends ResolverLifecycleNode { * * string: Indicates this is a leaf node and it can be used to continue querying for additional descendants * using this node's entity_id + * + * For more information see the resolver docs on pagination [here](../../server/endpoint/routes/resolver/docs/README.md#L129) */ nextChild?: string | null; } diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index f40161ff9b4c2..84096e242cbbd 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -256,12 +256,6 @@ "type": { "kind": "ENUM", "name": "TimelineType", "ofType": null }, "defaultValue": null }, - { - "name": "templateTimelineType", - "description": "", - "type": { "kind": "ENUM", "name": "TemplateTimelineType", "ofType": null }, - "defaultValue": null - }, { "name": "status", "description": "", @@ -10981,24 +10975,6 @@ ], "possibleTypes": null }, - { - "kind": "ENUM", - "name": "TemplateTimelineType", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "elastic", - "description": "", - "isDeprecated": false, - "deprecationReason": null - }, - { "name": "custom", "description": "", "isDeprecated": false, "deprecationReason": null } - ], - "possibleTypes": null - }, { "kind": "OBJECT", "name": "ResponseTimelines", @@ -13166,6 +13142,24 @@ "interfaces": null, "enumValues": null, "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "TemplateTimelineType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "elastic", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "custom", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null } ], "directives": [ diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index d7a2535fb1f54..90d1b8bd54df5 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -397,11 +397,6 @@ export enum SortFieldTimeline { created = 'created', } -export enum TemplateTimelineType { - elastic = 'elastic', - custom = 'custom', -} - export enum NetworkDirectionEcs { inbound = 'inbound', outbound = 'outbound', @@ -428,6 +423,11 @@ export enum FlowDirection { biDirectional = 'biDirectional', } +export enum TemplateTimelineType { + elastic = 'elastic', + custom = 'custom', +} + export type ToStringArrayNoNullable = any; export type ToIFieldSubTypeNonNullable = any; @@ -2334,8 +2334,6 @@ export interface GetAllTimelineQueryArgs { timelineType?: Maybe; - templateTimelineType?: Maybe; - status?: Maybe; } export interface AuthenticationsSourceArgs { @@ -4435,7 +4433,6 @@ export namespace GetAllTimeline { sort?: Maybe; onlyUserFavorite?: Maybe; timelineType?: Maybe; - templateTimelineType?: Maybe; status?: Maybe; }; diff --git a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx index aa562b9a20201..54d9131209d0d 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx @@ -5,10 +5,15 @@ */ import React, { memo } from 'react'; +import { EuiErrorBoundary } from '@elastic/eui'; import { PageView, PageViewProps } from '../../common/components/endpoint/page_view'; export const ManagementPageView = memo>((options) => { - return ; + return ( + + + + ); }); ManagementPageView.displayName = 'ManagementPageView'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index 621fab2e4ee11..4a4326d5b2919 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -81,6 +81,11 @@ interface ServerReturnedHostNonExistingPolicies { payload: HostState['nonExistingPolicies']; } +interface ServerReturnedHostExistValue { + type: 'serverReturnedHostExistValue'; + payload: boolean; +} + export type HostAction = | ServerReturnedHostList | ServerFailedToReturnHostList @@ -92,6 +97,7 @@ export type HostAction = | ServerFailedToReturnPoliciesForOnboarding | UserSelectedEndpointPolicy | ServerCancelledHostListLoading + | ServerReturnedHostExistValue | ServerCancelledPolicyItemsLoading | ServerReturnedEndpointPackageInfo | ServerReturnedHostNonExistingPolicies; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index b6e18506b6111..8ff4ad5a043b5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -51,6 +51,7 @@ describe('HostList store concerns', () => { policyItemsLoading: false, endpointPackageInfo: undefined, nonExistingPolicies: {}, + hostsExist: true, }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index edeca5659ee38..74bebf211258a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpSetup } from 'kibana/public'; +import { HttpStart } from 'kibana/public'; import { HostInfo, HostResultList } from '../../../../../common/endpoint/types'; import { GetPolicyListResponse } from '../../policy/types'; import { ImmutableMiddlewareFactory } from '../../../../common/store'; @@ -28,6 +28,8 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor return ({ getState, dispatch }) => (next) => async (action) => { next(action); const state = getState(); + + // Host list if ( action.type === 'userChangedUrl' && isOnHostPage(state) && @@ -89,6 +91,19 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor // No hosts, so we should check to see if there are policies for onboarding if (hostResponse && hostResponse.hosts.length === 0) { const http = coreStart.http; + + // The original query to the list could have had an invalid param (ex. invalid page_size), + // so we check first if hosts actually do exist before pulling in data for the onboarding + // messages. + if (await doHostsExist(http)) { + return; + } + + dispatch({ + type: 'serverReturnedHostExistValue', + payload: false, + }); + try { const policyDataResponse: GetPolicyListResponse = await sendGetEndpointSpecificPackageConfigs( http, @@ -119,6 +134,8 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor }); } } + + // Host Details if (action.type === 'userChangedUrl' && hasSelectedHost(state) === true) { dispatch({ type: 'serverCancelledPolicyItemsLoading', @@ -160,7 +177,6 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor type: 'serverFailedToReturnHostList', payload: error, }); - return; } } else { dispatch({ @@ -217,7 +233,7 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor }; const getNonExistingPoliciesForHostsList = async ( - http: HttpSetup, + http: HttpStart, hosts: HostResultList['hosts'], currentNonExistingPolicies: HostState['nonExistingPolicies'] ): Promise => { @@ -274,3 +290,23 @@ const getNonExistingPoliciesForHostsList = async ( return nonExisting; }; + +const doHostsExist = async (http: HttpStart): Promise => { + try { + return ( + ( + await http.post('/api/endpoint/metadata', { + body: JSON.stringify({ + paging_properties: [{ page_index: 0 }, { page_size: 1 }], + }), + }) + ).hosts.length !== 0 + ); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`error while trying to check if hosts exist`); + // eslint-disable-next-line no-console + console.error(error); + } + return false; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts index 05af1ee062de6..355c2bb5c19fc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts @@ -4,8 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostInfo, HostResultList, HostStatus } from '../../../../../common/endpoint/types'; +import { HttpStart } from 'kibana/public'; +import { + GetHostPolicyResponse, + HostInfo, + HostPolicyResponse, + HostResultList, + HostStatus, +} from '../../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; +import { + INGEST_API_AGENT_CONFIGS, + INGEST_API_EPM_PACKAGES, + INGEST_API_PACKAGE_CONFIGS, +} from '../../policy/store/policy_list/services/ingest'; +import { + GetAgentConfigsResponse, + GetPackagesResponse, +} from '../../../../../../ingest_manager/common/types/rest_spec'; +import { GetPolicyListResponse } from '../../policy/types'; + +const generator = new EndpointDocGenerator('seed'); export const mockHostResultList: (options?: { total?: number; @@ -26,7 +45,6 @@ export const mockHostResultList: (options?: { const hosts = []; for (let index = 0; index < actualCountToReturn; index++) { - const generator = new EndpointDocGenerator('seed'); hosts.push({ metadata: generator.generateHostMetadata(), host_status: HostStatus.ERROR, @@ -45,9 +63,133 @@ export const mockHostResultList: (options?: { * returns a mocked API response for retrieving a single host metadata */ export const mockHostDetailsApiResult = (): HostInfo => { - const generator = new EndpointDocGenerator('seed'); return { metadata: generator.generateHostMetadata(), host_status: HostStatus.ERROR, }; }; + +/** + * Mock API handlers used by the Endpoint Host list. It also sets up a list of + * API handlers for Host details based on a list of Host results. + */ +const hostListApiPathHandlerMocks = ({ + hostsResults = mockHostResultList({ total: 3 }).hosts, + epmPackages = [generator.generateEpmPackage()], + endpointPackageConfigs = [], + policyResponse = generator.generatePolicyResponse(), +}: { + /** route handlers will be setup for each individual host in this array */ + hostsResults?: HostResultList['hosts']; + epmPackages?: GetPackagesResponse['response']; + endpointPackageConfigs?: GetPolicyListResponse['items']; + policyResponse?: HostPolicyResponse; +} = {}) => { + const apiHandlers = { + // endpoint package info + [INGEST_API_EPM_PACKAGES]: (): GetPackagesResponse => { + return { + response: epmPackages, + success: true, + }; + }, + + // host list + '/api/endpoint/metadata': (): HostResultList => { + return { + hosts: hostsResults, + request_page_size: 10, + request_page_index: 0, + total: hostsResults?.length || 0, + }; + }, + + // Do policies referenced in host list exist + // just returns 1 single agent config that includes all of the packageConfig IDs provided + [INGEST_API_AGENT_CONFIGS]: (): GetAgentConfigsResponse => { + const agentConfig = generator.generateAgentConfig(); + (agentConfig.package_configs as string[]).push( + ...endpointPackageConfigs.map((packageConfig) => packageConfig.id) + ); + return { + items: [agentConfig], + total: 10, + success: true, + perPage: 10, + page: 1, + }; + }, + + // Policy Response + '/api/endpoint/policy_response': (): GetHostPolicyResponse => { + return { policy_response: policyResponse }; + }, + + // List of Policies (package configs) for onboarding + [INGEST_API_PACKAGE_CONFIGS]: (): GetPolicyListResponse => { + return { + items: endpointPackageConfigs, + page: 1, + perPage: 10, + total: endpointPackageConfigs?.length, + success: true, + }; + }, + }; + + // Build a GET route handler for each host details based on the list of Hosts passed on input + if (hostsResults) { + hostsResults.forEach((host) => { + // @ts-ignore + apiHandlers[`/api/endpoint/metadata/${host.metadata.host.id}`] = () => host; + }); + } + + return apiHandlers; +}; + +/** + * Sets up mock impelementations in support of the Hosts list view + * + * @param mockedHttpService + * @param hostsResults + * @param pathHandlersOptions + */ +export const setHostListApiMockImplementation: ( + mockedHttpService: jest.Mocked, + apiResponses?: Parameters[0] +) => void = ( + mockedHttpService, + { hostsResults = mockHostResultList({ total: 3 }).hosts, ...pathHandlersOptions } = {} +) => { + const apiHandlers = hostListApiPathHandlerMocks({ ...pathHandlersOptions, hostsResults }); + + mockedHttpService.post + .mockImplementation(async (...args) => { + throw new Error(`un-expected call to http.post: ${args}`); + }) + // First time called, return list of hosts + .mockImplementationOnce(async () => { + return apiHandlers['/api/endpoint/metadata'](); + }); + + // If the hosts list results is zero, then mock the second call to `/metadata` to return + // empty list - indicating there are no hosts currently present on the system + if (!hostsResults.length) { + mockedHttpService.post.mockImplementationOnce(async () => { + return apiHandlers['/api/endpoint/metadata'](); + }); + } + + // Setup handling of GET requests + mockedHttpService.get.mockImplementation(async (...args) => { + const [path] = args; + if (typeof path === 'string') { + if (apiHandlers[path]) { + return apiHandlers[path](); + } + } + + throw new Error(`MOCK: api request does not have a mocked handler: ${path}`); + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 7f68baa4b85bd..e54f7df4d4f75 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -29,6 +29,7 @@ export const initialHostListState: Immutable = { policyItemsLoading: false, endpointPackageInfo: undefined, nonExistingPolicies: {}, + hostsExist: true, }; /* eslint-disable-next-line complexity */ @@ -125,6 +126,11 @@ export const hostListReducer: ImmutableReducer = ( ...state, endpointPackageInfo: action.payload, }; + } else if (action.type === 'serverReturnedHostExistValue') { + return { + ...state, + hostsExist: action.payload, + }; } else if (action.type === 'userChangedUrl') { const newState: Immutable = { ...state, @@ -181,6 +187,7 @@ export const hostListReducer: ImmutableReducer = ( error: undefined, detailsError: undefined, policyResponseError: undefined, + hostsExist: true, }; } return state; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index 6e0823a920413..ca006f21c29ac 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -203,3 +203,9 @@ export const policyResponseStatus: (state: Immutable) => string = cre export const nonExistingPolicies: ( state: Immutable ) => Immutable = (state) => state.nonExistingPolicies; + +/** + * Return boolean that indicates whether hosts exist + * @param state + */ +export const hostsExist: (state: Immutable) => boolean = (state) => state.hostsExist; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 582a59cfd7605..6c949e9700b9a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -52,6 +52,8 @@ export interface HostState { endpointPackageInfo?: GetPackagesResponse['response'][0]; /** tracks the list of policies IDs used in Host metadata that may no longer exist */ nonExistingPolicies: Record; + /** Tracks whether hosts exist and helps control if onboarding should be visible */ + hostsExist: boolean; } /** diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx index 4cdfaad69eb72..3a1dd180405e0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx @@ -140,20 +140,16 @@ export const PolicyResponse = memo( responseActions: Immutable; responseAttentionCount: Map; }) => { + const generateId = useMemo(() => htmlIdGenerator(), []); + return ( <> {Object.entries(responseConfig).map(([key, val]) => { const attentionCount = responseAttentionCount.get(key); return ( htmlIdGenerator()(), []) - } - key={ - /* eslint-disable-next-line react-hooks/rules-of-hooks */ - useMemo(() => htmlIdGenerator()(), []) - } + id={generateId(`id_${key}`)} + key={generateId(`key_${key}`)} data-test-subj="hostDetailsPolicyResponseConfigAccordion" buttonContent={ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 9d49c8705affe..3e00a5cc33db1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -8,18 +8,22 @@ import React from 'react'; import * as reactTestingLibrary from '@testing-library/react'; import { HostList } from './index'; -import { mockHostDetailsApiResult, mockHostResultList } from '../store/mock_host_result_list'; -import { mockPolicyResultList } from '../../policy/store/policy_list/mock_policy_result_list'; +import { + mockHostDetailsApiResult, + mockHostResultList, + setHostListApiMockImplementation, +} from '../store/mock_host_result_list'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { HostInfo, + HostPolicyResponse, HostPolicyResponseActionStatus, HostPolicyResponseAppliedAction, HostStatus, } from '../../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; -import { AppAction } from '../../../../common/store/actions'; import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants'; +import { mockPolicyResultList } from '../../policy/store/policy_list/test_mock_utils'; jest.mock('../../../../common/components/link_to'); @@ -35,6 +39,9 @@ describe('when on the hosts page', () => { const mockedContext = createAppRootMockRenderer(); ({ history, store, coreStart, middlewareSpy } = mockedContext); render = () => mockedContext.render(); + reactTestingLibrary.act(() => { + history.push('/hosts'); + }); }); it('should NOT display timeline', async () => { @@ -43,35 +50,29 @@ describe('when on the hosts page', () => { expect(timelineFlyout).toBeNull(); }); - it('should show the empty state when there are no hosts or polices', async () => { - const renderResult = render(); - // Initially, there are no hosts or policies, so we prompt to add policies first. - const table = await renderResult.findByTestId('emptyPolicyTable'); - expect(table).not.toBeNull(); - }); - - describe('when there are policies, but no hosts', () => { + describe('when there are no hosts or polices', () => { beforeEach(() => { - reactTestingLibrary.act(() => { - const hostListData = mockHostResultList({ total: 0 }); - coreStart.http.get.mockReturnValue(Promise.resolve(hostListData)); - const hostAction: AppAction = { - type: 'serverReturnedHostList', - payload: hostListData, - }; - store.dispatch(hostAction); + setHostListApiMockImplementation(coreStart.http, { + hostsResults: [], + }); + }); - jest.clearAllMocks(); + it('should show the empty state when there are no hosts or polices', async () => { + const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); + // Initially, there are no hosts or policies, so we prompt to add policies first. + const table = await renderResult.findByTestId('emptyPolicyTable'); + expect(table).not.toBeNull(); + }); + }); - const policyListData = mockPolicyResultList({ total: 3 }); - coreStart.http.get.mockReturnValue(Promise.resolve(policyListData)); - const policyAction: AppAction = { - type: 'serverReturnedPoliciesForOnboarding', - payload: { - policyItems: policyListData.items, - }, - }; - store.dispatch(policyAction); + describe('when there are policies, but no hosts', () => { + beforeEach(async () => { + setHostListApiMockImplementation(coreStart.http, { + hostsResults: [], + endpointPackageConfigs: mockPolicyResultList({ total: 3 }).items, }); }); afterEach(() => { @@ -80,18 +81,27 @@ describe('when on the hosts page', () => { it('should show the no hosts empty state', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); const emptyHostsTable = await renderResult.findByTestId('emptyHostsTable'); expect(emptyHostsTable).not.toBeNull(); }); it('should display the onboarding steps', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); const onboardingSteps = await renderResult.findByTestId('onboardingSteps'); expect(onboardingSteps).not.toBeNull(); }); it('should show policy selection', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); const onboardingPolicySelect = await renderResult.findByTestId('onboardingPolicySelect'); expect(onboardingPolicySelect).not.toBeNull(); }); @@ -112,39 +122,54 @@ describe('when on the hosts page', () => { let firstPolicyID: string; beforeEach(() => { reactTestingLibrary.act(() => { - const hostListData = mockHostResultList({ total: 4 }); - firstPolicyID = hostListData.hosts[0].metadata.Endpoint.policy.applied.id; + const hostListData = mockHostResultList({ total: 4 }).hosts; + + firstPolicyID = hostListData[0].metadata.Endpoint.policy.applied.id; + [HostStatus.ERROR, HostStatus.ONLINE, HostStatus.OFFLINE, HostStatus.UNENROLLING].forEach( (status, index) => { - hostListData.hosts[index] = { - metadata: hostListData.hosts[index].metadata, + hostListData[index] = { + metadata: hostListData[index].metadata, host_status: status, }; } ); - hostListData.hosts.forEach((item, index) => { + hostListData.forEach((item, index) => { generatedPolicyStatuses[index] = item.metadata.Endpoint.policy.applied.status; }); - const action: AppAction = { - type: 'serverReturnedHostList', - payload: hostListData, - }; - store.dispatch(action); + + // Make sure that the first policy id in the host result is not set as non-existent + const ingestPackageConfigs = mockPolicyResultList({ total: 1 }).items; + ingestPackageConfigs[0].id = firstPolicyID; + + setHostListApiMockImplementation(coreStart.http, { + hostsResults: hostListData, + endpointPackageConfigs: ingestPackageConfigs, + }); }); }); it('should display rows in the table', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const rows = await renderResult.findAllByRole('row'); expect(rows).toHaveLength(5); }); it('should show total', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const total = await renderResult.findByTestId('hostListTableTotal'); expect(total.textContent).toEqual('4 Hosts'); }); it('should display correct status', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const hostStatuses = await renderResult.findAllByTestId('rowHostStatus'); expect(hostStatuses[0].textContent).toEqual('Error'); @@ -168,6 +193,9 @@ describe('when on the hosts page', () => { it('should display correct policy status', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const policyStatuses = await renderResult.findAllByTestId('rowPolicyStatus'); policyStatuses.forEach((status, index) => { @@ -184,6 +212,9 @@ describe('when on the hosts page', () => { it('should display policy name as a link', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const firstPolicyName = (await renderResult.findAllByTestId('policyNameCellLink'))[0]; expect(firstPolicyName).not.toBeNull(); expect(firstPolicyName.getAttribute('href')).toContain(`policy/${firstPolicyID}`); @@ -192,17 +223,10 @@ describe('when on the hosts page', () => { describe('when the user clicks the first hostname in the table', () => { let renderResult: reactTestingLibrary.RenderResult; beforeEach(async () => { - const hostDetailsApiResponse = mockHostDetailsApiResult(); - - coreStart.http.get.mockReturnValue(Promise.resolve(hostDetailsApiResponse)); - reactTestingLibrary.act(() => { - store.dispatch({ - type: 'serverReturnedHostDetails', - payload: hostDetailsApiResponse, - }); - }); - renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const hostNameLinks = await renderResult.findAllByTestId('hostnameCellLink'); if (hostNameLinks.length) { reactTestingLibrary.fireEvent.click(hostNameLinks[0]); @@ -221,9 +245,11 @@ describe('when on the hosts page', () => { describe('when there is a selected host in the url', () => { let hostDetails: HostInfo; let agentId: string; - const dispatchServerReturnedHostPolicyResponse = ( + let renderAndWaitForData: () => Promise>; + + const createPolicyResponse = ( overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success - ) => { + ): HostPolicyResponse => { const policyResponse = docGenerator.generatePolicyResponse(); const malwareResponseConfigurations = policyResponse.Endpoint.policy.applied.response.configurations.malware; @@ -269,21 +295,28 @@ describe('when on the hosts page', () => { policyResponse.Endpoint.policy.applied.actions.push(unknownAction); malwareResponseConfigurations.concerned_actions.push(unknownAction.name); + return policyResponse; + }; + + const dispatchServerReturnedHostPolicyResponse = ( + overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success + ) => { reactTestingLibrary.act(() => { store.dispatch({ type: 'serverReturnedHostPolicyResponse', payload: { - policy_response: policyResponse, + policy_response: createPolicyResponse(overallStatus), }, }); }); }; - beforeEach(() => { + beforeEach(async () => { const { host_status, metadata: { host, ...details }, } = mockHostDetailsApiResult(); + hostDetails = { host_status, metadata: { @@ -297,34 +330,37 @@ describe('when on the hosts page', () => { agentId = hostDetails.metadata.elastic.agent.id; - coreStart.http.get.mockReturnValue(Promise.resolve(hostDetails)); + const policy = docGenerator.generatePolicyPackageConfig(); + policy.id = hostDetails.metadata.Endpoint.policy.applied.id; - reactTestingLibrary.act(() => { - history.push({ - ...history.location, - search: '?selected_host=1', - }); + setHostListApiMockImplementation(coreStart.http, { + hostsResults: [hostDetails], + endpointPackageConfigs: [policy], }); + reactTestingLibrary.act(() => { - store.dispatch({ - type: 'serverReturnedHostDetails', - payload: hostDetails, - }); + history.push('/hosts?selected_host=1'); }); + + renderAndWaitForData = async () => { + const renderResult = render(); + await middlewareSpy.waitForAction('serverReturnedHostDetails'); + return renderResult; + }; }); afterEach(() => { jest.clearAllMocks(); }); - it('should show the flyout', () => { - const renderResult = render(); + it('should show the flyout', async () => { + const renderResult = await renderAndWaitForData(); return renderResult.findByTestId('hostDetailsFlyout').then((flyout) => { expect(flyout).not.toBeNull(); }); }); it('should display policy name value as a link', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue'); expect(policyDetailsLink).not.toBeNull(); expect(policyDetailsLink.getAttribute('href')).toEqual( @@ -333,10 +369,7 @@ describe('when on the hosts page', () => { }); it('should update the URL when policy name link is clicked', async () => { - const policyItem = mockPolicyResultList({ total: 1 }).items[0]; - coreStart.http.get.mockReturnValue(Promise.resolve({ item: policyItem })); - - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue'); const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { @@ -349,7 +382,7 @@ describe('when on the hosts page', () => { }); it('should display policy status value as a link', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusLink).not.toBeNull(); expect(policyStatusLink.getAttribute('href')).toEqual( @@ -358,7 +391,7 @@ describe('when on the hosts page', () => { }); it('should update the URL when policy status link is clicked', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { @@ -371,7 +404,7 @@ describe('when on the hosts page', () => { }); it('should display Success overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.success); }); @@ -385,7 +418,7 @@ describe('when on the hosts page', () => { }); it('should display Warning overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.warning); }); @@ -399,7 +432,7 @@ describe('when on the hosts page', () => { }); it('should display Failed overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.failure); }); @@ -413,7 +446,7 @@ describe('when on the hosts page', () => { }); it('should display Unknown overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse('' as HostPolicyResponseActionStatus); }); @@ -428,7 +461,7 @@ describe('when on the hosts page', () => { it('should include the link to reassignment in Ingest', async () => { coreStart.application.getUrlForApp.mockReturnValue('/app/ingestManager'); - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const linkToReassign = await renderResult.findByTestId('hostDetailsLinkToIngest'); expect(linkToReassign).not.toBeNull(); expect(linkToReassign.textContent).toEqual('Reassign Configuration'); @@ -440,7 +473,7 @@ describe('when on the hosts page', () => { describe('when link to reassignment in Ingest is clicked', () => { beforeEach(async () => { coreStart.application.getUrlForApp.mockReturnValue('/app/ingestManager'); - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const linkToReassign = await renderResult.findByTestId('hostDetailsLinkToIngest'); reactTestingLibrary.act(() => { reactTestingLibrary.fireEvent.click(linkToReassign); @@ -461,13 +494,14 @@ describe('when on the hosts page', () => { } throw new Error(`POST to '${requestOptions.path}' does not have a mock response!`); }); - renderResult = render(); + renderResult = await renderAndWaitForData(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { reactTestingLibrary.fireEvent.click(policyStatusLink); }); await userChangedUrlChecker; + await middlewareSpy.waitForAction('serverReturnedHostPolicyResponse'); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 2692f7791b7c0..58442ab417b60 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -89,6 +89,7 @@ export const HostList = () => { selectedPolicyId, policyItemsLoading, endpointPackageVersion, + hostsExist, } = useHostSelector(selector); const { formatUrl, search } = useFormatUrl(SecurityPageName.administration); @@ -329,7 +330,7 @@ export const HostList = () => { }, [formatUrl, queryParams, search]); const renderTableOrEmptyState = useMemo(() => { - if (!loading && listData && listData.length > 0) { + if (hostsExist) { return ( { error={listError?.message} pagination={paginationSetup} onChange={onTableChange} + loading={loading} /> ); } else if (!policyItemsLoading && policyItems && policyItems.length > 0) { @@ -356,19 +358,20 @@ export const HostList = () => { ); } }, [ - listData, + loading, + hostsExist, + policyItemsLoading, policyItems, + listData, columns, - loading, + listError?.message, paginationSetup, onTableChange, - listError?.message, - handleCreatePolicyClick, handleDeployEndpointsClick, - handleSelectableOnChange, selectedPolicyId, + handleSelectableOnChange, selectionOptions, - policyItemsLoading, + handleCreatePolicyClick, ]); return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts index 8203aae244f24..6ee6a4232f7cf 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts @@ -143,6 +143,7 @@ describe('policy list store concerns', () => { isLoading: false, isDeleting: false, deleteStatus: undefined, + endpointPackageInfo: undefined, pageIndex: 0, pageSize: 10, total: 0, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts index b4e1da4e43da3..6fe555113617d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts @@ -29,6 +29,7 @@ export const policyListMiddlewareFactory: ImmutableMiddlewareFactory GetPolicyListResponse = (options = {}) => { - const { - total = 1, - request_page_size: requestPageSize = 10, - request_page_index: requestPageIndex = 0, - } = options; - - // Skip any that are before the page we're on - const numberToSkip = requestPageSize * requestPageIndex; - - // total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0 - const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0); - - const policies = []; - for (let index = 0; index < actualCountToReturn; index++) { - const generator = new EndpointDocGenerator('seed'); - policies.push(generator.generatePolicyPackageConfig()); - } - const mock: GetPolicyListResponse = { - items: policies, - total, - page: requestPageIndex, - perPage: requestPageSize, - success: true, - }; - return mock; -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts index 7daa500ef1884..83cd8558306a0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts @@ -12,7 +12,7 @@ import { } from './ingest'; import { httpServiceMock } from '../../../../../../../../../../src/core/public/mocks'; import { PACKAGE_CONFIG_SAVED_OBJECT_TYPE } from '../../../../../../../../ingest_manager/common'; -import { apiPathMockResponseProviders } from '../test_mock_utils'; +import { policyListApiPathHandlers } from '../test_mock_utils'; describe('ingest service', () => { let http: ReturnType; @@ -61,7 +61,9 @@ describe('ingest service', () => { describe('sendGetEndpointSecurityPackage()', () => { it('should query EPM with category=security', async () => { - http.get.mockReturnValue(apiPathMockResponseProviders[INGEST_API_EPM_PACKAGES]()); + http.get.mockReturnValue( + Promise.resolve(policyListApiPathHandlers()[INGEST_API_EPM_PACKAGES]()) + ); await sendGetEndpointSecurityPackage(http); expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/epm/packages', { query: { category: 'security' }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts index c6e6146f4d5e4..266faf9eae32c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts @@ -20,7 +20,7 @@ import { NewPolicyData } from '../../../../../../../common/endpoint/types'; const INGEST_API_ROOT = `/api/ingest_manager`; export const INGEST_API_PACKAGE_CONFIGS = `${INGEST_API_ROOT}/package_configs`; -const INGEST_API_AGENT_CONFIGS = `${INGEST_API_ROOT}/agent_configs`; +export const INGEST_API_AGENT_CONFIGS = `${INGEST_API_ROOT}/agent_configs`; const INGEST_API_FLEET = `${INGEST_API_ROOT}/fleet`; const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`; export const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts index b5c67cc2c2014..3c9d5fde9b826 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts @@ -5,122 +5,84 @@ */ import { HttpStart } from 'kibana/public'; -import { INGEST_API_PACKAGE_CONFIGS, INGEST_API_EPM_PACKAGES } from './services/ingest'; +import { INGEST_API_EPM_PACKAGES, INGEST_API_PACKAGE_CONFIGS } from './services/ingest'; import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; import { GetPolicyListResponse } from '../../types'; -import { - KibanaAssetReference, - EsAssetReference, - GetPackagesResponse, - InstallationStatus, -} from '../../../../../../../ingest_manager/common'; +import { GetPackagesResponse } from '../../../../../../../ingest_manager/common'; const generator = new EndpointDocGenerator('policy-list'); -/** - * a list of API paths response mock providers - */ -export const apiPathMockResponseProviders = { - [INGEST_API_EPM_PACKAGES]: () => - Promise.resolve({ - response: [ - { - name: 'endpoint', - title: 'Elastic Endpoint', - version: '0.5.0', - description: 'This is the Elastic Endpoint package.', - type: 'solution', - download: '/epr/endpoint/endpoint-0.5.0.tar.gz', - path: '/package/endpoint/0.5.0', - icons: [ - { - src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg', - size: '16x16', - type: 'image/svg+xml', - }, - ], - status: 'installed' as InstallationStatus, - savedObject: { - type: 'epm-packages', - id: 'endpoint', - attributes: { - installed_kibana: [ - { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' }, - { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' }, - ] as KibanaAssetReference[], - installed_es: [ - { id: 'logs-endpoint.alerts', type: 'index_template' }, - { id: 'events-endpoint', type: 'index_template' }, - { id: 'logs-endpoint.events.file', type: 'index_template' }, - { id: 'logs-endpoint.events.library', type: 'index_template' }, - { id: 'metrics-endpoint.metadata', type: 'index_template' }, - { id: 'metrics-endpoint.metadata_mirror', type: 'index_template' }, - { id: 'logs-endpoint.events.network', type: 'index_template' }, - { id: 'metrics-endpoint.policy', type: 'index_template' }, - { id: 'logs-endpoint.events.process', type: 'index_template' }, - { id: 'logs-endpoint.events.registry', type: 'index_template' }, - { id: 'logs-endpoint.events.security', type: 'index_template' }, - { id: 'metrics-endpoint.telemetry', type: 'index_template' }, - ] as EsAssetReference[], - es_index_patterns: { - alerts: 'logs-endpoint.alerts-*', - events: 'events-endpoint-*', - file: 'logs-endpoint.events.file-*', - library: 'logs-endpoint.events.library-*', - metadata: 'metrics-endpoint.metadata-*', - metadata_mirror: 'metrics-endpoint.metadata_mirror-*', - network: 'logs-endpoint.events.network-*', - policy: 'metrics-endpoint.policy-*', - process: 'logs-endpoint.events.process-*', - registry: 'logs-endpoint.events.registry-*', - security: 'logs-endpoint.events.security-*', - telemetry: 'metrics-endpoint.telemetry-*', - }, - name: 'endpoint', - version: '0.5.0', - internal: false, - removable: false, - }, - references: [], - updated_at: '2020-06-24T14:41:23.098Z', - version: 'Wzc0LDFd', - }, - }, - ], - success: true, - }), -}; - /** * It sets the mock implementation on the necessary http methods to support the policy list view * @param mockedHttpService - * @param responseItems + * @param totalPolicies */ export const setPolicyListApiMockImplementation = ( mockedHttpService: jest.Mocked, - responseItems: GetPolicyListResponse['items'] = [generator.generatePolicyPackageConfig()] + totalPolicies: number = 1 ): void => { - mockedHttpService.get.mockImplementation((...args) => { + const policyApiHandlers = policyListApiPathHandlers(totalPolicies); + + mockedHttpService.get.mockImplementation(async (...args) => { const [path] = args; if (typeof path === 'string') { - if (path === INGEST_API_PACKAGE_CONFIGS) { - return Promise.resolve({ - items: responseItems, - total: 10, - page: 1, - perPage: 10, - success: true, - }); - } - - if (apiPathMockResponseProviders[path]) { - return apiPathMockResponseProviders[path](); + if (policyApiHandlers[path]) { + return policyApiHandlers[path](); } } return Promise.reject(new Error(`MOCK: unknown policy list api: ${path}`)); }); }; + +/** + * Returns the response body for a call to get the list of Policies + * @param options + */ +export const mockPolicyResultList: (options?: { + total?: number; + request_page_size?: number; + request_page_index?: number; +}) => GetPolicyListResponse = (options = {}) => { + const { + total = 1, + request_page_size: requestPageSize = 10, + request_page_index: requestPageIndex = 0, + } = options; + + // Skip any that are before the page we're on + const numberToSkip = requestPageSize * requestPageIndex; + + // total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0 + const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0); + + const policies = []; + for (let index = 0; index < actualCountToReturn; index++) { + policies.push(generator.generatePolicyPackageConfig()); + } + const mock: GetPolicyListResponse = { + items: policies, + total, + page: requestPageIndex, + perPage: requestPageSize, + success: true, + }; + return mock; +}; + +/** + * Returns an object comprised of the API path as the key along with a function that + * returns that API's result value + */ +export const policyListApiPathHandlers = (totalPolicies: number = 1) => { + return { + [INGEST_API_PACKAGE_CONFIGS]: () => { + return mockPolicyResultList({ total: totalPolicies }); + }, + [INGEST_API_EPM_PACKAGES]: (): GetPackagesResponse => { + return { + response: [generator.generateEpmPackage()], + success: true, + }; + }, + }; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index 03ab32dcb2b66..c81ffb0060c88 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -11,7 +11,7 @@ import { PolicyDetails } from './policy_details'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { getPolicyDetailPath, getHostListPath } from '../../../common/routing'; -import { apiPathMockResponseProviders } from '../store/policy_list/test_mock_utils'; +import { policyListApiPathHandlers } from '../store/policy_list/test_mock_utils'; jest.mock('../../../../common/components/link_to'); @@ -80,6 +80,8 @@ describe('Policy Details', () => { policyPackageConfig = generator.generatePolicyPackageConfig(); policyPackageConfig.id = '1'; + const policyListApiHandlers = policyListApiPathHandlers(); + http.get.mockImplementation((...args) => { const [path] = args; if (typeof path === 'string') { @@ -103,9 +105,9 @@ describe('Policy Details', () => { // Get package data // Used in tests that route back to the list - if (apiPathMockResponseProviders[path]) { + if (policyListApiHandlers[path]) { asyncActions = asyncActions.then(async () => sleep()); - return apiPathMockResponseProviders[path](); + return Promise.resolve(policyListApiHandlers[path]()); } } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx index 9ceade5d0264c..76077831c670b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx @@ -27,6 +27,7 @@ export const EventsCheckbox = React.memo(function ({ const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); const selected = getter(policyDetailsConfig); const dispatch = useDispatch<(action: PolicyDetailsAction) => void>(); + const checkboxId = useMemo(() => htmlIdGenerator()(), []); const handleCheckboxChange = useCallback( (event: React.ChangeEvent) => { @@ -42,7 +43,7 @@ export const EventsCheckbox = React.memo(function ({ return ( htmlIdGenerator()(), [])} + id={checkboxId} label={name} checked={selected} onChange={handleCheckboxChange} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index 84d4bf5355cd9..1698f5bc3fd0c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -41,6 +41,7 @@ const protection = 'malware'; const ProtectionRadio = React.memo(({ id, label }: { id: ProtectionModes; label: string }) => { const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); const dispatch = useDispatch(); + const radioButtonId = useMemo(() => htmlIdGenerator()(), []); // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; @@ -66,7 +67,7 @@ const ProtectionRadio = React.memo(({ id, label }: { id: ProtectionModes; label: htmlIdGenerator()(), [])} + id={radioButtonId} checked={selected === id} onChange={handleRadioChange} disabled={selected === ProtectionModes.off} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx index e35c97698f5cb..97eaceff91e9c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx @@ -5,12 +5,9 @@ */ import React from 'react'; -import * as reactTestingLibrary from '@testing-library/react'; - import { PolicyList } from './index'; -import { mockPolicyResultList } from '../store/policy_list/mock_policy_result_list'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; -import { AppAction } from '../../../../common/store/actions'; +import { setPolicyListApiMockImplementation } from '../store/policy_list/test_mock_utils'; jest.mock('../../../../common/components/link_to'); @@ -18,11 +15,12 @@ jest.mock('../../../../common/components/link_to'); describe.skip('when on the policies page', () => { let render: () => ReturnType; let history: AppContextTestRender['history']; - let store: AppContextTestRender['store']; + let coreStart: AppContextTestRender['coreStart']; + let middlewareSpy: AppContextTestRender['middlewareSpy']; beforeEach(() => { const mockedContext = createAppRootMockRenderer(); - ({ history, store } = mockedContext); + ({ history, coreStart, middlewareSpy } = mockedContext); render = () => mockedContext.render(); }); @@ -46,34 +44,30 @@ describe.skip('when on the policies page', () => { describe('when list data loads', () => { let firstPolicyID: string; - beforeEach(() => { - reactTestingLibrary.act(() => { - history.push('/policy'); - reactTestingLibrary.act(() => { - const policyListData = mockPolicyResultList({ total: 3 }); - firstPolicyID = policyListData.items[0].id; - const action: AppAction = { - type: 'serverReturnedPolicyListData', - payload: { - policyItems: policyListData.items, - total: policyListData.total, - pageSize: policyListData.perPage, - pageIndex: policyListData.page, - }, - }; - store.dispatch(action); - }); - }); + const renderList = async () => { + const renderResult = render(); + history.push('/policy'); + await Promise.all([ + middlewareSpy + .waitForAction('serverReturnedPolicyListData') + .then((action) => (firstPolicyID = action.payload.policyItems[0].id)), + // middlewareSpy.waitForAction('serverReturnedAgentConfigListData'), + ]); + return renderResult; + }; + + beforeEach(async () => { + setPolicyListApiMockImplementation(coreStart.http, 3); }); it('should display rows in the table', async () => { - const renderResult = render(); + const renderResult = await renderList(); const rows = await renderResult.findAllByRole('row'); expect(rows).toHaveLength(4); }); it('should display policy name value as a link', async () => { - const renderResult = render(); + const renderResult = await renderList(); const policyNameLink = (await renderResult.findAllByTestId('policyNameLink'))[0]; expect(policyNameLink).not.toBeNull(); expect(policyNameLink.getAttribute('href')).toContain(`policy/${firstPolicyID}`); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 246dbeb39886f..39b77d259add1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -377,6 +377,22 @@ export const PolicyList = React.memo(() => { [services.application, handleDeleteOnClick, formatUrl, search] ); + const bodyContent = useMemo(() => { + return policyItems && policyItems.length > 0 ? ( + + ) : ( + + ); + }, [policyItems, loading, columns, handleCreatePolicyClick, handleTableChange, paginationSetup]); + return ( <> {showDelete && ( @@ -449,32 +465,7 @@ export const PolicyList = React.memo(() => { )} - {useMemo(() => { - return ( - <> - {policyItems && policyItems.length > 0 ? ( - - ) : ( - - )} - - ); - }, [ - policyItems, - loading, - columns, - handleCreatePolicyClick, - handleTableChange, - paginationSetup, - ])} + {bodyContent} diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap index 30f7df940fa99..b72b9e5c73977 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap @@ -6,7 +6,7 @@ exports[`IndexPatternsMissingPrompt renders correctly against snapshot 1`] = ` Configure index patterns @@ -28,7 +28,7 @@ exports[`IndexPatternsMissingPrompt renders correctly against snapshot 1`] = ` beats , "defaultIndex": diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx index 62ffad515f424..b5595daa9cf47 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx @@ -13,7 +13,7 @@ import * as i18n from './translations'; export const IndexPatternsMissingPromptComponent = () => { const { docLinks } = useKibana().services; - const kibanaBasePath = `${useBasePath()}/app/kibana`; + const kibanaBasePath = `${useBasePath()}/app`; return ( { values={{ defaultIndex: ( @@ -61,7 +61,7 @@ export const IndexPatternsMissingPromptComponent = () => { } actions={ ( const { fetchAllTimeline, timelines, loading } = useGetAllTimeline(); const timelineType = TimelineType.default; - const { templateTimelineType, timelineStatus } = useTimelineStatus({ timelineType }); + const { timelineStatus } = useTimelineStatus({ timelineType }); useEffect(() => { fetchAllTimeline({ pageInfo: { @@ -100,9 +100,8 @@ const StatefulRecentTimelinesComponent = React.memo( onlyUserFavorite: filterBy === 'favorites', status: timelineStatus, timelineType, - templateTimelineType, }); - }, [fetchAllTimeline, filterBy, timelineStatus, timelineType, templateTimelineType]); + }, [fetchAllTimeline, filterBy, timelineStatus, timelineType]); return ( <> diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index d2ddaae47d1e3..188b8979f613c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -126,7 +126,6 @@ export const StatefulOpenTimelineComponent = React.memo( }); const { timelineStatus, - templateTimelineType, templateTimelineFilter, installPrepackagedTimelines, } = useTimelineStatus({ @@ -147,7 +146,6 @@ export const StatefulOpenTimelineComponent = React.memo( }, onlyUserFavorite: onlyFavorites, timelineType, - templateTimelineType, status: timelineStatus, }); }, [ @@ -159,7 +157,6 @@ export const StatefulOpenTimelineComponent = React.memo( sortDirection, timelineType, timelineStatus, - templateTimelineType, onlyFavorites, ]); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx index 6bea5a7b7635e..b64ca0ccc0b35 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx @@ -93,7 +93,6 @@ describe('SelectableTimeline', () => { status: null, onlyUserFavorite: false, timelineType: TimelineType.default, - templateTimelineType: null, }; beforeAll(() => { mount(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx index 7ecbc9a53cb21..ff103aa7d2c5a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx @@ -263,7 +263,6 @@ const SelectableTimelineComponent: React.FC = ({ onlyUserFavorite: onlyFavorites, status: null, timelineType, - templateTimelineType: null, }); }, [fetchAllTimeline, onlyFavorites, pageSize, searchTimelineValue, timelineType]); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts index cd03e43938b44..b06f0ed4f25f7 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts @@ -13,7 +13,6 @@ export const allTimelinesQuery = gql` $sort: SortTimeline $onlyUserFavorite: Boolean $timelineType: TimelineType - $templateTimelineType: TemplateTimelineType $status: TimelineStatus ) { getAllTimeline( @@ -22,7 +21,6 @@ export const allTimelinesQuery = gql` sort: $sort onlyUserFavorite: $onlyUserFavorite timelineType: $timelineType - templateTimelineType: $templateTimelineType status: $status ) { totalCount diff --git a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx index 3cf33048007e3..59d7fd6945637 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx @@ -26,7 +26,6 @@ import { TimelineType, TimelineTypeLiteralWithNull, TimelineStatusLiteralWithNull, - TemplateTimelineTypeLiteralWithNull, } from '../../../../common/types/timeline'; export interface AllTimelinesArgs { @@ -55,7 +54,6 @@ export interface AllTimelinesVariables { sort: SortTimeline; status: TimelineStatusLiteralWithNull; timelineType: TimelineTypeLiteralWithNull; - templateTimelineType: TemplateTimelineTypeLiteralWithNull; } export const ALL_TIMELINE_QUERY_ID = 'FETCH_ALL_TIMELINES'; @@ -121,7 +119,6 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { sort, status, timelineType, - templateTimelineType, }: AllTimelinesVariables) => { let didCancel = false; const abortCtrl = new AbortController(); @@ -138,7 +135,6 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { sort, status, timelineType, - templateTimelineType, }; const response = await apolloClient.query< GetAllTimeline.Query, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md new file mode 100644 index 0000000000000..1c0692db344c4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md @@ -0,0 +1,216 @@ +# Resolver Backend + +This readme will describe the backend implementation for resolver. + +## Ancestry Array + +The ancestry array is an array of entity_ids. This array is included with each event sent by the elastic endpoint +and defines the ancestors of a particular process. The array is formatted such that [0] of the array contains the direct +parent of the process. [1] of the array contains the grandparent of the process. For example if Process A spawned process +B which spawned process C. Process C's array would be [B,A]. + +The presence of the ancestry array makes querying ancestors and children for a process more efficient. + +## Ancestry Array Limit + +The ancestry array is currently limited to 20 values. The exact limit should not be relied on though. + +## Ancestors + +To query for ancestors of a process leveraging the ancestry array, we first retrieve the lifecycle events for the event_id +passed in. Once we have the origin node we can check to see if the document has the `process.Ext.ancestry` array. If +it does we can perform a search for the values in the array. This will retrieve all the ancestors for the process of interest +up to the limit of the ancestry array. Since the array is capped at 20, if the request is asking for more than 20 +ancestors we will have to examine the most distant ancestor that has been retrieved and use its ancestry array to retrieve +the next set of results to fulfill the request. + +### Pagination + +After the backend gathers the results for an ancestry query, it will set a pagination cursor depending on the results from ES. + +If the number of ancestors we have gathered is equal to the size in the request we don't know if ES has more results or not. So we will set `nextAncestor` to the entity_id of the most distant ancestor retrieved. + +If the request asked for 10 and we only found 8 from ES, we know for sure that there aren't anymore results. In this case we will set `nextAncestor` to `null`. + +### Code + +The code for handling the ancestor logic is in [here](../utils/ancestry_query_handler.ts) + +### Ancestors Multiple Queries Example + +![alt text](./resolver_tree_ancestry.png 'Retrieve ancestors') + +For this example let's assume that the _ancestry array limit_ is 2. The process of interest is A (the entity_id of a node is the character in the circle). Process A has an ancestry array of `[3,2]`, its parent has an ancestry array of `[2,1]` etc. Here is the execution of a request for 3 ancestors for entity_id A. + +**Request:** `GET /resolver/A/ancestry?ancestors=3` + +1. Retrieve lifecycle events for entity_id `A` +2. Retrieve `A`'s start event's ancestry array + 1. In the event that the node of interest does not have an ancestry array, the entity id of it's parent will be used, essentially an ancestry array of length 1, [3] in the example here +3. `A`'s ancestry array is `[3,2]`, query for the lifecycle events for processes with `entity_id` 3 or 2 +4. Check to see if we have retrieved enough ancestors to fulfill the request (we have not, we only received 2 nodes of the 3 that were requested) +5. We haven't so use the most distant ancestor in our result set (process 2) +6. Use process 2's ancestry array to query for the next set of results to fulfill the request +7. Process 2's ancestry array is `[1]` so repeat the process in steps 3-4 and retrieve process with entity_id 1. This fulfills the request so we can return the results for the lifecycle events of A, 3, 2, and 1. + +If process 2 had an ancestry array of `[1,0]` we know that we only need 1 more process to fulfill the request so we can truncate the array to `[1]` instead of searching for all the entries in the array. + +More generically: In the event where our request stops at the x (non-final) position in an ancestry array, we won't search all items in the array, just those up to the x position. The next-cursor will be set to the last ancestor received since there might be more data. + +The `nextAncestor` cursor will be set to `1` in this scenario because we retrieved all 3 ancestors from ES but we don't know if ES has anymore. + +## Descendants + +We can also leverage the ancestry array to query for the descendants of a process. The basic query for the descendants of a process is: _find all processes where their ancestry array contains a particular entity_id_. The results of this query will be sorted in ascending order by the timestamp field. I will try to outline a couple different scenarios for retrieving descendants using the ancestry array below. + +### Start events vs all lifecycle events + +There are two parts to querying for descendant process nodes. When a request comes in for 7 process nodes we need to communicate to ES that we want all of the lifecycle nodes for 7 processes. We could use a query that retrieves all lifecycle events (start, end, etc) but the issue with this is that we need to indicate a `size` in our ES query. If we set the `size` to 7, we will only get 7 lifecycle events. These events could be start, end, or already_running events. It doesn't guarantee that we get all of the lifecycle events for 7 process nodes. + +Instead we can first query for 7 start events, which guarantees that we will have 7 unique process descendants and then we can gather all those entity_ids and do another query for all the lifecycle events for those 7 processes. The downside here is that you have to do two queries to retrieve all the lifecycle events. Optimizations can be made for the first query for the start events by reducing the `_source` that ES returns to only include the `entity_id` and `ancestry`. This will reduce the amount of data that ES has to send back and speed up the query. + +### Scenario Background + +In the scenarios below let's assume the _ancestry array limit_ is 2. The times next to the nodes are the time the node was spawned. The value in red indicates that the process terminated at the time in red. + +Let's also ignore the fact that retrieving the lifecycle events for a descendant actually takes two queries. Let's assume that it's taken care of, and when we say "query for lifecycle events" we get all the lifecycle events back for the descendants using the algorithm described in the [previous section](#start-events-vs-all-lifecycle-events) + +### Simple Scenario + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> E -> F -> G -> H. + +![alt text](./resolver_tree_children_simple.png 'Descendants Simple Scenario') + +**Request:** `GET /resolver/A/children?children=6` + +For this scenario we will retrieve all the lifecycle events for 6 descendants of the process with entity_id `A`. As shown in the diagram above ES has 6 descendants for A so the response to this request will be: `[B, C, E, F, G, H]` because the results are sorted in ascending ordering based on the `timestamp` field which is when the process was started. + +### Looping Scenario + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> E -> F -> G -> J -> K -> H. + +![alt text](./resolver_tree_children_loop.png 'Descendants Looping Scenario') + +**Request:** `GET /resolver/A/children?children=9` + +In this scenario the request is for more descendants than can be retrieved using a single querying with the entity_id `A`. This is because the ancestry array for the descendants in the red section do not have `A` in their ancestry array. So when we query for all process nodes that have `A` in their ancestry array we won't receive D, J, or K. + +Like in the previous scenario, for the first query we will receive `[B, C, E, F, G, H]`. What we want to do next is use a subset of that response that will get us 3 more descendants to fulfill the request for a total of 9. + +We _could_ use `B` and `G` to do this (mostly `B`) but the problem is that when we query for descendants that have `B` or `G` in their ancestry array we will get back some duplicates that we have already received before. For example if we use `B` and `G` we'd get `[C, D, E, F, J, K, H]` but this isn't efficient because we have already received `[E, F, G, H]` from the previous query. + +What we want to do is use the most distant descendants from `A` to make the next query to retrieve the last 3 process nodes to fulfill the request. Those would be `[C, E, F, H]`. So our next query will be: _find all process nodes where their ancestry array contains C or E or F or H_. This query can be limited to a size of 3 so that we will only receive `[D, J, K]`. + +We have now received all the nodes for the request and we can return the results as `[B, C, E, F, G, H, D, J, K]`. + +### Important Caveats + +#### Ordering + +In the previous example the final results are not sorted based on timestamp in ascending order. This is because we had to perform multiple queries to retrieve all the results. The backend will not return the results in sorted order. + +#### Tie breaks on timestamp + +In the previous example we saw that J and K had the same timestamp of `12:13 pm`. The reason they were returned in the order `[J, K]` is because the `event.id` field is used to break ties like this. The `event.id` field is unique for a particular event and an increasing value per ECS's guidelines. Therefore J comes before K because it has an `event.id` of 1 vs 2. + +#### Finding the most distant descendants + +In the previous scenario we saw that we needed to use the most distant descendants from a particular node. To determine if a node is a most distant descendant we can use the ancestry array. Nodes C, E, F, and H all have `A` as their last entry in the ancestry array. This indicates that they are a distant descendant that should be used in the next query. There's one problem with this approach. In a mostly impossible scenario where the node of interest (A) does not have any ancestors, its direct children will also have `A` as the last entry in their ancestry array. + +This edge case will likely never be encountered but we'll try to solve it anyway. To get around this as we iterate over the results from our first query (`[B, C, E, F, G, H]`) we can bucket the ones that have `A` as the last entry in their ancestry array. We bucket the results based on the length of their ancestry array (basically a `Map>`). So after bucketing our results will look like: + +```javascript +{ + 1: [B, G] + 2: [C, E, F, H] +} +``` + +While we are iterating we also keep track of the largest ancestry array that we have seen. In our scenario that will be a size of 2. Then to determine the distant descendants we simply get the nodes that had the largest ancestry array length. In this scenario that'd be `[C, E, F, H]`. + +### Handling Pagination + +#### Pagination Cursor Values + +There are 3 possible states for the pagination cursor for a child node and 2 possible states for the pagination cursor for the node of interest (the node that we are using in the API request). + +Potential cursors for the node of interest: +**a string cursor:** a cursor that can be used to skip the previous set of results. The cursor is a base64 version of a json object with `event.id` and `timestamp` of the last process's start event that was received. +**null:** indicates that no more results can be received using this process's entity_id + +Potential cursors for descendants of the node of interest (these apply to the results of a request): +**a string cursor:** a cursor that can be used to skip the previous set of results. The cursor is a base64 version of a json object with `event.id` and `timestamp` of the last process's start event that was received. This cursor should be used in conjunction with using this process's entity_id for a query. +**undefined:** the node may contain additional children, but we are not aware. To find out, perform additional queries on the node of interest that original returned these results or move down the tree to a descendant of this node to query for more descendants. +**null:** We have found all possible direct children for this node. There may be more descendants but not direct children for this node. + +#### Pagination Examples + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K. + +![alt text](./resolver_tree_children_pagination.png 'Descendants Pagination Scenario') + +Handling pagination for the children API in a little tricky. Let's consider this scenario: + +**Request:** `GET /resolver/A/children?children=3` + +Let's use the diagram above to show the relationship between processes and the data in ES. The response for the request for 3 children is `[B, C, G]`. More process nodes exist in ES so it would be helpful to indicate in the response that there is more data and a way to skip `[B, C, G]` to get the next set of data. A cursor can be set on the response for `A` to point to the last process node in the response which can be sent in another request to retrieve the next set of data. This cursor will contain information from `G` because it has the latest timestamp (in ascending order). + +If another request was made using the returned cursor like the following: + +**Request:** `GET /resolver/A/children?children=5&after=` + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K (the same as above). + +![alt text](./resolver_tree_children_pagination_with_after.png 'Descendants Pagination Scenario Part 2') + +For this request we will do a query for _find all process nodes where their ancestry array has entity_id `A` and use the cursor to skip old results_. The response for this request is `[F, H, E, K]`. The request actually asked for 5 nodes but there was only 4 in ES so only 4 were returned. + +The odd thing about this response is that it did not receive D and J. The problem is that the backend does not have any concept of C in this second request because it was received in the previous one. It will be skipped based on the pagination cursor returned previously. + +This example highlights a scenario where it is not easy for the backend to go back and continue to get the descendants for C because of the limitation of the ancestry array. + +#### Pagination cursor for descendant nodes + +Let's go back to the first request where we got `[B, C, G]`. How could we go about getting the rest of the children for `B`? We have two ways of solving this. First we could determine what the last descendant we had received of `B` and use that as the cursor when returning all the results for this request. That is actually kind of difficult. + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K (the same as above). + +![alt text](./resolver_tree_children_pagination.png 'Descendants Pagination Scenario') + +Let's imagine for a moment that the _ancestry array limit_ is 3 instead of 2. Taking our previous request we would instead get `[B, C, D]` because D started before G. In this case the last descendant for `B` is actually `D` and not `C`. This gets complicated because we'd have to keep track of which descendant was the last (time wise) one for each intermediate process node. In this example we'd need to find the last descendant for both `B` and `C`. We'd have to track the descendants for each process node and build a map to quickly be able to retrieve the last descendant. + +Instead of doing that we could also continue to get the immediate children of `B` by doing another request for `A` like was shown in previous example when using the after cursor. This would guarantee that all children (first level descendants of a node) had been retrieved. + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K (the same as above). + +![alt text](./resolver_tree_children_pagination_with_after.png 'Descendants Pagination Scenario Part 2') + +To get to the response shown in the diagram above (the blue nodes is the response) a request was made for 3 nodes which returned `[B, C, D]` and then another request was made using the returned cursor for `A` to get an additional 4 nodes `[F, H, E, K]`. Let's assume that the request looked like: + +**Request:** `GET /resolver/A/children?children=5&after=` + +So 5 nodes were actually asked for. After `[F, H, E]` are returned during the first query to ES, we will use the most distant children (also `[F, H, E]`) and make another request for any nodes that have F or H or E in their ancestry array. Only a single node satisfies that query which returns `K`. Therefore we can know with certainty that E, F, and H have no more children because ES only returned K instead of K and one more node (since we requested a total of 5). With this knowledge we can mark A, E, F, H's pagination cursors in a way to communicate that they have no more descendants. + +The way the backend communicates this is by marking the cursor as null. + +If the request was actually for only 4 children like: + +**Request:** `GET /resolver/A/children?children=4&after=` + +Then we wouldn't know for sure whether ES had more results than K. But what we can know is that `A` does not have any more descendants that we can retrieve in a single query using its ancestry array. We have received all nodes where `A` is in their ancestry array when we made the second query for nodes E, F, and H and received `K`. Therefore at the moment when we received `[F, H, E]` we can mark `A`'s cursor as null. + +When we make the next query for any nodes that have F or H or E in their ancestry array and get back K, this satisfies our size of only needing one more node (4 total). At this point we don't know for sure if E, F, or H have more descendants. Since we don't know we will mark E, F, and H's cursors to point to K. + +#### Undefined Pagination + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> E -> F -> G -> J -> K -> H. (Not the same as above). + +For this scenario let's assume ES has the data in the diagram below. Let's say the request looks like: + +**Request:** `GET /resolver/A/children?children=6` + +![alt text](./resolver_tree_children_loop.png 'Descendants Looping Scenario') + +The result for this request will be `[B, C, E, F, G, H]`. Since the request was looking for 6 nodes and we got that amount the cursor for `A` will be set to the last one: `H`. The cursors for the intermediate nodes `B` and `G` will be undefined. This is because we don't know if `B` or `G` have more children but `A` can be used to determine that. We also don't know if C, E, F, or H have more descendants so their cursor will also be marked as undefined. If we wanted to know if C had more descendants we can simply issue a new request like `GET /resolver/C/children` to get its descendants and we won't receive and duplicates because we never received D, J or K. + +If we want to know if `B` has more children we can issue another request using the cursor set for `A`. diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_ancestry.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_ancestry.png new file mode 100644 index 0000000000000..a2636c7cd38cb Binary files /dev/null and b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_ancestry.png differ diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_loop.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_loop.png new file mode 100644 index 0000000000000..9914031a3becb Binary files /dev/null and b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_loop.png differ diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination.png new file mode 100644 index 0000000000000..42b691616a679 Binary files /dev/null and b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination.png differ diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination_with_after.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination_with_after.png new file mode 100644 index 0000000000000..5a6accb279760 Binary files /dev/null and b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination_with_after.png differ diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_simple.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_simple.png new file mode 100644 index 0000000000000..f05963c0f0b67 Binary files /dev/null and b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_simple.png differ diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts index 9bf16dac791d7..7dd47658bc4c1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts @@ -74,8 +74,28 @@ export class AncestryQueryHandler implements QueryHandler { // bucket the start and end events together for a single node const ancestryNodes = this.toMapOfNodes(results); - // the order of this array is going to be weird, it will look like this - // [furthest grandparent...closer grandparent, next recursive call furthest grandparent...closer grandparent] + /** + * This array (this.ancestry.ancestors) is the accumulated ancestors of the node of interest. This array is different + * from the ancestry array of a specific document. The order of this array is going to be weird, it will look like this + * [most distant ancestor...closer ancestor, next recursive call most distant ancestor...closer ancestor] + * + * Here is an example of why this happens + * Consider the following tree: + * A -> B -> C -> D -> E -> Origin + * Where A was spawn before B, which was before C, etc + * + * Let's assume the ancestry array limit is 2 so Origin's array would be: [E, D] + * E's ancestry array would be: [D, C] etc + * + * If a request comes in to retrieve all the ancestors in this tree, the accumulate results will be: + * [D, E, B, C, A] + * + * The first iteration would retrieve D and E in that order because they are sorted in ascending order by timestamp. + * The next iteration would get the ancestors of D (since that's the most distant ancestor from Origin) which are + * [B, C] + * The next iteration would get the ancestors of B which is A + * Hence: [D, E, B, C, A] + */ this.ancestry.ancestors.push(...ancestryNodes.values()); this.ancestry.nextAncestor = parentEntityId(results[0]) || null; this.levels = this.levels - ancestryNodes.size; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts index 9e47f4eb94485..3f941851a4143 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts @@ -30,38 +30,9 @@ export interface Options { } /** - * This class aids in constructing a tree of process events. It works in the following way: + * This class aids in constructing a tree of process events. * - * 1. We construct a tree structure starting with the root node for the event we're requesting. - * 2. We leverage the ability to pass hashes and arrays by reference to construct a fast cache of - * process identifiers that updates the tree structure as we push values into the cache. - * - * When we query a single level of results for child process events we have a flattened, sorted result - * list that we need to add into a constructed tree. We also need to signal in an API response whether - * or not there are more child processes events that we have not yet retrieved, and, if so, for what parent - * process. So, at the end of our tree construction we have a relational layout of the events with no - * pagination information for the given parent nodes. In order to actually construct both the tree and - * insert the pagination information we basically do the following: - * - * 1. Using a terms aggregation query, we return an approximate roll-up of the number of child process - * "creation" events, this gives us an estimation of the number of associated children per parent - * 2. We feed these child process creation event "unique identifiers" (basically a process.entity_id) - * into a second query to get the current state of the process via its "lifecycle" events. - * 3. We construct the tree above with the "lifecycle" events. - * 4. Using the terms query results, we mark each non-leaf node with the number of expected children, if our - * tree has less children than expected, we create a pagination cursor to indicate "we have a truncated set - * of values". - * 5. We mark each leaf node (the last level of the tree we're constructing) with a "null" for the expected - * number of children to indicate "we have not yet attempted to get any children". - * - * Following this scheme, we use exactly 2 queries per level of children that we return--one for the pagination - * and one for the lifecycle events of the processes. The downside to this is that we need to dynamically expand - * the number of documents we can retrieve per level due to the exponential fanout of child processes, - * what this means is that noisy neighbors for a given level may hide other child process events that occur later - * temporally in the same level--so, while a heavily forking process might get shown, maybe the actually malicious - * event doesn't show up in the tree at the beginning. - * - * This Tree's root/origin could be in the middle of the tree. The origin corresponds to the id passed in when this + * This Tree's root/origin will likely be in the middle of the tree. The origin corresponds to the id passed in when this * Tree object is constructed. The tree can have ancestors and children coming from the origin. */ export class Tree { diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts index ab729bae6474d..f4a18a40f7d4b 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts @@ -54,8 +54,7 @@ export const createTimelineResolvers = ( args.search || null, args.sort || null, args.status || null, - args.timelineType || null, - args.templateTimelineType || null + args.timelineType || null ); }, }, diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index fce81e2f0dce0..58a13a7115b72 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -142,11 +142,6 @@ export const timelineSchema = gql` immutable } - enum TemplateTimelineType { - elastic - custom - } - enum RowRendererId { auditd auditd_file @@ -321,7 +316,7 @@ export const timelineSchema = gql` extend type Query { getOneTimeline(id: ID!): TimelineResult! - getAllTimeline(pageInfo: PageInfoTimeline, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineType: TimelineType, templateTimelineType: TemplateTimelineType, status: TimelineStatus): ResponseTimelines! + getAllTimeline(pageInfo: PageInfoTimeline, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineType: TimelineType, status: TimelineStatus): ResponseTimelines! } extend type Mutation { diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 1e397a4e6bb6c..ca0732816aa4d 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -399,11 +399,6 @@ export enum SortFieldTimeline { created = 'created', } -export enum TemplateTimelineType { - elastic = 'elastic', - custom = 'custom', -} - export enum NetworkDirectionEcs { inbound = 'inbound', outbound = 'outbound', @@ -430,6 +425,11 @@ export enum FlowDirection { biDirectional = 'biDirectional', } +export enum TemplateTimelineType { + elastic = 'elastic', + custom = 'custom', +} + export type ToStringArrayNoNullable = any; export type ToIFieldSubTypeNonNullable = any; @@ -2336,8 +2336,6 @@ export interface GetAllTimelineQueryArgs { timelineType?: Maybe; - templateTimelineType?: Maybe; - status?: Maybe; } export interface AuthenticationsSourceArgs { @@ -2814,8 +2812,6 @@ export namespace QueryResolvers { timelineType?: Maybe; - templateTimelineType?: Maybe; - status?: Maybe; } } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh index c267b4d9f36d5..3dd8e7f1097f4 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/find_timeline_by_filter.sh @@ -12,23 +12,22 @@ set -e # Uses a default if no argument is specified STATUS=${1:-active} TIMELINE_TYPE=${2:-default} -TEMPLATE_TIMELINE_TYPE=${3:-custom} # Example get all timelines: # ./timelines/find_timeline_by_filter.sh active # Example get all prepackaged timeline templates: -# ./timelines/find_timeline_by_filter.sh immutable template elastic +# ./timelines/find_timeline_by_filter.sh immutable template # Example get all custom timeline templates: -# ./timelines/find_timeline_by_filter.sh active template custom +# ./timelines/find_timeline_by_filter.sh active template curl -s -k \ -H "Content-Type: application/json" \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X POST "${KIBANA_URL}${SPACE_URL}/api/solutions/security/graphql" \ - -d '{"operationName":"GetAllTimeline","variables":{"onlyUserFavorite":false,"pageInfo":{"pageIndex":1,"pageSize":10},"search":"","sort":{"sortField":"updated","sortOrder":"desc"},"status":"'$STATUS'","timelineType":"'$TIMELINE_TYPE'","templateTimelineType":"'$TEMPLATE_TIMELINE_TYPE'"},"query":"query GetAllTimeline($pageInfo: PageInfoTimeline!, $search: String, $sort: SortTimeline, $onlyUserFavorite: Boolean, $timelineType: TimelineType, $templateTimelineType: TemplateTimelineType, $status: TimelineStatus) {\n getAllTimeline(pageInfo: $pageInfo, search: $search, sort: $sort, onlyUserFavorite: $onlyUserFavorite, timelineType: $timelineType, templateTimelineType: $templateTimelineType, status: $status) {\n totalCount\n defaultTimelineCount\n templateTimelineCount\n elasticTemplateTimelineCount\n customTemplateTimelineCount\n favoriteCount\n timeline {\n savedObjectId\n description\n favorite {\n fullName\n userName\n favoriteDate\n __typename\n }\n eventIdToNoteIds {\n eventId\n note\n timelineId\n noteId\n created\n createdBy\n timelineVersion\n updated\n updatedBy\n version\n __typename\n }\n notes {\n eventId\n note\n timelineId\n timelineVersion\n noteId\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n noteIds\n pinnedEventIds\n status\n title\n timelineType\n templateTimelineId\n templateTimelineVersion\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n __typename\n }\n}\n"}' \ + -d '{"operationName":"GetAllTimeline","variables":{"onlyUserFavorite":false,"pageInfo":{"pageIndex":1,"pageSize":10},"search":"","sort":{"sortField":"updated","sortOrder":"desc"},"status":"'$STATUS'","timelineType":"'$TIMELINE_TYPE'"},"query":"query GetAllTimeline($pageInfo: PageInfoTimeline!, $search: String, $sort: SortTimeline, $onlyUserFavorite: Boolean, $timelineType: TimelineType, $status: TimelineStatus) {\n getAllTimeline(pageInfo: $pageInfo, search: $search, sort: $sort, onlyUserFavorite: $onlyUserFavorite, timelineType: $timelineType, status: $status) {\n totalCount\n defaultTimelineCount\n templateTimelineCount\n elasticTemplateTimelineCount\n customTemplateTimelineCount\n favoriteCount\n timeline {\n savedObjectId\n description\n favorite {\n fullName\n userName\n favoriteDate\n __typename\n }\n eventIdToNoteIds {\n eventId\n note\n timelineId\n noteId\n created\n createdBy\n timelineVersion\n updated\n updatedBy\n version\n __typename\n }\n notes {\n eventId\n note\n timelineId\n timelineVersion\n noteId\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n noteIds\n pinnedEventIds\n status\n title\n timelineType\n templateTimelineId\n templateTimelineVersion\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n __typename\n }\n}\n"}' \ | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh index f58632c7cbbe3..335d1b8c86696 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/get_all_timelines.sh @@ -29,9 +29,8 @@ curl -s -k \ "sortOrder": "desc" }, "status": "active", - "timelineType": null, - "templateTimelineType": null + "timelineType": null }, - "query": "query GetAllTimeline($pageInfo: PageInfoTimeline!, $search: String, $sort: SortTimeline, $onlyUserFavorite: Boolean, $timelineType: TimelineType, $templateTimelineType: TemplateTimelineType, $status: TimelineStatus) {\n getAllTimeline(pageInfo: $pageInfo, search: $search, sort: $sort, onlyUserFavorite: $onlyUserFavorite, timelineType: $timelineType, templateTimelineType: $templateTimelineType, status: $status) {\n totalCount\n defaultTimelineCount\n templateTimelineCount\n elasticTemplateTimelineCount\n customTemplateTimelineCount\n favoriteCount\n timeline {\n savedObjectId\n description\n favorite {\n fullName\n userName\n favoriteDate\n __typename\n }\n eventIdToNoteIds {\n eventId\n note\n timelineId\n noteId\n created\n createdBy\n timelineVersion\n updated\n updatedBy\n version\n __typename\n }\n notes {\n eventId\n note\n timelineId\n timelineVersion\n noteId\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n noteIds\n pinnedEventIds\n status\n title\n timelineType\n templateTimelineId\n templateTimelineVersion\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n __typename\n }\n}\n" + "query": "query GetAllTimeline($pageInfo: PageInfoTimeline!, $search: String, $sort: SortTimeline, $onlyUserFavorite: Boolean, $timelineType: TimelineType, $status: TimelineStatus) {\n getAllTimeline(pageInfo: $pageInfo, search: $search, sort: $sort, onlyUserFavorite: $onlyUserFavorite, timelineType: $timelineType, status: $status) {\n totalCount\n defaultTimelineCount\n templateTimelineCount\n elasticTemplateTimelineCount\n customTemplateTimelineCount\n favoriteCount\n timeline {\n savedObjectId\n description\n favorite {\n fullName\n userName\n favoriteDate\n __typename\n }\n eventIdToNoteIds {\n eventId\n note\n timelineId\n noteId\n created\n createdBy\n timelineVersion\n updated\n updatedBy\n version\n __typename\n }\n notes {\n eventId\n note\n timelineId\n timelineVersion\n noteId\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n noteIds\n pinnedEventIds\n status\n title\n timelineType\n templateTimelineId\n templateTimelineVersion\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n __typename\n }\n}\n" }' | jq . diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts index b50195219f993..6bc0ca64ae33f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts @@ -16,8 +16,6 @@ import { TimelineTypeLiteralWithNull, ExportTimelineNotFoundError, TimelineStatusLiteralWithNull, - TemplateTimelineTypeLiteralWithNull, - TemplateTimelineType, } from '../../../common/types/timeline'; import { ResponseTimeline, @@ -69,8 +67,7 @@ export interface Timeline { search: string | null, sort: SortTimeline | null, status: TimelineStatusLiteralWithNull, - timelineType: TimelineTypeLiteralWithNull, - templateTimelineType: TemplateTimelineTypeLiteralWithNull + timelineType: TimelineTypeLiteralWithNull ) => Promise; persistFavorite: ( @@ -121,7 +118,6 @@ export const getTimelineByTemplateTimelineId = async ( * which has no timelineType exists in the savedObject */ const getTimelineTypeFilter = ( timelineType: TimelineTypeLiteralWithNull, - templateTimelineType: TemplateTimelineTypeLiteralWithNull, status: TimelineStatusLiteralWithNull ) => { const typeFilter = @@ -149,14 +145,7 @@ const getTimelineTypeFilter = ( ? `siem-ui-timeline.attributes.status: ${TimelineStatus.immutable}` : `not siem-ui-timeline.attributes.status: ${TimelineStatus.immutable}`; - const templateTimelineTypeFilter = - templateTimelineType == null - ? null - : templateTimelineType === TemplateTimelineType.elastic - ? `siem-ui-timeline.attributes.createdBy: "Elastic"` - : `not siem-ui-timeline.attributes.createdBy: "Elastic"`; - - const filters = [typeFilter, draftFilter, immutableFilter, templateTimelineTypeFilter]; + const filters = [typeFilter, draftFilter, immutableFilter]; return filters.filter((f) => f != null).join(' and '); }; @@ -177,11 +166,7 @@ export const getExistingPrepackagedTimelines = async ( const elasticTemplateTimelineOptions = { type: timelineSavedObjectType, ...queryPageInfo, - filter: getTimelineTypeFilter( - TimelineType.template, - TemplateTimelineType.elastic, - TimelineStatus.immutable - ), + filter: getTimelineTypeFilter(TimelineType.template, TimelineStatus.immutable), }; return getAllSavedTimeline(request, elasticTemplateTimelineOptions); @@ -194,8 +179,7 @@ export const getAllTimeline = async ( search: string | null, sort: SortTimeline | null, status: TimelineStatusLiteralWithNull, - timelineType: TimelineTypeLiteralWithNull, - templateTimelineType: TemplateTimelineTypeLiteralWithNull + timelineType: TimelineTypeLiteralWithNull ): Promise => { const options: SavedObjectsFindOptions = { type: timelineSavedObjectType, @@ -205,7 +189,7 @@ export const getAllTimeline = async ( searchFields: onlyUserFavorite ? ['title', 'description', 'favorite.keySearch'] : ['title', 'description'], - filter: getTimelineTypeFilter(timelineType, templateTimelineType, status), + filter: getTimelineTypeFilter(timelineType, status), sortField: sort != null ? sort.sortField : undefined, sortOrder: sort != null ? sort.sortOrder : undefined, }; @@ -214,25 +198,21 @@ export const getAllTimeline = async ( type: timelineSavedObjectType, perPage: 1, page: 1, - filter: getTimelineTypeFilter(TimelineType.default, null, TimelineStatus.active), + filter: getTimelineTypeFilter(TimelineType.default, TimelineStatus.active), }; const templateTimelineOptions = { type: timelineSavedObjectType, perPage: 1, page: 1, - filter: getTimelineTypeFilter(TimelineType.template, null, null), + filter: getTimelineTypeFilter(TimelineType.template, null), }; const customTemplateTimelineOptions = { type: timelineSavedObjectType, perPage: 1, page: 1, - filter: getTimelineTypeFilter( - TimelineType.template, - TemplateTimelineType.custom, - TimelineStatus.active - ), + filter: getTimelineTypeFilter(TimelineType.template, TimelineStatus.active), }; const favoriteTimelineOptions = { @@ -240,7 +220,7 @@ export const getAllTimeline = async ( searchFields: ['title', 'description', 'favorite.keySearch'], perPage: 1, page: 1, - filter: getTimelineTypeFilter(timelineType, null, TimelineStatus.active), + filter: getTimelineTypeFilter(timelineType, TimelineStatus.active), }; const result = await Promise.all([ @@ -269,11 +249,7 @@ export const getDraftTimeline = async ( const options: SavedObjectsFindOptions = { type: timelineSavedObjectType, perPage: 1, - filter: getTimelineTypeFilter( - timelineType, - timelineType === TimelineType.template ? TemplateTimelineType.custom : null, - TimelineStatus.draft - ), + filter: getTimelineTypeFilter(timelineType, TimelineStatus.draft), sortField: 'created', sortOrder: 'desc', }; @@ -567,7 +543,6 @@ export const getTimelines = async (request: FrameworkRequest, timelineIds?: stri null, null, TimelineStatus.active, - null, null ); exportedIds = savedAllTimelines.map((t) => t.savedObjectId); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts index b8e7012529554..688a1a409680a 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts @@ -236,9 +236,9 @@ export function registerPolicyRoutes({ 'transport.request', { method: 'GET', - path: `_resolve/index/*`, + path: `/_resolve/index/*`, query: { - expand_wildcards: 'all,hidden', + expand_wildcards: 'all', }, } ); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a433cdace3710..ff8c3186d5cf8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -87,7 +87,7 @@ "advancedSettings.categoryNames.notificationsLabel": "通知", "advancedSettings.categoryNames.reportingLabel": "レポート", "advancedSettings.categoryNames.searchLabel": "検索", - "advancedSettings.categoryNames.securitySolutionLabel": "Security Solution", + "advancedSettings.categoryNames.securitySolutionLabel": "セキュリティソリューション", "advancedSettings.categoryNames.timelionLabel": "Timelion", "advancedSettings.categoryNames.visualizationsLabel": "可視化", "advancedSettings.categorySearchLabel": "カテゴリー", @@ -124,6 +124,121 @@ "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", "advancedSettings.searchBarAriaLabel": "高度な設定を検索", "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "{query} を検索しました。{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other {# オプション}}があります。", + "apmOss.tutorial.apmAgents.statusCheck.btnLabel": "エージェントステータスを確認", + "apmOss.tutorial.apmAgents.statusCheck.errorMessage": "エージェントからまだデータを受け取っていません", + "apmOss.tutorial.apmAgents.statusCheck.successMessage": "1 つまたは複数のエージェントからデータを受け取りました", + "apmOss.tutorial.apmAgents.statusCheck.text": "アプリケーションが実行されていてエージェントがデータを送信していることを確認してください。", + "apmOss.tutorial.apmAgents.statusCheck.title": "エージェントステータス", + "apmOss.tutorial.apmAgents.title": "APM エージェント", + "apmOss.tutorial.apmServer.callOut.message": "ご使用の APM Server を 7.0 以上に更新してあることを確認してください。 Kibana の管理セクションにある移行アシスタントで 6.x データを移行することもできます。", + "apmOss.tutorial.apmServer.callOut.title": "重要:7.0 以上に更新中", + "apmOss.tutorial.apmServer.statusCheck.btnLabel": "APM Server ステータスを確認", + "apmOss.tutorial.apmServer.statusCheck.errorMessage": "APM Server が検出されました。7.0 以上に更新され、動作中であることを確認してください。", + "apmOss.tutorial.apmServer.statusCheck.successMessage": "APM Server が正しくセットアップされました", + "apmOss.tutorial.apmServer.statusCheck.text": "APM エージェントの導入を開始する前に、APM Server が動作していることを確認してください。", + "apmOss.tutorial.apmServer.statusCheck.title": "APM Server ステータス", + "apmOss.tutorial.apmServer.title": "APM Server", + "apmOss.tutorial.djangoClient.configure.commands.addAgentComment": "インストールされたアプリにエージェントを追加します", + "apmOss.tutorial.djangoClient.configure.commands.addTracingMiddlewareComment": "パフォーマンスメトリックを送信するには、追跡ミドルウェアを追加します。", + "apmOss.tutorial.djangoClient.configure.commands.allowedCharactersComment": "a-z、A-Z、0-9、-、_、スペース", + "apmOss.tutorial.djangoClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", + "apmOss.tutorial.djangoClient.configure.commands.setRequiredServiceNameComment": "必要なサーバー名を設定します。使用できる文字:", + "apmOss.tutorial.djangoClient.configure.commands.useIfApmServerRequiresTokenComment": "APM Server にトークンが必要な場合に使います", + "apmOss.tutorial.djangoClient.configure.textPost": "高度な用途に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.djangoClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは「SERVICE_NAME」に基づいてプログラムで作成されます。", + "apmOss.tutorial.djangoClient.configure.title": "エージェントの構成", + "apmOss.tutorial.djangoClient.install.textPre": "Python 用の APM エージェントを依存関係としてインストールします。", + "apmOss.tutorial.djangoClient.install.title": "APM エージェントのインストール", + "apmOss.tutorial.dotNetClient.configureAgent.textPost": "エージェントに「IConfiguration」インスタンスが渡されていない場合、(例: 非 ASP.NET Core アプリケーションの場合)、エージェントを環境変数で構成することもできます。\n 高度な用途に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.dotNetClient.configureAgent.title": "appsettings.json ファイルの例:", + "apmOss.tutorial.dotNetClient.configureApplication.textPost": "「IConfiguration」インスタンスを渡すのは任意であり、これにより、エージェントはこの「IConfiguration」インスタンス (例: 「appsettings.json」ファイル) から構成を読み込みます。", + "apmOss.tutorial.dotNetClient.configureApplication.textPre": "「Elastic.Apm.NetCoreAll」パッケージの ASP.NET Core の場合、「Startup.cs」ファイル内の「Configure」メソドの「UseElasticApm」メソドを呼び出します。", + "apmOss.tutorial.dotNetClient.configureApplication.title": "エージェントをアプリケーションに追加", + "apmOss.tutorial.dotNetClient.download.textPre": "[NuGet]({allNuGetPackagesLink}) から .NET アプリケーションにエージェントパッケージを追加してください。用途の異なる複数の NuGet パッケージがあります。\n\nEntity Framework Core の ASP.NET Core アプリケーションの場合は、[Elastic.Apm.NetCoreAll]({netCoreAllApmPackageLink}) パッケージをダウンロードしてください。このパッケージは、自動的にすべてのエージェントコンポーネントをアプリケーションに追加します。\n\n 依存性を最低限に抑えたい場合、ASP.NET Core の監視のみに [Elastic.Apm.AspNetCore]({aspNetCorePackageLink}) パッケージ、または Entity Framework Core の監視のみに [Elastic.Apm.EfCore]({efCorePackageLink}) パッケージを使用することができます。\n\n 手動インストルメンテーションのみにパブリック Agent API を使用する場合は、[Elastic.Apm]({elasticApmPackageLink}) パッケージを使用してください。", + "apmOss.tutorial.dotNetClient.download.title": "APM エージェントのダウンロード", + "apmOss.tutorial.downloadServer.title": "APM Server をダウンロードして展開します", + "apmOss.tutorial.downloadServerRpm": "32 ビットパッケージをお探しですか?[ダウンロードページ]({downloadPageLink}) をご覧ください。", + "apmOss.tutorial.downloadServerTitle": "32 ビットパッケージをお探しですか?[ダウンロードページ]({downloadPageLink}) をご覧ください。", + "apmOss.tutorial.editConfig.textPre": "Elastic Stack の X-Pack セキュアバージョンをご使用の場合、「apm-server.yml」構成ファイルで認証情報を指定する必要があります。", + "apmOss.tutorial.editConfig.title": "構成を編集する", + "apmOss.tutorial.flaskClient.configure.commands.allowedCharactersComment": "a-z、A-Z、0-9、-、_、スペース", + "apmOss.tutorial.flaskClient.configure.commands.configureElasticApmComment": "またはアプリケーションの設定で ELASTIC_APM を使用するよう構成します。", + "apmOss.tutorial.flaskClient.configure.commands.initializeUsingEnvironmentVariablesComment": "環境変数を使用して初期化します", + "apmOss.tutorial.flaskClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", + "apmOss.tutorial.flaskClient.configure.commands.setRequiredServiceNameComment": "必要なサーバー名を設定します。使用できる文字:", + "apmOss.tutorial.flaskClient.configure.commands.useIfApmServerRequiresTokenComment": "APM Server にトークンが必要な場合に使います", + "apmOss.tutorial.flaskClient.configure.textPost": "高度な用途に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.flaskClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは「SERVICE_NAME」に基づいてプログラムで作成されます。", + "apmOss.tutorial.flaskClient.configure.title": "エージェントの構成", + "apmOss.tutorial.flaskClient.install.textPre": "Python 用の APM エージェントを依存関係としてインストールします。", + "apmOss.tutorial.flaskClient.install.title": "APM エージェントのインストール", + "apmOss.tutorial.goClient.configure.commands.initializeUsingEnvironmentVariablesComment": "環境変数を使用して初期化します:", + "apmOss.tutorial.goClient.configure.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", + "apmOss.tutorial.goClient.configure.commands.setServiceNameComment": "サービス名を設定します。使用できる文字は # a-z、A-Z、0-9、-、_、スペースです。", + "apmOss.tutorial.goClient.configure.commands.usedExecutableNameComment": "ELASTIC_APM_SERVICE_NAME が指定されていない場合、実行可能な名前が使用されます。", + "apmOss.tutorial.goClient.configure.commands.useIfApmRequiresTokenComment": "APM Server にトークンが必要な場合に使います", + "apmOss.tutorial.goClient.configure.textPost": "高度な構成に関しては [ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.goClient.configure.textPre": "エージェントとは、アプリケーションプロセス内で実行されるライブラリです。APM サービスは実行ファイル名または「ELASTIC_APM_SERVICE_NAME」環境変数に基づいてプログラムで作成されます。", + "apmOss.tutorial.goClient.configure.title": "エージェントの構成", + "apmOss.tutorial.goClient.install.textPre": "Go の APM エージェントパッケージをインストールします。", + "apmOss.tutorial.goClient.install.title": "APM エージェントのインストール", + "apmOss.tutorial.goClient.instrument.textPost": "Go のソースコードのインストルメンテーションの詳細ガイドは、[ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.goClient.instrument.textPre": "提供されたインストルメンテーションモジュールの 1 つ、またはトレーサー API を直接使用して、Go アプリケーションにインストルメンテーションを設定します。", + "apmOss.tutorial.goClient.instrument.title": "アプリケーションのインストルメンテーション", + "apmOss.tutorial.introduction": "アプリケーション内から詳細なパフォーマンスメトリックやエラーを収集します。", + "apmOss.tutorial.javaClient.download.textPre": "[Maven Central]({mavenCentralLink}) からエージェントをダウンロードします。アプリケーションにエージェントを依存関係として「追加しない」でください。", + "apmOss.tutorial.javaClient.download.title": "APM エージェントのダウンロード", + "apmOss.tutorial.javaClient.startApplication.textPost": "構成オプションと高度な用途に関しては、[ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.javaClient.startApplication.textPre": "「-javaagent」フラグを追加してエージェントをシステムプロパティで構成します。\n\n * 必要なサービス名を設定します (使用可能な文字は a-z、A-Z、0-9、-、_、スペースです)\n * カスタム APM Server URL (デフォルト: {customApmServerUrl})\n * アプリケーションのベースパッケージを設定します", + "apmOss.tutorial.javaClient.startApplication.title": "javaagent フラグでアプリケーションを起動", + "apmOss.tutorial.jsClient.enableRealUserMonitoring.textPre": "デフォルトでは、APM Server を実行すると RUM サポートは無効になります。RUM サポートを有効にする手順については、[ドキュメンテーション]({documentationLink}) をご覧ください。", + "apmOss.tutorial.jsClient.enableRealUserMonitoring.title": "APMサーバーのリアルユーザー監視サポートを有効にする", + "apmOss.tutorial.jsClient.installDependency.commands.setCustomApmServerUrlComment": "カスタム APM Server URL (デフォルト: {defaultApmServerUrl})", + "apmOss.tutorial.jsClient.installDependency.commands.setRequiredServiceNameComment": "必要なサービス名を設定します (使用可能な文字は a-z、A-Z、0-9、-、_、スペースです)", + "apmOss.tutorial.jsClient.installDependency.commands.setServiceVersionComment": "サービスバージョンを設定します (ソースマップ機能に必要)", + "apmOss.tutorial.jsClient.installDependency.textPost": "React や Angular などのフレームワーク統合には、カスタム依存関係があります。詳細は [統合ドキュメント]({docLink}) をご覧ください。", + "apmOss.tutorial.jsClient.installDependency.textPre": "「npm install @elastic/apm-rum --save」でエージェントをアプリケーションへの依存関係としてインストールできます。\n\nその後で以下のようにアプリケーションでエージェントを初期化して構成できます。", + "apmOss.tutorial.jsClient.installDependency.title": "エージェントを依存関係としてセットアップ", + "apmOss.tutorial.jsClient.scriptTags.textPre": "または、スクリプトタグを使用してエージェントのセットアップと構成ができます。` を追加