diff --git a/.eslintrc.js b/.eslintrc.js index c9f9d96f9ddaee..b3d29c98664114 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 9bc24f4d1d366c..4778c98493b5bd 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 8be7a9edde3632..6d3f8df2fa5182 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 e545cf42d3c26f..ed64d61e75fabc 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 1f6e1f9988fc26..1d837d9b4705d6 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 bd343f5bc74764..8eeb5ef638a829 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 00000000000000..1ba359e81b9c62 --- /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 00000000000000..591f126c423e3c --- /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 00000000000000..d4a13812ab5338 --- /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 e059acdbd52fa2..860867d6544358 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 8958b49d98b0c7..b0dc4d44f75594 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 00000000000000..c7adc345af5a33 --- /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 00000000000000..301fcbfee58581 --- /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 00000000000000..f6bacee322538d --- /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 00000000000000..5fa2e93cca75bd --- /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 00000000000000..189a50b5d6c20a --- /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 c70a5ac07c6ad8..b5fbb3d54b972a 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 a3cb8f13150212..4da121984d0847 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 1263b85acb4983..51d0b2e4882cb6 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 00f16595490784..706898c4ad9aa2 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 00000000000000..ddc6357bb8835e --- /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 00000000000000..f7f308aa131614 --- /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 00000000000000..f39db268288a6f --- /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 e6c2878d2b3556..168209659046e7 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 9ebe2fc57a54bf..40def157114ef2 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 c51f1858c97a57..668d0b2866a264 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 62b0f216c863c0..78f7bf582d355e 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 f760780504e55f..40fc1a8e05a68b 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 c4a94d8661c47b..7f752d70921ba7 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 95b7627398b45e..5347f0b55e19b4 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 69fb573d365300..a924f0cea6b6b0 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 c3256eaa783314..0fcae8c847cb40 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 2d31c24a077cbf..5b8492ec5ece1b 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 07e6dcbdae125e..4e530973f9d50a 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/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc new file mode 100644 index 00000000000000..6bc085b0f78b9e --- /dev/null +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -0,0 +1,202 @@ +[role="xpack"] +[[alerting-getting-started]] += Alerting and Actions + +beta[] + +-- + +Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. + +image::images/alerting-overview.png[Alerts and actions UI] + +[IMPORTANT] +============================================== +To make sure you can access alerting and actions, see the <> section. +============================================== + +[float] +== Concepts and terminology + +*Alerts* work by running checks on a schedule to detect conditions. When a condition is met, the alert tracks it as an *alert instance* and responds by triggering one or more *actions*. +Actions typically involve interaction with {kib} services or third party integrations. *Connectors* allow actions to talk to these services and integrations. +This section describes all of these elements and how they operate together. + +[float] +=== What is an alert? + +An alert specifies a background task that runs on the {kib} server to check for specific conditions. It consists of three main parts: + +* *Conditions*: what needs to be detected? +* *Schedule*: when/how often should detection checks run? +* *Actions*: what happens when a condition is detected? + +For example, when monitoring a set of servers, an alert might check for average CPU usage > 0.9 on each server for the two minutes (condition), checked every minute (schedule), sending a warning email message via SMTP with subject `CPU on {{server}} is high` (action). + +image::images/what-is-an-alert.svg[Three components of an alert] + +The following sections each part of the alert is described in more detail. + +[float] +[[alerting-concepts-conditions]] +==== Conditions + +Under the hood, {kib} alerts detect conditions by running javascript function on the {kib} server, which gives it flexibility to support a wide range of detections, anything from the results of a simple {es} query to heavy computations involving data from multiple sources or external systems. + +These detections are packaged and exposed as *alert types*. An alert type hides the underlying details of the detection, and exposes a set of parameters +to control the details of the conditions to detect. + +For example, an <> lets you specify the index to query, an aggregation field, and a time window, but the details of the underlying {es} query are hidden. + +See <> for the types of alerts provided by {kib} and how they express their conditions. + +[float] +[[alerting-concepts-scheduling]] +==== Schedule + +Alert schedules are defined as an interval between subsequent checks, and can range from a few seconds to months. + +[IMPORTANT] +============================================== +The intervals of alert checks in {kib} are approximate, their timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. +============================================== + +[float] +[[alerting-concepts-actions]] +==== Actions + +Actions are invocations of {kib} services or integrations with third-party systems, that run as background tasks on the {kib} server when alert conditions are met. + +When defining actions in an alert, you specify: + +* the *action type*: the type of service or integration to use +* the connection for that type by referencing a <> +* a mapping of alert values to properties exposed for that type of action + +The result is a template: all the parameters needed to invoke a service are supplied except for specific values that are only known at the time the alert condition is detected. + +In the server monitoring example, the `email` action type is used, and `server` is mapped to the body of the email, using the template string `CPU on {{server}} is high`. + +When the alert detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and executes the action on the {kib} server by invoking the `email` action type. + +image::images/what-is-an-action.svg[Actions are like templates that are rendered when an alert detects a condition] + +See <> for details on the types of actions provided by {kib}. + +[float] +[[alerting-concepts-alert-instances]] +=== Alert instances + +When checking for a condition, an alert might identify multiple occurrences of the condition. {kib} tracks each of these *alert instances* separately and takes action per instance. + +Using the server monitoring example, each server with average CPU > 0.9 is tracked as an alert instance. This means a separate email is sent for each server that exceeds the threshold. + +image::images/alert-instances.svg[{kib} tracks each detected condition as an alert instance and takes action on each instance] + +[float] +[[alerting-concepts-suppressing-duplicate-notifications]] +=== Suppressing duplicate notifications + +Since actions are taken per instance, alerts can end up generating a large number of actions. Take the following example where an alert is monitoring three servers every minute for CPU usage > 0.9: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, on for X123 and one for Y456. +* Minute 3: X123, Y456, Z789 > 0.9. *Three emails* are sent, one for each of X123, Y456, Z789. + +In the above example, three emails are sent for server X123 in the span of 3 minutes for the same condition. Often it's desirable to suppress frequent re-notification. Operations like muting and re-notification throttling can be applied at the instance level. If we set the alert re-notify interval to 5 minutes, we reduce noise by only getting emails for new servers that exceed the threshold: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *One email* is sent for Y456 +* Minute 3: X123, Y456, Z789 > 0.9. *One email* is sent for Z789. + +[float] +[[alerting-concepts-connectors]] +=== Connectors + +Actions often involve connecting with services inside {kib} or integrations with third-party systems. +Rather than repeatedly entering connection information and credentials for each action, {kib} simplifies action setup using *connectors*. + +*Connectors* provide a central place to store connection information for services and integrations. For example if four alerts send email notifications via the same SMTP service, +they all reference the same SMTP connector. When the SMTP settings change they are updated once in the connector, instead of having to update four alerts. + +image::images/alert-concepts-connectors.svg[Connectors provide a central place to store service connection settings] + +[float] +=== Summary + +An _alert_ consists of conditions, _actions_, and a schedule. When conditions are met, _alert instances_ are created that render _actions_ and invoke them. To make action setup and update easier, actions refer to _connectors_ that centralize the information used to connect with {kib} services and third-party integrations. + +image::images/alert-concepts-summary.svg[Alerts, actions, alert instances and connectors work together to convert detection into action] + +* *Alert*: a specification of the conditions to be detected, the schedule for detection, and the response when detection occurs. +* *Action*: the response to a detected condition defined in the alert. Typically actions specify a service or third party integration along with alert details that will be sent to it. +* *Alert instance*: state tracked by {kib} for every occurrence of a detected condition. Actions as well as controls like muting and re-notification are controlled at the instance level. +* *Connector*: centralized configurations for services and third party integration that are referenced by actions. + +[float] +[[alerting-concepts-differences]] +== Differences from Watcher + +{kib} alerting and <> are both used to detect conditions and can trigger actions in response, but they are completely independent alerting systems. + +This section will clarify some of the important differences in the function and intent of the two systems. + +Functionally, {kib} alerting differs in that: + +* Scheduled checks are run on {kib} instead of {es} +* {kib} <> through *alert types*, whereas watches provide low-level control over inputs, conditions, and transformations. +* {kib} alerts tracks and persists the state of each detected condition through *alert instances*. This makes it possible to mute and throttle individual instances, and detect changes in state such as resolution. +* Actions are linked to *alert instances* in {kib} alerting. Actions are fired for each occurrence of a detected condition, rather than for the entire alert. + +At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. +Pre-packaged *alert types* simplify setup, hide the details complex domain-specific detections, while providing a consistent interface across {kib}. + +[float] +[[alerting-setup-prerequisites]] +== Setup and prerequisites + +If you are using an *on-premises* Elastic Stack deployment: + +* In the kibana.yml configuration file, add the <> setting. + +If you are using an *on-premises* Elastic Stack deployment with <>: + +* You must enable Transport Layer Security (TLS) for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. A proxy will not suffice. + +[float] +[[alerting-security]] +== Security + +To access alerting in a space, a user must have access to one of the following features: + +* <> +* <> +* <> +* <> + +See <> for more information on configuring roles that provide access to these features. + +[float] +[[alerting-spaces]] +=== Space isolation + +Alerts and connectors are isolated to the {kib} space in which they were created. An alert or connector created in one space will not be visible in another. + +[float] +[[alerting-authorization]] +=== Authorization + +Alerts, including all background detection and the actions they generate are authorized using an <> associated with the last user to edit the alert. Upon creating or modifying an alert, an API key is generated for that user, capturing a snapshot of their privileges at that moment in time. The API key is then used to run all background tasks associated with the alert including detection checks and executing actions. + +[IMPORTANT] +============================================== +If an alert requires certain privileges to run such as index privileges, keep in mind that if a user without those privileges updates the alert, the alert will no longer function. +============================================== + +[float] +[[alerting-restricting-actions]] +=== Restricting actions + +For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and whitelist the hostnames that {kib} can connect with. + +-- \ No newline at end of file diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index 6f691f2715bc8b..56404d9a33b80f 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -1,205 +1,4 @@ -[role="xpack"] -[[alerting-getting-started]] -= Alerting and Actions - -beta[] - --- - -Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. - -image::images/alerting-overview.png[Alerts and actions UI] - -[IMPORTANT] -============================================== -To make sure you can access alerting and actions, see the <> section. -============================================== - -[float] -== Concepts and terminology - -*Alerts* work by running checks on a schedule to detect conditions. When a condition is met, the alert tracks it as an *alert instance* and responds by triggering one or more *actions*. -Actions typically involve interaction with {kib} services or third party integrations. *Connectors* allow actions to talk to these services and integrations. -This section describes all of these elements and how they operate together. - -[float] -=== What is an alert? - -An alert specifies a background task that runs on the {kib} server to check for specific conditions. It consists of three main parts: - -* *Conditions*: what needs to be detected? -* *Schedule*: when/how often should detection checks run? -* *Actions*: what happens when a condition is detected? - -For example, when monitoring a set of servers, an alert might check for average CPU usage > 0.9 on each server for the two minutes (condition), checked every minute (schedule), sending a warning email message via SMTP with subject `CPU on {{server}} is high` (action). - -image::images/what-is-an-alert.svg[Three components of an alert] - -The following sections each part of the alert is described in more detail. - -[float] -[[alerting-concepts-conditions]] -==== Conditions - -Under the hood, {kib} alerts detect conditions by running javascript function on the {kib} server, which gives it flexibility to support a wide range of detections, anything from the results of a simple {es} query to heavy computations involving data from multiple sources or external systems. - -These detections are packaged and exposed as *alert types*. An alert type hides the underlying details of the detection, and exposes a set of parameters -to control the details of the conditions to detect. - -For example, an <> lets you specify the index to query, an aggregation field, and a time window, but the details of the underlying {es} query are hidden. - -See <> for the types of alerts provided by {kib} and how they express their conditions. - -[float] -[[alerting-concepts-scheduling]] -==== Schedule - -Alert schedules are defined as an interval between subsequent checks, and can range from a few seconds to months. - -[IMPORTANT] -============================================== -The intervals of alert checks in {kib} are approximate, their timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. -============================================== - -[float] -[[alerting-concepts-actions]] -==== Actions - -Actions are invocations of {kib} services or integrations with third-party systems, that run as background tasks on the {kib} server when alert conditions are met. - -When defining actions in an alert, you specify -* the *action type*: the type of service or integration to use> -* the connection for that type by referencing a <>. -* a mapping of alert values to properties exposed for that type of action. - -The result is a template: all the parameters needed to invoke a service are supplied except for specific values that are only known at the time the alert condition is detected. - -In the server monitoring example, the `email` action type is used, and `server` is mapped to the body of the email, using the template string `CPU on {{server}} is high`. - -When the alert detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and executes the action on the {kib} server by invoking the `email` action type. - -image::images/what-is-an-action.svg[Actions are like templates that are rendered when an alert detects a condition] - -See <> for details on the types of actions provided by {kib}. - -[float] -[[alerting-concepts-alert-instances]] -=== Alert instances - -When checking for a condition, an alert might identify multiple occurrences of the condition. {kib} tracks each of these *alert instances* separately and takes action per instance. - -Using the server monitoring example, each server with average CPU > 0.9 is tracked as an alert instance. This means a separate email is sent for each server that exceeds the threshold. - -image::images/alert-instances.svg[{kib} tracks each detected condition as an alert instance and takes action on each instance] - -[float] -[[alerting-concepts-suppressing-duplicate-notifications]] -=== Suppressing duplicate notifications - -Since actions are taken per instance, alerts can end up generating a large number of actions. Take the following example where an alert is monitoring three servers every minute for CPU usage > 0.9: - -* Minute 1: server X123 > 0.9. *One email* is sent for server X123. -* Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, on for X123 and one for Y456. -* Minute 3: X123, Y456, Z789 > 0.9. *Three emails* are sent, one for each of X123, Y456, Z789. - -In the above example, three emails are sent for server X123 in the span of 3 minutes for the same condition. Often it's desirable to suppress frequent re-notification. Operations like muting and re-notification throttling can be applied at the instance level. If we set the alert re-notify interval to 5 minutes, we reduce noise by only getting emails for new servers that exceed the threshold: - -* Minute 1: server X123 > 0.9. *One email* is sent for server X123. -* Minute 2: X123 and Y456 > 0.9. *One email* is sent for Y456 -* Minute 3: X123, Y456, Z789 > 0.9. *One email* is sent for Z789. - -[float] -[[alerting-concepts-connectors]] -=== Connectors - -Actions often involve connecting with services inside {kib} or integrations with third-party systems. -Rather than repeatedly entering connection information and credentials for each action, {kib} simplifies action setup using *connectors*. - -*Connectors* provide a central place to store connection information for services and integrations. For example if four alerts send email notifications via the same SMTP service, -they all reference the same SMTP connector. When the SMTP settings change they are updated once in the connector, instead of having to update four alerts. - -image::images/alert-concepts-connectors.svg[Connectors provide a central place to store service connection settings] - -[float] -=== Summary - -An _alert_ consists of conditions, _actions_, and a schedule. When conditions are met, _alert instances_ are created that render _actions_ and invoke them. To make action setup and update easier, actions refer to _connectors_ that centralize the information used to connect with {kib} services and third-party integrations. - -image::images/alert-concepts-summary.svg[Alerts, actions, alert instances and connectors work together to convert detection into action] - -* *Alert*: a specification of the conditions to be detected, the schedule for detection, and the response when detection occurs. -* *Action*: the response to a detected condition defined in the alert. Typically actions specify a service or third party integration along with alert details that will be sent to it. -* *Alert instance*: state tracked by {kib} for every occurrence of a detected condition. Actions as well as controls like muting and re-notification are controlled at the instance level. -* *Connector*: centralized configurations for services and third party integration that are referenced by actions. - -[float] -[[alerting-concepts-differences]] -== Differences from Watcher - -{kib} alerting and <> are both used to detect conditions and can trigger actions in response, but they are completely independent alerting systems. - -This section will clarify some of the important differences in the function and intent of the two systems. - -Functionally, {kib} alerting differs in that: - -* Scheduled checks are run on {kib} instead of {es} -* {kib} <> through *alert types*, whereas watches provide low-level control over inputs, conditions, and transformations. -* {kib} alerts tracks and persists the state of each detected condition through *alert instances*. This makes it possible to mute and throttle individual instances, and detect changes in state such as resolution. -* Actions are linked to *alert instances* in {kib} alerting. Actions are fired for each occurrence of a detected condition, rather than for the entire alert. - -At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. -Pre-packaged *alert types* simplify setup, hide the details complex domain-specific detections, while providing a consistent interface across {kib}. - -[float] -[[alerting-setup-prerequisites]] -== Setup and prerequisites - -If you are using an *on-premises* Elastic Stack deployment: - -* In the kibana.yml configuration file, add the <> setting. - -If you are using an *on-premises* Elastic Stack deployment with <>: - -* You must enable Transport Layer Security (TLS) for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. A proxy will not suffice. - -[float] -[[alerting-security]] -== Security - -To access alerting in a space, a user must have access to one of the following features: - -* <> -* <> -* <> -* <> - -See <> for more information on configuring roles that provide access to these features. - -[float] -[[alerting-spaces]] -=== Space isolation - -Alerts and connectors are isolated to the {kib} space in which they were created. An alert or connector created in one space will not be visible in another. - -[float] -[[alerting-authorization]] -=== Authorization - -Alerts, including all background detection and the actions they generate are authorized using an <> associated with the last user to edit the alert. Upon creating or modifying an alert, an API key is generated for that user, capturing a snapshot of their privileges at that moment in time. The API key is then used to run all background tasks associated with the alert including detection checks and executing actions. - -[IMPORTANT] -============================================== -If an alert requires certain privileges to run such as index privileges, keep in mind that if a user without those privileges updates the alert, the alert will no longer function. -============================================== - -[float] -[[alerting-restricting-actions]] -=== Restricting actions - -For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and whitelist the hostnames that {kib} can connect with. - --- - +include::alerting-getting-started.asciidoc[] include::defining-alerts.asciidoc[] include::action-types.asciidoc[] include::alert-types.asciidoc[] diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index a07d584b4391db..01be8c2e264c5c 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -6,7 +6,10 @@ include::getting-started.asciidoc[] include::setup.asciidoc[] -include::monitoring/configuring-monitoring.asciidoc[] +include::monitoring/configuring-monitoring.asciidoc[leveloffset=+1] +include::monitoring/monitoring-metricbeat.asciidoc[leveloffset=+2] +include::monitoring/viewing-metrics.asciidoc[leveloffset=+2] +include::monitoring/monitoring-kibana.asciidoc[leveloffset=+2] include::security/securing-kibana.asciidoc[] diff --git a/docs/user/monitoring/beats-details.asciidoc b/docs/user/monitoring/beats-details.asciidoc index f4ecb2a74d91ec..3d7a726d2f8a23 100644 --- a/docs/user/monitoring/beats-details.asciidoc +++ b/docs/user/monitoring/beats-details.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[beats-page]] -== Beats Monitoring Metrics += Beats Monitoring Metrics ++++ Beats Metrics ++++ diff --git a/docs/user/monitoring/cluster-alerts-license.asciidoc b/docs/user/monitoring/cluster-alerts-license.asciidoc deleted file mode 100644 index ec86b6f578e19e..00000000000000 --- a/docs/user/monitoring/cluster-alerts-license.asciidoc +++ /dev/null @@ -1,2 +0,0 @@ -NOTE: Watcher must be enabled to view cluster alerts. If you have a Basic -license, Top Cluster Alerts are not displayed. \ No newline at end of file diff --git a/docs/user/monitoring/cluster-alerts.asciidoc b/docs/user/monitoring/cluster-alerts.asciidoc index a58ccc7f7d68de..2945ebc67710cf 100644 --- a/docs/user/monitoring/cluster-alerts.asciidoc +++ b/docs/user/monitoring/cluster-alerts.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[cluster-alerts]] -== Cluster Alerts += Cluster Alerts The *Stack Monitoring > Clusters* page in {kib} summarizes the status of your {stack}. You can drill down into the metrics to view more information about your @@ -39,11 +39,12 @@ valid for 30 days. The {monitor-features} check the cluster alert conditions every minute. Cluster alerts are automatically dismissed when the condition is resolved. -include::cluster-alerts-license.asciidoc[] +NOTE: {watcher} must be enabled to view cluster alerts. If you have a Basic +license, Top Cluster Alerts are not displayed. [float] [[cluster-alert-email-notifications]] -==== Email Notifications +== Email Notifications To receive email notifications for the Cluster Alerts: . Configure an email account as described in diff --git a/docs/user/monitoring/configuring-monitoring.asciidoc b/docs/user/monitoring/configuring-monitoring.asciidoc index 7bcddcac923b20..f158dcc3eee6f4 100644 --- a/docs/user/monitoring/configuring-monitoring.asciidoc +++ b/docs/user/monitoring/configuring-monitoring.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[configuring-monitoring]] -== Configure monitoring in {kib} += Configure monitoring in {kib} ++++ Configure monitoring ++++ @@ -16,7 +16,3 @@ You can also use {kib} to To learn about monitoring in general, see {ref}/monitor-elasticsearch-cluster.html[Monitor a cluster]. - -include::monitoring-metricbeat.asciidoc[] -include::viewing-metrics.asciidoc[] -include::monitoring-kibana.asciidoc[] diff --git a/docs/user/monitoring/dashboards.asciidoc b/docs/user/monitoring/dashboards.asciidoc deleted file mode 100644 index 4ffe76f634d938..00000000000000 --- a/docs/user/monitoring/dashboards.asciidoc +++ /dev/null @@ -1,67 +0,0 @@ -[[dashboards]] -== Monitoring's Dashboards - -=== Overview Dashboard - -The _Overview_ dashboard is Monitoring's main page. The dashboard displays the -essentials metrics you need to know that your cluster is healthy. It also -provides an overview of your nodes and indices, displayed in two clean tables -with the relevant key metrics. If some value needs your attention, they will -be highlighted in yellow or red. The nodes and indices tables also serve as an -entry point to the more detailed _Node Statistics_ and _Index Statistics_ -dashboards. - -overview_thumb.png["Overview Dashboard",link="images/overview.png"] - -=== Node & Index Statistics - -The _Node Statistics_ dashboard displays metric charts from the perspective of -one or more nodes. Metrics include hardware level metrics (like load and CPU -usage), process and JVM metrics (memory usage, GC), and node level -Elasticsearch metrics such as field data usage, search requests rate and -thread pool rejection. - -node_stats_thumb.png["Node Statistics Dashboard",link="images/node_stats.png"] - -The _Index Statistics_ dashboard is very similar to the _Node Statistics_ -dashboard, but it shows you all the metrics from the perspective of one or -more indices. The metrics are per index, with data aggregated from all of the -nodes in the cluster. For example, the ''store size'' chart shows the total -size of the index data across the whole cluster. - -index_stats_thumb.png["Index Statistics Dashboard",link="images/index_stats.png"] - -=== Shard Allocation - -The _Shard Allocation_ dashboard displays how the shards are allocated across nodes. -The dashboard also shows the status of the shards. It has two perspectives, _By Indices_ and _By Nodes_. -The _By Indices_ view lists each index and shows you how it's shards are -distributed across nodes. The _By Nodes_ view lists each node and shows you which shards the node current host. - -The viewer also has a playback feature which allows you to view the history of the shard allocation. You can rewind to each -allocation event and then play back the history from any point in time. Hover on relocating shards to highlight both -their previous and new location. The time line color changes based on the state of the cluster for -each time period. - -shard_allocation_thumb.png["Shard Allocation Dashboard",link="images/shard_allocation.png"] - -=== Cluster Pulse - -The Cluster Pulse Dashboard allows you to see any event of interest in the cluster. Typical -events include nodes joining or leaving, master election, index creation, shard (re)allocation -and more. - -cluster_pulse_thumb.png["Index Statistics Dashboard",link="images/cluster_pulse.png"] - -[[sense]] -=== Sense - -_Sense_ is a lightweight developer console. The console is handy when you want -to make an extra API call to check something or perhaps tweak a setting. The -developer console understands both JSON and the Elasticsearch API, offering -suggestions and autocompletion. It is very useful for prototyping queries, -researching your data or any other administrative work with the API. - -image::images/sense_thumb.png["Developer Console",link="sense.png"] - - diff --git a/docs/user/monitoring/elasticsearch-details.asciidoc b/docs/user/monitoring/elasticsearch-details.asciidoc index 15e4676c443dfa..11a561e7ad01f5 100644 --- a/docs/user/monitoring/elasticsearch-details.asciidoc +++ b/docs/user/monitoring/elasticsearch-details.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[elasticsearch-metrics]] -== {es} Monitoring Metrics += {es} Monitoring Metrics [subs="attributes"] ++++ {es} Metrics @@ -18,7 +18,7 @@ See also {ref}/monitor-elasticsearch-cluster.html[Monitor a cluster]. [float] [[cluster-overview-page]] -==== Cluster Overview +== Cluster Overview To view the key metrics that indicate the overall health of an {es} cluster, click **Overview** in the {es} section. Anything that needs your attention is @@ -44,7 +44,7 @@ From there, you can dive into detailed metrics for particular nodes and indices. [float] [[nodes-page]] -==== Nodes +== Nodes To view node metrics, click **Nodes**. The Nodes section shows the status of each node in your cluster. @@ -54,7 +54,7 @@ image::user/monitoring/images/monitoring-nodes.png["Elasticsearch Nodes"] [float] [[nodes-page-overview]] -===== Node Overview +=== Node Overview Click the name of a node to view its node statistics over time. These represent high-level statistics collected from {es} that provide a good overview of @@ -66,7 +66,7 @@ image::user/monitoring/images/monitoring-node.png["Elasticsearch Node Overview"] [float] [[nodes-page-advanced]] -===== Node Advanced +=== Node Advanced To view advanced node metrics, click the **Advanced** tab for a node. The *Advanced* tab shows additional metrics, such as memory and garbage collection @@ -81,7 +81,7 @@ more advanced knowledge of {es}, such as poor garbage collection performance. [float] [[indices-overview-page]] -==== Indices +== Indices To view index metrics, click **Indices**. The Indices section shows the same overall index and search metrics as the Overview and a table of your indices. @@ -91,7 +91,7 @@ image::user/monitoring/images/monitoring-indices.png["Elasticsearch Indices"] [float] [[indices-page-overview]] -===== Index Overview +=== Index Overview From the Indices listing, you can view data for a particular index. To drill down into the data for a particular index, click its name in the Indices table. @@ -101,7 +101,7 @@ image::user/monitoring/images/monitoring-index.png["Elasticsearch Index Overview [float] [[indices-page-advanced]] -===== Index Advanced +=== Index Advanced To view advanced index metrics, click the **Advanced** tab for an index. The *Advanced* tab shows additional metrics, such as memory statistics reported @@ -116,7 +116,7 @@ more advanced knowledge of {es}, such as wasteful index memory usage. [float] [[jobs-page]] -==== Jobs +== Jobs To view {ml} job metrics, click **Jobs**. For each job in your cluster, it shows information such as its status, the number of records processed, the size of the @@ -127,7 +127,7 @@ image::user/monitoring/images/monitoring-jobs.png["Machine learning jobs",link=" [float] [[ccr-overview-page]] -==== CCR +== CCR To view {ccr} metrics, click **CCR**. For each follower index on the cluster, it shows information such as the leader index, an indication of how much the @@ -149,7 +149,7 @@ For more information, see {ref}/xpack-ccr.html[{ccr-cap}]. [float] [[logs-monitor-page]] -==== Logs +== Logs If you use {filebeat} to collect log data from your cluster, you can see its recent logs in the *Stack Monitoring* application. The *Clusters* page lists the diff --git a/docs/user/monitoring/gs-index.asciidoc b/docs/user/monitoring/gs-index.asciidoc deleted file mode 100644 index 69c523647393ca..00000000000000 --- a/docs/user/monitoring/gs-index.asciidoc +++ /dev/null @@ -1,31 +0,0 @@ -[[xpack-monitoring]] -= Monitoring the Elastic Stack - -[partintro] --- -The {monitoring} components enable you to easily monitor the Elastic Stack -from {kibana-ref}/introduction.html[Kibana]. -You can view health and performance data for {es}, Logstash, and {kib} in real -time, as well as analyze past performance. - -A monitoring agent runs on each {es}, {kib}, and Logstash instance to collect -and index metrics. - -By default, metrics are indexed within the cluster you are monitoring. -Setting up a dedicated monitoring cluster ensures you can access historical -monitoring data even if the cluster you're -monitoring goes down. It also enables you to monitor multiple clusters -from a central location. - -When you use a dedicated monitoring cluster, the metrics collected by the -Logstash and Kibana monitoring agents are shipped to the Elasticsearch -cluster you're monitoring, which then forwards all of the metrics to -the monitoring cluster. - -//   - -// image:monitoring-architecture.png["Monitoring Architecture",link="images/monitoring-architecture.png"] - --- - -include::getting-started.asciidoc[] diff --git a/docs/user/monitoring/index.asciidoc b/docs/user/monitoring/index.asciidoc index edc572a56434e3..ab773657073bad 100644 --- a/docs/user/monitoring/index.asciidoc +++ b/docs/user/monitoring/index.asciidoc @@ -1,33 +1,7 @@ -[role="xpack"] -[[xpack-monitoring]] -= Stack Monitoring - -[partintro] --- - -The {kib} {monitor-features} serve two separate purposes: - -. To visualize monitoring data from across the {stack}. You can view health and -performance data for {es}, {ls}, and Beats in real time, as well as analyze past -performance. -. To monitor {kib} itself and route that data to the monitoring cluster. - -If you enable monitoring across the {stack}, each {es} node, {ls} node, {kib} -instance, and Beat is considered unique based on its persistent -UUID, which is written to the <> directory when the node -or instance starts. - -NOTE: Watcher must be enabled to view cluster alerts. If you have a Basic -license, Top Cluster Alerts are not displayed. - -For more information, see <> and -{ref}/monitor-elasticsearch-cluster.html[Monitor a cluster]. - --- - -include::beats-details.asciidoc[] -include::cluster-alerts.asciidoc[] -include::elasticsearch-details.asciidoc[] -include::kibana-details.asciidoc[] -include::logstash-details.asciidoc[] -include::monitoring-troubleshooting.asciidoc[] +include::xpack-monitoring.asciidoc[] +include::beats-details.asciidoc[leveloffset=+1] +include::cluster-alerts.asciidoc[leveloffset=+1] +include::elasticsearch-details.asciidoc[leveloffset=+1] +include::kibana-details.asciidoc[leveloffset=+1] +include::logstash-details.asciidoc[leveloffset=+1] +include::monitoring-troubleshooting.asciidoc[leveloffset=+1] diff --git a/docs/user/monitoring/kibana-details.asciidoc b/docs/user/monitoring/kibana-details.asciidoc index 976ef456fcfa52..a5466f1418ae89 100644 --- a/docs/user/monitoring/kibana-details.asciidoc +++ b/docs/user/monitoring/kibana-details.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[kibana-page]] -== {kib} Monitoring Metrics += {kib} Monitoring Metrics [subs="attributes"] ++++ {kib} Metrics diff --git a/docs/user/monitoring/logstash-details.asciidoc b/docs/user/monitoring/logstash-details.asciidoc index 1433a6a036ca8a..9d7e3ce342e163 100644 --- a/docs/user/monitoring/logstash-details.asciidoc +++ b/docs/user/monitoring/logstash-details.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[logstash-page]] -== Logstash Monitoring Metrics += Logstash Monitoring Metrics ++++ Logstash Metrics ++++ diff --git a/docs/user/monitoring/monitoring-details.asciidoc b/docs/user/monitoring/monitoring-details.asciidoc deleted file mode 100644 index 580e02d86155a0..00000000000000 --- a/docs/user/monitoring/monitoring-details.asciidoc +++ /dev/null @@ -1,4 +0,0 @@ -[[monitoring-details]] -== Viewing Monitoring Metrics - -See {kibana-ref}/monitoring-data.html[Viewing Monitoring Data in {kib}]. diff --git a/docs/user/monitoring/monitoring-kibana.asciidoc b/docs/user/monitoring/monitoring-kibana.asciidoc index bb8b3e5d42851b..47fbe1bea9f2a6 100644 --- a/docs/user/monitoring/monitoring-kibana.asciidoc +++ b/docs/user/monitoring/monitoring-kibana.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[monitoring-kibana]] -=== Collect monitoring data using legacy collectors += Collect monitoring data using legacy collectors ++++ Legacy collection methods ++++ diff --git a/docs/user/monitoring/monitoring-metricbeat.asciidoc b/docs/user/monitoring/monitoring-metricbeat.asciidoc index f2b32ba1de5ddc..d18ebe95c7974d 100644 --- a/docs/user/monitoring/monitoring-metricbeat.asciidoc +++ b/docs/user/monitoring/monitoring-metricbeat.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[monitoring-metricbeat]] -=== Collect {kib} monitoring data with {metricbeat} += Collect {kib} monitoring data with {metricbeat} [subs="attributes"] ++++ Collect monitoring data with {metricbeat} diff --git a/docs/user/monitoring/monitoring-troubleshooting.asciidoc b/docs/user/monitoring/monitoring-troubleshooting.asciidoc index bdaa10990c3aa9..5bec56df0398b7 100644 --- a/docs/user/monitoring/monitoring-troubleshooting.asciidoc +++ b/docs/user/monitoring/monitoring-troubleshooting.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[monitor-troubleshooting]] -== Troubleshooting monitoring in {kib} += Troubleshooting monitoring in {kib} ++++ Troubleshooting ++++ @@ -9,7 +9,7 @@ Use the information in this section to troubleshoot common problems and find answers for frequently asked questions related to the {kib} {monitor-features}. [float] -=== Cannot view the cluster because the license information is invalid +== Cannot view the cluster because the license information is invalid *Symptoms:* @@ -24,7 +24,7 @@ To resolve this issue, upgrade {kib} to 6.3 or later. See {stack-ref}/upgrading-elastic-stack.html[Upgrading the {stack}]. [float] -=== {filebeat} index is corrupt +== {filebeat} index is corrupt *Symptoms:* @@ -41,7 +41,7 @@ text fields by default. [float] -=== No monitoring data is visible in {kib} +== No monitoring data is visible in {kib} *Symptoms:* diff --git a/docs/user/monitoring/viewing-metrics.asciidoc b/docs/user/monitoring/viewing-metrics.asciidoc index 6203565c3fe939..f35caea025cdd5 100644 --- a/docs/user/monitoring/viewing-metrics.asciidoc +++ b/docs/user/monitoring/viewing-metrics.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[monitoring-data]] -=== View monitoring data in {kib} += View monitoring data in {kib} ++++ View monitoring data ++++ diff --git a/docs/user/monitoring/xpack-monitoring.asciidoc b/docs/user/monitoring/xpack-monitoring.asciidoc new file mode 100644 index 00000000000000..c3aafe7f90db9c --- /dev/null +++ b/docs/user/monitoring/xpack-monitoring.asciidoc @@ -0,0 +1,26 @@ +[role="xpack"] +[[xpack-monitoring]] += Stack Monitoring + +[partintro] +-- + +The {kib} {monitor-features} serve two separate purposes: + +. To visualize monitoring data from across the {stack}. You can view health and +performance data for {es}, {ls}, and Beats in real time, as well as analyze past +performance. +. To monitor {kib} itself and route that data to the monitoring cluster. + +If you enable monitoring across the {stack}, each {es} node, {ls} node, {kib} +instance, and Beat is considered unique based on its persistent +UUID, which is written to the <> directory when the node +or instance starts. + +NOTE: Watcher must be enabled to view cluster alerts. If you have a Basic +license, Top Cluster Alerts are not displayed. + +For more information, see <> and +{ref}/monitor-elasticsearch-cluster.html[Monitor a cluster]. + +-- \ No newline at end of file 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 00000000000000..698a4eb889c8ca 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 00000000000000..027841af934d6a 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 00000000000000..8b9093be9b18d0 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 3a1c57da93f07e..9b8c32d7e41f09 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 51a41cbbab9ffc..880534997cff0c 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 c11bd1b6469332..4fbbc920c4447a 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 00000000000000..10602d2e7981a7 --- /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 571bae86dee371..7313de07be04cf 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 6197a084858548..b8f9b94379f200 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 a354da7a21521f..25b37ace09a8fb 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 109188e163d06f..5f44d8068e694f 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 a70cfc759dd55b..a823f66cf767b8 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 04ab992addeec1..9350b9464242af 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 c7be943d65a489..d78eb8214f607f 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 271ad49aee351c..ae5d2b5fb32922 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 e30920b9601444..a1f97c4314774b 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 740ee3819c36f5..661312b9a0581c 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 b2df4f40d4fbe9..0a9977463aee8c 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 b7ba1e87b2f001..bb8e7b72cb7bd7 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 abf64906e02539..7933ce06d6847a 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 8398d1c081da6d..3c03a52383f770 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 3fdab481dc7500..4facbe1ffbb07e 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 c93294404b52ff..2f2ca08fee6f20 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 3aa47e8b40e24e..c9366c575ba743 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 78ca8fcbd3c073..4288c6bf6421d7 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 1af7948a65e168..05ab67073f9e16 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 b97f6df6b0afcf..501ab619316c22 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 b9699ab290e3fc..896a58e085d49b 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 7a39113d25a14d..f8b2d39a4251c0 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 3b3b8da51a9075..de4d2739977bb7 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 9edb73645f0e2c..aee7a1daa81668 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 40399aecbc4466..88094af8047e7c 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 3df098a1df00d6..4345783e46e110 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 f46b41d6b87938..382318ea86a343 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 84e4b4741b717c..bf9dcc4abe01c1 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 c17b8df8bb52c0..5235f3ee6d580d 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 fb8fb4ef950811..0b3ad1b6e3cc89 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 78601d033f8d8b..b0669774207dd9 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 40c06677c4a5a5..a6da62095060cd 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 c3ed97a89af800..cc443093e30a34 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 b902179b012ff2..4a9fceb9bf3578 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 86a984fb671246..61df94fb6bfe2e 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 c94151f8cee179..c1054c27d084e4 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 2dae7f8f38f23a..aff749ca975342 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 bfcc98d6cd9a87..1d41f4c270caab 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/license_checker/config.ts b/src/dev/license_checker/config.ts index efc42405688d4e..60172a31062760 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -79,7 +79,7 @@ export const DEV_ONLY_LICENSE_WHITELIST = ['MPL-2.0']; // Globally overrides a license for a given package@version export const LICENSE_OVERRIDES = { - 'jsts@1.1.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts + 'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint // TODO can be removed if the https://github.com/jindw/xmldom/issues/239 is released diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 864bf7515053c6..404ad671746815 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 41628a22641931..74973887ae9c1e 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 7ad2f9edd33254..d35a6a5bbb9a99 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 d598f28a0ad120..0c1a44d7845cf1 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 4508735f39ff90..e2911eb2d70e3b 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 eda49ccdca1783..18380680283ef5 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 59256d47de97c6..6335aeaf217a1b 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 4fdd68f9e9dbe9..1aac8e25d5c738 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 18387a6ab08765..a213b59be2ad0e 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 9876faf0fc88f4..b830b24c92082b 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 e29e16e3212f43..62563dce2a18d5 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 9ac8f72d56d4af..132b51765b3edd 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 782ea271869ba4..d024943bef792a 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/context/_date_nanos.js b/test/functional/apps/context/_date_nanos.js index cdf2d6c04be83c..89769caaea2536 100644 --- a/test/functional/apps/context/_date_nanos.js +++ b/test/functional/apps/context/_date_nanos.js @@ -30,7 +30,8 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); - describe('context view for date_nanos', () => { + // FLAKY/FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/58815 + describe.skip('context view for date_nanos', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos']); await esArchiver.loadIfNeeded('date_nanos'); diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.js index dbfb77c31dff17..6329f6c431e6af 100644 --- a/test/functional/apps/context/_date_nanos_custom_timestamp.js +++ b/test/functional/apps/context/_date_nanos_custom_timestamp.js @@ -29,8 +29,10 @@ export default function ({ getService, getPageObjects }) { const security = getService('security'); const PageObjects = getPageObjects(['common', 'context', 'timePicker', 'discover']); const esArchiver = getService('esArchiver'); + // skipped due to a recent change in ES that caused search_after queries with data containing // custom timestamp formats like in the testdata to fail + // https://github.com/elastic/kibana/issues/58815 describe.skip('context view for date_nanos with custom timestamp', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos_custom']); diff --git a/test/functional/apps/discover/_field_visualize.ts b/test/functional/apps/discover/_field_visualize.ts index e202dcb7e2af77..b0db6c149e41aa 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/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 8f69bf629ce285..c558d9e2d8a31f 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -17,11 +17,9 @@ * under the License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function DiscoverPageProvider({ getService, getPageObjects }: FtrProviderContext) { - const log = getService('log'); const retry = getService('retry'); const testSubjects = getService('testSubjects'); const find = getService('find'); @@ -51,9 +49,16 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider } public async saveSearch(searchName: string) { - log.debug('saveSearch'); await this.clickSaveSearchButton(); - await testSubjects.setValue('savedObjectTitle', searchName); + // preventing an occasional flakiness when the saved object wasn't set and the form can't be submitted + await retry.waitFor( + `saved search title is set to ${searchName} and save button is clickable`, + async () => { + const saveButton = await testSubjects.find('confirmSaveSavedObjectButton'); + await testSubjects.setValue('savedObjectTitle', searchName); + return (await saveButton.getAttribute('disabled')) !== 'true'; + } + ); await testSubjects.click('confirmSaveSavedObjectButton'); await header.waitUntilLoadingHasFinished(); // LeeDr - this additional checking for the saved search name was an attempt @@ -61,9 +66,8 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider // that the next action wouldn't have to retry. But it doesn't really solve // that issue. But it does typically take about 3 retries to // complete with the expected searchName. - await retry.try(async () => { - const name = await this.getCurrentQueryName(); - expect(name).to.be(searchName); + await retry.waitFor(`saved search was persisted with name ${searchName}`, async () => { + return (await this.getCurrentQueryName()) === searchName; }); } @@ -96,11 +100,11 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider // We need this try loop here because previous actions in Discover like // saving a search cause reloading of the page and the "Open" menu item goes stale. - await retry.try(async () => { + await retry.waitFor('saved search panel is opened', async () => { await this.clickLoadSavedSearchButton(); await header.waitUntilLoadingHasFinished(); isOpen = await testSubjects.exists('loadSearchForm'); - expect(isOpen).to.be(true); + return isOpen === true; }); } diff --git a/test/plugin_functional/README.md b/test/plugin_functional/README.md index 476c08408658c5..075d321917c395 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 f51fb5e1bade4c..c611300eade103 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 00000000000000..a7674881e8ba02 --- /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 00000000000000..02c2955f26c863 --- /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 10b60df0aa35c8..3801e33a2cf3e6 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 00000000000000..5e018ca7818d39 --- /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 00000000000000..d0751f31ecc5ea --- /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 dd2b25e14fe170..453cfc5a8636ff 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 a4c2db733b8949..f56a6e8d62fb12 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 00000000000000..9b9efc261126f3 --- /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 8f54ec6c0f4cd9..99ac6dc9b84743 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 5c46e3d7f76db1..6d2c6b7f85d28a 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 3a9b3ca606de6c..e3104aabbb02bf 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", @@ -212,8 +218,12 @@ "@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@scant/router": "^0.1.0", "@slack/webhook": "^5.0.0", + "@turf/bbox": "6.0.1", + "@turf/bbox-polygon": "6.0.1", "@turf/boolean-contains": "6.0.1", "@turf/circle": "6.0.1", + "@turf/distance": "6.0.1", + "@turf/helpers": "6.0.1", "angular": "^1.7.9", "angular-resource": "1.7.9", "angular-sanitize": "1.7.9", @@ -308,7 +318,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", @@ -362,7 +371,6 @@ "tinymath": "1.2.1", "topojson-client": "3.0.0", "tslib": "^2.0.0", - "turf": "3.0.14", "typescript-fsa": "^3.0.0", "typescript-fsa-reducers": "^1.2.1", "ui-select": "0.19.8", diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index 9e0a3e3d0d889a..07270b572a4bee 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/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index b09c03f853aa9f..c6c0861c26a345 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -83,7 +83,8 @@ interface Props { } export function Home({ tab }: Props) { - const { config } = useApmPluginContext(); + const { config, core } = useApmPluginContext(); + const isMLEnabled = !!core.application.capabilities.ml; const homeTabs = getHomeTabs(config); const selectedTab = homeTabs.find( (homeTab) => homeTab.name === tab @@ -105,9 +106,11 @@ export function Home({ tab }: Props) { - - - + {isMLEnabled && ( + + + + )} 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 cb2090d1cbe2b3..a594edb32b083c 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'; @@ -64,8 +64,8 @@ export function AddEnvironments({ return ( {MLErrorMessages.MISSING_WRITE_PRIVILEGES}} + iconType="alert" + 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 acea38732b40a2..2e2c2ccbad7cf3 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 dab30761c6ebef..9c04caf61022a3 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,12 +25,11 @@ export type AnomalyDetectionApiResponse = APIReturnType< const DEFAULT_VALUE: AnomalyDetectionApiResponse = { jobs: [], hasLegacyJobs: false, - errorCode: undefined, }; export function AnomalyDetection() { const plugin = useApmPluginContext(); - const canGetJobs = !!plugin.core.application.capabilities.ml.canGetJobs; + const canGetJobs = !!plugin.core.application.capabilities.ml?.canGetJobs; const license = useLicense(); const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); @@ -49,15 +48,7 @@ export function AnomalyDetection() { if (!hasValidLicense) { return ( - + ); } @@ -66,8 +57,8 @@ export function AnomalyDetection() { return ( {MLErrorMessages.MISSING_READ_PRIVILEGES}} + iconType="alert" + 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 8494004ae56399..05ea585108c691 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/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index bd2ea706e492dd..1471bc345d850a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -16,8 +16,11 @@ import { import { HomeLink } from '../../shared/Links/apm/HomeLink'; import { useLocation } from '../../../hooks/useLocation'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; export function Settings(props: { children: ReactNode }) { + const plugin = useApmPluginContext(); + const isMLEnabled = !!plugin.core.application.capabilities.ml; const { search, pathname } = useLocation(); return ( <> @@ -48,17 +51,25 @@ export function Settings(props: { children: ReactNode }) { '/settings/agent-configuration' ), }, - { - name: i18n.translate( - 'xpack.apm.settings.anomalyDetection', - { - defaultMessage: 'Anomaly detection', - } - ), - id: '4', - href: getAPMHref('/settings/anomaly-detection', search), - isSelected: pathname === '/settings/anomaly-detection', - }, + ...(isMLEnabled + ? [ + { + name: i18n.translate( + 'xpack.apm.settings.anomalyDetection', + { + defaultMessage: 'Anomaly detection', + } + ), + id: '4', + href: getAPMHref( + '/settings/anomaly-detection', + search + ), + isSelected: + pathname === '/settings/anomaly-detection', + }, + ] + : []), { name: i18n.translate('xpack.apm.settings.customizeApp', { defaultMessage: 'Customize app', diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx index 359468073f7f45..ca02abc3959929 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx @@ -33,8 +33,8 @@ describe('DiscoverLinks', () => { } as Location ); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'processor.event:"transaction" AND transaction.id:"8b60bd32ecc6e150" AND trace.id:"8b60bd32ecc6e1506735a8b6cfcf175c"'))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'processor.event:\\"transaction\\" AND transaction.id:\\"8b60bd32ecc6e150\\" AND trace.id:\\"8b60bd32ecc6e1506735a8b6cfcf175c\\"'))"` ); }); @@ -50,8 +50,8 @@ describe('DiscoverLinks', () => { '?rangeFrom=now/w&rangeTo=now&refreshPaused=true&refreshInterval=0', } as Location); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'span.id:"test-span-id"'))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'span.id:\\"test-span-id\\"'))"` ); }); @@ -72,8 +72,8 @@ describe('DiscoverLinks', () => { } as Location ); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:"service-name" AND error.grouping_key:"grouping-key"'),sort:('@timestamp':desc))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:\\"service-name\\" AND error.grouping_key:\\"grouping-key\\"'),sort:('@timestamp':desc))"` ); }); @@ -95,8 +95,8 @@ describe('DiscoverLinks', () => { } as Location ); - expect(href).toEqual( - `/basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:"service-name" AND error.grouping_key:"grouping-key" AND some:kuery-string'),sort:('@timestamp':desc))` + expect(href).toMatchInlineSnapshot( + `"/basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:'service.name:\\"service-name\\" AND error.grouping_key:\\"grouping-key\\" AND some:kuery-string'),sort:('@timestamp':desc))"` ); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx index 39082c2639a2cf..9ba4aab0e23d9f 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx @@ -22,7 +22,7 @@ describe('MLJobLink', () => { ); expect(href).toMatchInlineSnapshot( - `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))"` + `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now-4h))"` ); }); it('should produce the correct URL with jobId, serviceName, and transactionType', async () => { @@ -41,7 +41,7 @@ describe('MLJobLink', () => { ); expect(href).toMatchInlineSnapshot( - `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request)))"` + `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request),zoom:(from:now/w,to:now-4h)))"` ); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx index b4187b2f797aba..da345e35c10b13 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx @@ -21,6 +21,6 @@ test('MLLink produces the correct URL', async () => { ); expect(href).toMatchInlineSnapshot( - `"/basepath/app/ml#/some/path?_g=(ml:(jobIds:!(something)),refreshInterval:(pause:true,value:'0'),time:(from:now-5h,to:now-2h))"` + `"/basepath/app/ml#/some/path?_g=(ml:(jobIds:!(something)),refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` ); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts new file mode 100644 index 00000000000000..28daae7fd830e3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts @@ -0,0 +1,34 @@ +/* + * 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 { useTimeSeriesExplorerHref } from './useTimeSeriesExplorerHref'; + +jest.mock('../../../../hooks/useApmPluginContext', () => ({ + useApmPluginContext: () => ({ + core: { http: { basePath: { prepend: (url: string) => url } } }, + }), +})); + +jest.mock('../../../../hooks/useLocation', () => ({ + useLocation: () => ({ + search: + '?rangeFrom=2020-07-29T17:27:29.000Z&rangeTo=2020-07-29T18:45:00.000Z&refreshInterval=10000&refreshPaused=true', + }), +})); + +describe('useTimeSeriesExplorerHref', () => { + it('correctly encodes time range values', async () => { + const href = useTimeSeriesExplorerHref({ + jobId: 'apm-production-485b-high_mean_transaction_duration', + serviceName: 'opbeans-java', + transactionType: 'request', + }); + + expect(href).toMatchInlineSnapshot( + `"/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(apm-production-485b-high_mean_transaction_duration)),refreshInterval:(pause:!t,value:10000),time:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z'))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-java,transaction.type:request),zoom:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z')))"` + ); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts index 625b9205b6ce01..459ee8f0282ff1 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts @@ -22,12 +22,14 @@ export function useTimeSeriesExplorerHref({ }) { const { core } = useApmPluginContext(); const location = useLocation(); + const { time, refreshInterval } = getTimepickerRisonData(location.search); const search = querystring.stringify( { _g: rison.encode({ ml: { jobIds: [jobId] }, - ...getTimepickerRisonData(location.search), + time, + refreshInterval, }), ...(serviceName && transactionType ? { @@ -37,6 +39,7 @@ export function useTimeSeriesExplorerHref({ 'service.name': serviceName, 'transaction.type': transactionType, }, + zoom: time, }, }), } 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 2149cb676f0d86..585ab22b5fb278 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 e989244d431481..a80dcca4a107db 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/public/components/shared/Links/rison_helpers.test.ts b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.test.ts new file mode 100644 index 00000000000000..8dd662339b61a2 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.test.ts @@ -0,0 +1,27 @@ +/* + * 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 { getTimepickerRisonData } from './rison_helpers'; + +describe('getTimepickerRisonData', () => { + it('returns object of timepicker range and refresh interval values', async () => { + const locationSearch = `?rangeFrom=2020-07-29T17:27:29.000Z&rangeTo=2020-07-29T18:45:00.000Z&refreshInterval=10000&refreshPaused=true`; + const timepickerValues = getTimepickerRisonData(locationSearch); + + expect(timepickerValues).toMatchInlineSnapshot(` + Object { + "refreshInterval": Object { + "pause": true, + "value": 10000, + }, + "time": Object { + "from": "2020-07-29T17:27:29.000Z", + "to": "2020-07-29T18:45:00.000Z", + }, + } + `); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts index 8b4d891dba83b9..cab822b42be561 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts @@ -22,18 +22,16 @@ export function getTimepickerRisonData(currentSearch: Location['search']) { const currentQuery = toQuery(currentSearch); return { time: { - from: currentQuery.rangeFrom - ? encodeURIComponent(currentQuery.rangeFrom) - : '', - to: currentQuery.rangeTo ? encodeURIComponent(currentQuery.rangeTo) : '', + from: currentQuery.rangeFrom || '', + to: currentQuery.rangeTo || '', }, refreshInterval: { pause: currentQuery.refreshPaused - ? String(currentQuery.refreshPaused) - : '', + ? Boolean(currentQuery.refreshPaused) + : true, value: currentQuery.refreshInterval - ? String(currentQuery.refreshInterval) - : '', + ? parseInt(currentQuery.refreshInterval, 10) + : 0, }, }; } diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts index 186fc082ce5fe1..e057e9c034615e 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts @@ -68,7 +68,7 @@ describe('Transaction action menu', () => { key: 'sampleDocument', label: 'View sample document', href: - 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', condition: true, }, ], @@ -139,7 +139,7 @@ describe('Transaction action menu', () => { key: 'sampleDocument', label: 'View sample document', href: - 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', condition: true, }, ], @@ -209,7 +209,7 @@ describe('Transaction action menu', () => { key: 'sampleDocument', label: 'View sample document', href: - 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:true,value:\'0\'),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', + 'some-basepath/app/discover#/?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(index:apm_static_index_pattern_id,interval:auto,query:(language:kuery,query:\'processor.event:"transaction" AND transaction.id:"123" AND trace.id:"123"\'))', condition: true, }, ], 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 993dcf4c5354bf..00000000000000 --- 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 e5338ac9f57975..7bcd945d890ad3 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 62d4243a060285..05f41cdfdffd42 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 999d28309121af..ed66236726b9f8 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 3e5ef5eb37b023..03716382af8593 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 ea2bb14efdfc77..cd125f944f8a54 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 218d47fcf9bb45..ac25f22751f2f5 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 b1815e88d29178..c95719da881ea8 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/canvas_plugin_src/renderers/shape/index.js b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/index.js index 02c86afd7182b5..5684c8c4602b5b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/index.js @@ -75,6 +75,7 @@ export const shape = () => ({ domNode.removeChild(oldShape); } + domNode.style.lineHeight = 0; domNode.appendChild(shapeSvg); }; diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx index 7939c1d04631aa..c5fe7074fea0bf 100644 --- a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx +++ b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx @@ -5,7 +5,6 @@ */ import React, { useState, useEffect, useRef, FC, useCallback } from 'react'; -import { useDebounce } from 'react-use'; import { useNotifyService } from '../../services'; import { RenderToDom } from '../render_to_dom'; @@ -73,7 +72,7 @@ export const RenderWithFn: FC = ({ firstRender.current = true; }, [domNode]); - useDebounce(() => handlers.current.resize({ height, width }), 150, [height, width]); + useEffect(() => handlers.current.resize({ height, width }), [height, width]); useEffect( () => () => { diff --git a/x-pack/plugins/canvas/public/components/text_style_picker/__stories__/text_style_picker.stories.tsx b/x-pack/plugins/canvas/public/components/text_style_picker/__stories__/text_style_picker.stories.tsx index b33a34fcd5e65f..7613c834bfc02c 100644 --- a/x-pack/plugins/canvas/public/components/text_style_picker/__stories__/text_style_picker.stories.tsx +++ b/x-pack/plugins/canvas/public/components/text_style_picker/__stories__/text_style_picker.stories.tsx @@ -8,11 +8,15 @@ import React, { useState } from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { TextStylePicker } from '../text_style_picker'; +import { TextStylePicker, StyleProps } from '../text_style_picker'; const Interactive = () => { - const [props, setProps] = useState({}); - return ; + const [style, setStyle] = useState({}); + const onChange = (styleChange: StyleProps) => { + setStyle(styleChange); + action('onChange')(styleChange); + }; + return ; }; storiesOf('components/TextStylePicker', module) diff --git a/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx b/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx index 3dfc55919395d4..c501e78a5e338a 100644 --- a/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx +++ b/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState, useEffect } from 'react'; +import React, { FC, useState } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiSelect, EuiSpacer, EuiButtonGroup } from '@elastic/eui'; import { FontValue } from 'src/plugins/expressions'; @@ -15,7 +15,7 @@ import { fontSizes } from './font_sizes'; const { TextStylePicker: strings } = ComponentStrings; -interface BaseProps { +export interface StyleProps { family?: FontValue; size?: number; align?: 'left' | 'center' | 'right'; @@ -25,9 +25,9 @@ interface BaseProps { italic?: boolean; } -interface Props extends BaseProps { +export interface Props extends StyleProps { colors?: string[]; - onChange: (props: BaseProps) => void; + onChange: (style: StyleProps) => void; } type StyleType = 'bold' | 'italic' | 'underline'; @@ -68,20 +68,26 @@ const styleButtons = [ }, ]; -export const TextStylePicker: FC = (props) => { - const [style, setStyle] = useState(props); - - const { - align = 'left', +export const TextStylePicker: FC = ({ + align = 'left', + color, + colors, + family, + italic = false, + onChange, + size = 14, + underline = false, + weight = 'normal', +}) => { + const [style, setStyle] = useState({ + align, color, - colors, family, - italic = false, - onChange, - size = 14, - underline = false, - weight = 'normal', - } = style; + italic, + size, + underline, + weight, + }); const stylesSelectedMap: Record = { ['bold']: weight === 'bold', @@ -94,10 +100,10 @@ export const TextStylePicker: FC = (props) => { fontSizes.sort((a, b) => a - b); } - useEffect(() => onChange(style), [onChange, style]); - - const doChange = (propName: keyof Props, value: string | boolean | number) => { - setStyle({ ...style, [propName]: value }); + const doChange = (propName: keyof StyleProps, value: string | boolean | number) => { + const newStyle = { ...style, [propName]: value }; + setStyle(newStyle); + onChange(newStyle); }; const onStyleChange = (optionId: string) => { diff --git a/x-pack/plugins/canvas/shareable_runtime/postcss.config.js b/x-pack/plugins/canvas/shareable_runtime/postcss.config.js index 10baaddfc9b053..e1db6e4a64f710 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 93dc3dbccd549e..43e422a1615696 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 927f71b832ba05..982185a731b149 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 0e9371e4cb5e45..81d19c035075f7 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/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx index f3136ca155c788..d5b50fce387183 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx @@ -194,7 +194,7 @@ export const AlertPreview: React.FC = (props) => { plural: previewResult.resultTotals.noData !== 1 ? 's' : '', }} /> - ) : null} + ) : null}{' '} {previewResult.resultTotals.error ? ( { const [popoverOpen, setPopoverOpen] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); - const kibana = useKibana(); const { inventoryPrefill } = useAlertPrefillContext(); const { nodeType, metric, filterQuery } = inventoryPrefill; @@ -27,26 +26,12 @@ export const InventoryAlertDropdown = () => { setPopoverOpen(true); }, [setPopoverOpen]); - const menuItems = useMemo(() => { - return [ - setFlyoutVisible(true)}> - - , - - - , - ]; - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [kibana.services]); + const menuItems = [ + setFlyoutVisible(true)}> + + , + , + ]; return ( <> diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index 583cbe18ee9db9..b69078beec670a 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -368,6 +368,7 @@ export const Expressions: React.FC = (props) => { validate={validateMetricThreshold} fetch={alertsContext.http.fetch} groupByDisplayName={alertParams.nodeType} + showNoDataResults={alertParams.alertOnNoData} /> diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx new file mode 100644 index 00000000000000..fc565aee37ff48 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx @@ -0,0 +1,22 @@ +/* + * 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 { EuiContextMenuItem } from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useLinkProps } from '../../../hooks/use_link_props'; + +export const ManageAlertsContextMenuItem = () => { + const manageAlertsLinkProps = useLinkProps({ + app: 'management', + pathname: '/insightsAndAlerting/triggersActions/alerts', + }); + return ( + + + + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx index 384a93e796dbe3..dd61be0eee3627 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx @@ -4,17 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useCallback, useMemo } from 'react'; +import React, { useState, useCallback } from 'react'; import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useAlertPrefillContext } from '../../use_alert_prefill'; import { AlertFlyout } from './alert_flyout'; +import { ManageAlertsContextMenuItem } from '../../inventory/components/manage_alerts_context_menu_item'; export const MetricsAlertDropdown = () => { const [popoverOpen, setPopoverOpen] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); - const kibana = useKibana(); const { metricThresholdPrefill } = useAlertPrefillContext(); const { groupBy, filterQuery, metrics } = metricThresholdPrefill; @@ -27,26 +26,12 @@ export const MetricsAlertDropdown = () => { setPopoverOpen(true); }, [setPopoverOpen]); - const menuItems = useMemo(() => { - return [ - setFlyoutVisible(true)}> - - , - - - , - ]; - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [kibana.services]); + const menuItems = [ + setFlyoutVisible(true)}> + + , + , + ]; return ( <> diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index cd1e93a2a0c96a..8bb8b3934b5fdf 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -185,7 +185,7 @@ export const Expressions: React.FC = (props) => { const preFillAlertCriteria = useCallback(() => { const md = alertsContext.metadata; - if (md && md.currentOptions?.metrics) { + if (md?.currentOptions?.metrics?.length) { setAlertParams( 'criteria', md.currentOptions.metrics.map((metric) => ({ @@ -249,13 +249,18 @@ export const Expressions: React.FC = (props) => { if (!alertParams.sourceId) { setAlertParams('sourceId', source?.id || 'default'); } - }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + }, [alertsContext.metadata, source]); // eslint-disable-line react-hooks/exhaustive-deps const handleFieldSearchChange = useCallback( (e: ChangeEvent) => onFilterChange(e.target.value), [onFilterChange] ); + const groupByPreviewDisplayName = useMemo(() => { + if (Array.isArray(alertParams.groupBy)) return alertParams.groupBy.join(', '); + return alertParams.groupBy; + }, [alertParams.groupBy]); + return ( <> @@ -400,7 +405,7 @@ export const Expressions: React.FC = (props) => { showNoDataResults={alertParams.alertOnNoData} validate={validateMetricThreshold} fetch={alertsContext.http.fetch} - groupByDisplayName={alertParams.groupBy} + groupByDisplayName={groupByPreviewDisplayName} /> diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index cdb6b341c7299c..c90c534193fdce 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -45,7 +45,7 @@ interface Props { derivedIndexPattern: IIndexPattern; source: InfraSource | null; filterQuery?: string; - groupBy?: string; + groupBy?: string | string[]; } const tooltipProps = { diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts index 185895062cfe29..a3d09742e9a57c 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts @@ -19,7 +19,7 @@ export const useMetricsExplorerChartData = ( derivedIndexPattern: IIndexPattern, source: InfraSource | null, filterQuery?: string, - groupBy?: string + groupBy?: string | string[] ) => { const { timeSize, timeUnit } = expression || { timeSize: 1, timeUnit: 'm' }; const options: MetricsExplorerOptions = useMemo( diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts index 58586c1dd8b98b..b2317c558be44f 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts @@ -53,7 +53,7 @@ export interface ExpressionChartData { export interface AlertParams { criteria: MetricExpression[]; - groupBy?: string; + groupBy?: string[]; filterQuery?: string; sourceId?: string; filterQueryText?: string; 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 3716e618068a39..02d5a415eec2e4 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 a2b4e162756e31..ea5c374c47d6e0 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/infra/server/lib/alerting/common/messages.ts b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts index 4add0ee9af5d3c..18b1460d643d5e 100644 --- a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts +++ b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts @@ -29,6 +29,9 @@ export const stateToAlertMessage = { }), }; +const toNumber = (value: number | string) => + typeof value === 'string' ? parseFloat(value) : value; + const comparatorToI18n = (comparator: Comparator, threshold: number[], currentValue: number) => { const gtText = i18n.translate('xpack.infra.metrics.alerting.threshold.gtComparator', { defaultMessage: 'greater than', @@ -54,10 +57,11 @@ const comparatorToI18n = (comparator: Comparator, threshold: number[], currentVa case Comparator.LT: return ltText; case Comparator.GT_OR_EQ: - case Comparator.LT_OR_EQ: + case Comparator.LT_OR_EQ: { if (threshold[0] === currentValue) return eqText; else if (threshold[0] < currentValue) return ltText; return gtText; + } } }; @@ -88,7 +92,7 @@ const recoveredComparatorToI18n = ( } }; -const thresholdToI18n = ([a, b]: number[]) => { +const thresholdToI18n = ([a, b]: Array) => { if (typeof b === 'undefined') return a; return i18n.translate('xpack.infra.metrics.alerting.threshold.thresholdRange', { defaultMessage: '{a} and {b}', @@ -99,15 +103,15 @@ const thresholdToI18n = ([a, b]: number[]) => { export const buildFiredAlertReason: (alertResult: { metric: string; comparator: Comparator; - threshold: number[]; - currentValue: number; + threshold: Array; + currentValue: number | string; }) => string = ({ metric, comparator, threshold, currentValue }) => i18n.translate('xpack.infra.metrics.alerting.threshold.firedAlertReason', { defaultMessage: '{metric} is {comparator} a threshold of {threshold} (current value is {currentValue})', values: { metric, - comparator: comparatorToI18n(comparator, threshold, currentValue), + comparator: comparatorToI18n(comparator, threshold.map(toNumber), toNumber(currentValue)), threshold: thresholdToI18n(threshold), currentValue, }, @@ -116,15 +120,19 @@ export const buildFiredAlertReason: (alertResult: { export const buildRecoveredAlertReason: (alertResult: { metric: string; comparator: Comparator; - threshold: number[]; - currentValue: number; + threshold: Array; + currentValue: number | string; }) => string = ({ metric, comparator, threshold, currentValue }) => i18n.translate('xpack.infra.metrics.alerting.threshold.recoveredAlertReason', { defaultMessage: '{metric} is now {comparator} a threshold of {threshold} (current value is {currentValue})', values: { metric, - comparator: recoveredComparatorToI18n(comparator, threshold, currentValue), + comparator: recoveredComparatorToI18n( + comparator, + threshold.map(toNumber), + toNumber(currentValue) + ), threshold: thresholdToI18n(threshold), currentValue, }, @@ -150,3 +158,56 @@ export const buildErrorAlertReason = (metric: string) => metric, }, }); + +export const groupActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.groupActionVariableDescription', + { + defaultMessage: 'Name of the group reporting data', + } +); + +export const alertStateActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.alertStateActionVariableDescription', + { + defaultMessage: 'Current state of the alert', + } +); + +export const reasonActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.reasonActionVariableDescription', + { + defaultMessage: + 'A description of why the alert is in this state, including which metrics have crossed which thresholds', + } +); + +export const timestampActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.timestampDescription', + { + defaultMessage: 'A timestamp of when the alert was detected.', + } +); + +export const valueActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.valueActionVariableDescription', + { + defaultMessage: + 'The value of the metric in the specified condition. Usage: (ctx.value.condition0, ctx.value.condition1, etc...).', + } +); + +export const metricActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.metricActionVariableDescription', + { + defaultMessage: + 'The metric name in the specified condition. Usage: (ctx.metric.condition0, ctx.metric.condition1, etc...).', + } +); + +export const thresholdActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.thresholdActionVariableDescription', + { + defaultMessage: + 'The threshold value of the metric for the specified condition. Usage: (ctx.threshold.condition0, ctx.threshold.condition1, etc...).', + } +); diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index 5c31c78b10fa9a..3b795810b39f09 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -23,9 +23,9 @@ import { InfraSourceConfiguration } from '../../sources'; import { UNGROUPED_FACTORY_KEY } from '../common/utils'; type ConditionResult = InventoryMetricConditions & { - shouldFire: boolean | boolean[]; + shouldFire: boolean[]; currentValue: number; - isNoData: boolean; + isNoData: boolean[]; isError: boolean; }; @@ -71,8 +71,8 @@ export const evaluateCondition = async ( value !== null && (Array.isArray(value) ? value.map((v) => comparisonFunction(Number(v), threshold)) - : comparisonFunction(value as number, threshold)), - isNoData: value === null, + : [comparisonFunction(value as number, threshold)]), + isNoData: Array.isArray(value) ? value.map((v) => v === null) : [value === null], isError: value === undefined, currentValue: getCurrentValue(value), }; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 0a3910f2c5d7c5..7b816f2f225b53 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { first, get } from 'lodash'; +import { first, get, last } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; @@ -56,11 +56,14 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = for (const item of inventoryItems) { const alertInstance = services.alertInstanceFactory(`${item}`); // AND logic; all criteria must be across the threshold - const shouldAlertFire = results.every((result) => result[item].shouldFire); + const shouldAlertFire = results.every((result) => + // Grab the result of the most recent bucket + last(result[item].shouldFire) + ); // AND logic; because we need to evaluate all criteria, if one of them reports no data then the // whole alert is in a No Data/Error state - const isNoData = results.some((result) => result[item].isNoData); + const isNoData = results.some((result) => last(result[item].isNoData)); const isError = results.some((result) => result[item].isError); const nextState = isError @@ -79,6 +82,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = const resultWithVerboseMetricName = { ...result[item], metric: toMetricOpt(result[item].metric)?.text || result[item].metric, + currentValue: formatMetric(result[item].metric, result[item].currentValue), }; return buildFiredAlertReason(resultWithVerboseMetricName); }) diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts index 5c654e2f47e783..562f344dbd0601 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts @@ -59,28 +59,29 @@ export const previewInventoryMetricThresholdAlert = async ({ const inventoryItems = Object.keys(first(results) as any); const previewResults = inventoryItems.map((item) => { - const isNoData = results.some((result) => result[item].isNoData); - if (isNoData) { - return null; - } - const isError = results.some((result) => result[item].isError); - if (isError) { - return undefined; - } - const numberOfResultBuckets = lookbackSize; const numberOfExecutionBuckets = Math.floor(numberOfResultBuckets / alertResultsPerExecution); - return [...Array(numberOfExecutionBuckets)].reduce( - (totalFired, _, i) => - totalFired + - (results.every((result) => { - const shouldFire = result[item].shouldFire as boolean[]; - return shouldFire[Math.floor(i * alertResultsPerExecution)]; - }) - ? 1 - : 0), - 0 - ); + let numberOfTimesFired = 0; + let numberOfNoDataResults = 0; + let numberOfErrors = 0; + for (let i = 0; i < numberOfExecutionBuckets; i++) { + const mappedBucketIndex = Math.floor(i * alertResultsPerExecution); + const allConditionsFiredInMappedBucket = results.every((result) => { + const shouldFire = result[item].shouldFire as boolean[]; + return shouldFire[mappedBucketIndex]; + }); + const someConditionsNoDataInMappedBucket = results.some((result) => { + const hasNoData = result[item].isNoData as boolean[]; + return hasNoData[mappedBucketIndex]; + }); + const someConditionsErrorInMappedBucket = results.some((result) => { + return result[item].isError; + }); + if (allConditionsFiredInMappedBucket) numberOfTimesFired++; + if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++; + if (someConditionsErrorInMappedBucket) numberOfErrors++; + } + return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors]; }); return previewResults; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts index fa5277cb09987b..f664a59acd165e 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { createInventoryMetricThresholdExecutor, @@ -12,6 +11,15 @@ import { import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; import { InfraBackendLibs } from '../../infra_types'; import { oneOfLiterals, validateIsStringElasticsearchJSONFilter } from '../common/utils'; +import { + groupActionVariableDescription, + alertStateActionVariableDescription, + reasonActionVariableDescription, + timestampActionVariableDescription, + valueActionVariableDescription, + metricActionVariableDescription, + thresholdActionVariableDescription, +} from '../common/messages'; const condition = schema.object({ threshold: schema.arrayOf(schema.number()), @@ -44,45 +52,13 @@ export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs executor: createInventoryMetricThresholdExecutor(libs), actionVariables: { context: [ - { - name: 'group', - description: i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.groupActionVariableDescription', - { - defaultMessage: 'Name of the group reporting data', - } - ), - }, - { - name: 'valueOf', - description: i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.valueOfActionVariableDescription', - { - defaultMessage: - 'Record of the current value of the watched metric; grouped by condition, i.e valueOf.condition0, valueOf.condition1, etc.', - } - ), - }, - { - name: 'thresholdOf', - description: i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.thresholdOfActionVariableDescription', - { - defaultMessage: - 'Record of the alerting threshold; grouped by condition, i.e thresholdOf.condition0, thresholdOf.condition1, etc.', - } - ), - }, - { - name: 'metricOf', - description: i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.metricOfActionVariableDescription', - { - defaultMessage: - 'Record of the watched metric; grouped by condition, i.e metricOf.condition0, metricOf.condition1, etc.', - } - ), - }, + { name: 'group', description: groupActionVariableDescription }, + { name: 'alertState', description: alertStateActionVariableDescription }, + { name: 'reason', description: reasonActionVariableDescription }, + { name: 'timestamp', description: timestampActionVariableDescription }, + { name: 'value', description: valueActionVariableDescription }, + { name: 'metric', description: metricActionVariableDescription }, + { name: 'threshold', description: thresholdActionVariableDescription }, ], }, }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index d862f70c47caec..49f82c7ccec0b8 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -67,10 +67,15 @@ export const evaluateAlert = ( currentValue: Array.isArray(points) ? last(points)?.value : NaN, timestamp: Array.isArray(points) ? last(points)?.key : NaN, shouldFire: Array.isArray(points) - ? points.map((point) => comparisonFunction(point.value, threshold)) + ? points.map( + (point) => + typeof point.value === 'number' && comparisonFunction(point.value, threshold) + ) : [false], - isNoData: points === null, - isError: isNaN(points), + isNoData: Array.isArray(points) + ? points.map((point) => point?.value === null || point === null) + : [points === null], + isError: isNaN(Array.isArray(points) ? last(points)?.value : points), }; }); }) @@ -172,7 +177,7 @@ const getValuesFromAggregations = ( } return buckets.map((bucket) => ({ key: bucket.key_as_string, - value: bucket.aggregatedValue.value, + value: bucket.aggregatedValue?.value ?? null, })); } catch (e) { return NaN; // Error state diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 9a46925a51762e..fa705798baf7a2 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -318,6 +318,31 @@ describe('The metric threshold alert type', () => { }); }); + describe("querying a rate-aggregated metric that hasn't reported data", () => { + const instanceID = '*'; + const execute = () => + executor({ + services, + params: { + criteria: [ + { + ...baseCriterion, + comparator: Comparator.GT, + threshold: 1, + metric: 'test.metric.3', + aggType: 'rate', + }, + ], + alertOnNoData: true, + }, + }); + test('sends a No Data alert', async () => { + await execute(); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.NO_DATA); + }); + }); + // describe('querying a metric that later recovers', () => { // const instanceID = '*'; // const execute = (threshold: number[]) => @@ -401,7 +426,9 @@ services.callCluster.mockImplementation(async (_: string, { body, index }: any) if (metric === 'test.metric.2') { return mocks.alternateMetricResponse; } else if (metric === 'test.metric.3') { - return mocks.emptyMetricResponse; + return body.aggs.aggregatedIntervals.aggregations.aggregatedValue_max + ? mocks.emptyRateResponse + : mocks.emptyMetricResponse; } return mocks.basicMetricResponse; }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index b4754a8624fd52..b2a8f0281b9e2d 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -45,7 +45,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => ); // AND logic; because we need to evaluate all criteria, if one of them reports no data then the // whole alert is in a No Data/Error state - const isNoData = alertResults.some((result) => result[group].isNoData); + const isNoData = alertResults.some((result) => last(result[group].isNoData)); const isError = alertResults.some((result) => result[group].isError); const nextState = isError diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts index 0ecfa27d0f0a84..5aca7f0890940a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts @@ -36,7 +36,7 @@ export const previewMetricThresholdAlert: ( params: PreviewMetricThresholdAlertParams, iterations?: number, precalculatedNumberOfGroups?: number -) => Promise> = async ( +) => Promise = async ( { callCluster, params, @@ -77,15 +77,6 @@ export const previewMetricThresholdAlert: ( const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds; const previewResults = await Promise.all( groups.map(async (group) => { - const isNoData = alertResults.some((alertResult) => alertResult[group].isNoData); - if (isNoData) { - return null; - } - const isError = alertResults.some((alertResult) => alertResult[group].isError); - if (isError) { - return NaN; - } - // Interpolate the buckets returned by evaluateAlert and return a count of how many of these // buckets would have fired the alert. If the alert interval and bucket interval are the same, // this will be a 1:1 evaluation of the alert results. If these are different, the interpolation @@ -95,14 +86,25 @@ export const previewMetricThresholdAlert: ( numberOfResultBuckets / alertResultsPerExecution ); let numberOfTimesFired = 0; + let numberOfNoDataResults = 0; + let numberOfErrors = 0; for (let i = 0; i < numberOfExecutionBuckets; i++) { const mappedBucketIndex = Math.floor(i * alertResultsPerExecution); const allConditionsFiredInMappedBucket = alertResults.every( (alertResult) => alertResult[group].shouldFire[mappedBucketIndex] ); + const someConditionsNoDataInMappedBucket = alertResults.some((alertResult) => { + const hasNoData = alertResult[group].isNoData as boolean[]; + return hasNoData[mappedBucketIndex]; + }); + const someConditionsErrorInMappedBucket = alertResults.some((alertResult) => { + return alertResult[group].isError; + }); if (allConditionsFiredInMappedBucket) numberOfTimesFired++; + if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++; + if (someConditionsErrorInMappedBucket) numberOfErrors++; } - return numberOfTimesFired; + return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors]; }) ); return previewResults; @@ -152,9 +154,9 @@ export const previewMetricThresholdAlert: ( // so filter these results out entirely and only regard the resultA portion .filter((value) => typeof value !== 'undefined') .reduce((a, b) => { - if (typeof a !== 'number') return a; - if (typeof b !== 'number') return b; - return a + b; + if (!a) return b; + if (!b) return a; + return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; }) ); return zippedResult as any; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 51a127e9345b44..45b1df2f03ea11 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -3,13 +3,21 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; import { METRIC_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; import { InfraBackendLibs } from '../../infra_types'; import { oneOfLiterals, validateIsStringElasticsearchJSONFilter } from '../common/utils'; +import { + groupActionVariableDescription, + alertStateActionVariableDescription, + reasonActionVariableDescription, + timestampActionVariableDescription, + valueActionVariableDescription, + metricActionVariableDescription, + thresholdActionVariableDescription, +} from '../common/messages'; export function registerMetricThresholdAlertType(libs: InfraBackendLibs) { const baseCriterion = { @@ -31,59 +39,6 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs) { metric: schema.never(), }); - const groupActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.groupActionVariableDescription', - { - defaultMessage: 'Name of the group reporting data', - } - ); - - const alertStateActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.alertStateActionVariableDescription', - { - defaultMessage: 'Current state of the alert', - } - ); - - const reasonActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.reasonActionVariableDescription', - { - defaultMessage: - 'A description of why the alert is in this state, including which metrics have crossed which thresholds', - } - ); - - const timestampActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.timestampDescription', - { - defaultMessage: 'A timestamp of when the alert was detected.', - } - ); - - const valueActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.valueActionVariableDescription', - { - defaultMessage: - 'The value of the metric in the specified condition. Usage: (ctx.value.condition0, ctx.value.condition1, etc...).', - } - ); - - const metricActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.metricActionVariableDescription', - { - defaultMessage: - 'The metric name in the specified condition. Usage: (ctx.metric.condition0, ctx.metric.condition1, etc...).', - } - ); - - const thresholdActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.thresholdActionVariableDescription', - { - defaultMessage: - 'The threshold value of the metric for the specified condition. Usage: (ctx.threshold.condition0, ctx.threshold.condition1, etc...).', - } - ); - return { id: METRIC_THRESHOLD_ALERT_TYPE_ID, name: 'Metric threshold', diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index c7e53eb2008f54..5c2f76cea87c4b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -62,6 +62,19 @@ export const emptyMetricResponse = { }, }; +export const emptyRateResponse = { + aggregations: { + aggregatedIntervals: { + buckets: [ + { + doc_count: 2, + aggregatedValue_max: { value: null }, + }, + ], + }, + }, +}; + export const basicCompositeResponse = { aggregations: { groupings: { diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index 8a3e9e4d0bedcb..5594323d706de7 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -55,10 +55,13 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) const numberOfGroups = previewResult.length; const resultTotals = previewResult.reduce( - (totals, groupResult) => { - if (groupResult === null) return { ...totals, noData: totals.noData + 1 }; - if (isNaN(groupResult)) return { ...totals, error: totals.error + 1 }; - return { ...totals, fired: totals.fired + groupResult }; + (totals, [firedResult, noDataResult, errorResult]) => { + return { + ...totals, + fired: totals.fired + firedResult, + noData: totals.noData + noDataResult, + error: totals.error + errorResult, + }; }, { fired: 0, @@ -66,7 +69,6 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) error: 0, } ); - return response.ok({ body: alertPreviewSuccessResponsePayloadRT.encode({ numberOfGroups, @@ -86,10 +88,13 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) const numberOfGroups = previewResult.length; const resultTotals = previewResult.reduce( - (totals, groupResult) => { - if (groupResult === null) return { ...totals, noData: totals.noData + 1 }; - if (isNaN(groupResult)) return { ...totals, error: totals.error + 1 }; - return { ...totals, fired: totals.fired + groupResult }; + (totals, [firedResult, noDataResult, errorResult]) => { + return { + ...totals, + fired: totals.fired + firedResult, + noData: totals.noData + noDataResult, + error: totals.error + errorResult, + }; }, { fired: 0, diff --git a/x-pack/plugins/ingest_manager/common/constants/epm.ts b/x-pack/plugins/ingest_manager/common/constants/epm.ts index 3d3c91a4310f8c..73cd8463bb6aab 100644 --- a/x-pack/plugins/ingest_manager/common/constants/epm.ts +++ b/x-pack/plugins/ingest_manager/common/constants/epm.ts @@ -6,5 +6,4 @@ export const PACKAGES_SAVED_OBJECT_TYPE = 'epm-packages'; export const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; -export const DEFAULT_REGISTRY_URL = 'https://epr-snapshot.ea-web.elastic.dev'; export const INDEX_PATTERN_PLACEHOLDER_SUFFIX = '-index_pattern_placeholder'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx index e98ebb7cadc7cf..5343d86244f1ea 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx @@ -64,6 +64,14 @@ export const EnrollmentStepAgentConfig: React.FC = (props) => { useEffect( function useDefaultConfigEffect() { if (agentConfigs && agentConfigs.length && !selectedState.agentConfigId) { + if (agentConfigs.length === 1) { + setSelectedState({ + ...selectedState, + agentConfigId: agentConfigs[0].id, + }); + return; + } + const defaultConfig = agentConfigs.find((config) => config.is_default); if (defaultConfig) { setSelectedState({ diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index ce81736f2e84f1..1ec13bd80f0fb9 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -43,5 +43,4 @@ export { // Defaults DEFAULT_AGENT_CONFIG, DEFAULT_OUTPUT, - DEFAULT_REGISTRY_URL, } from '../../common'; diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 40e0153a265817..6f8c4948559d3a 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -6,7 +6,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; import { IngestManagerPlugin } from './plugin'; -export { AgentService, ESIndexPatternService } from './services'; +export { AgentService, ESIndexPatternService, getRegistryUrl } from './services'; export { IngestManagerSetupContract, IngestManagerSetupDeps, diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index e7495df254a090..e5e1194d59ecb9 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -83,9 +83,9 @@ export interface IngestManagerAppContext { security?: SecurityPluginSetup; config$?: Observable; savedObjects: SavedObjectsServiceStart; - isProductionMode: boolean; - kibanaVersion: string; - kibanaBranch: string; + isProductionMode: PluginInitializerContext['env']['mode']['prod']; + kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; + kibanaBranch: PluginInitializerContext['env']['packageInfo']['branch']; cloud?: CloudSetup; logger?: Logger; httpSetup?: HttpServiceSetup; @@ -144,9 +144,9 @@ export class IngestManagerPlugin private cloud: CloudSetup | undefined; private logger: Logger | undefined; - private isProductionMode: boolean; - private kibanaVersion: string; - private kibanaBranch: string; + private isProductionMode: IngestManagerAppContext['isProductionMode']; + private kibanaVersion: IngestManagerAppContext['kibanaVersion']; + private kibanaBranch: IngestManagerAppContext['kibanaBranch']; private httpSetup: HttpServiceSetup | undefined; private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined; diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts index bdc7a443ba6dd0..7f82670a4d02c2 100644 --- a/x-pack/plugins/ingest_manager/server/services/app_context.ts +++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts @@ -10,6 +10,7 @@ import { EncryptedSavedObjectsClient, EncryptedSavedObjectsPluginSetup, } from '../../../encrypted_saved_objects/server'; +import packageJSON from '../../../../../package.json'; import { SecurityPluginSetup } from '../../../security/server'; import { IngestManagerConfigType } from '../../common'; import { ExternalCallback, ExternalCallbacksStorage, IngestManagerAppContext } from '../plugin'; @@ -22,9 +23,9 @@ class AppContextService { private config$?: Observable; private configSubject$?: BehaviorSubject; private savedObjects: SavedObjectsServiceStart | undefined; - private isProductionMode: boolean = false; - private kibanaVersion: string | undefined; - private kibanaBranch: string | undefined; + private isProductionMode: IngestManagerAppContext['isProductionMode'] = false; + private kibanaVersion: IngestManagerAppContext['kibanaVersion'] = packageJSON.version; + private kibanaBranch: IngestManagerAppContext['kibanaBranch'] = packageJSON.branch; private cloud?: CloudSetup; private logger: Logger | undefined; private httpSetup?: HttpServiceSetup; @@ -121,16 +122,10 @@ class AppContextService { } public getKibanaVersion() { - if (!this.kibanaVersion) { - throw new Error('Kibana version is not set.'); - } return this.kibanaVersion; } public getKibanaBranch() { - if (!this.kibanaBranch) { - throw new Error('Kibana branch is not set.'); - } return this.kibanaBranch; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts index 47c91218089883..b788d1bcbb4a92 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts @@ -3,20 +3,37 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_REGISTRY_URL } from '../../../constants'; import { appContextService, licenseService } from '../../'; +// from https://github.com/elastic/package-registry#docker (maybe from OpenAPI one day) +// the unused variables cause a TS warning about unused values +// chose to comment them out vs @ts-ignore or @ts-expect-error on each line + +const PRODUCTION_REGISTRY_URL_CDN = 'https://epr.elastic.co'; +// const STAGING_REGISTRY_URL_CDN = 'https://epr-staging.elastic.co'; +// const EXPERIMENTAL_REGISTRY_URL_CDN = 'https://epr-experimental.elastic.co/'; +const SNAPSHOT_REGISTRY_URL_CDN = 'https://epr-snapshot.elastic.co'; + +// const PRODUCTION_REGISTRY_URL_NO_CDN = 'https://epr.ea-web.elastic.dev'; +// const STAGING_REGISTRY_URL_NO_CDN = 'https://epr-staging.ea-web.elastic.dev'; +// const EXPERIMENTAL_REGISTRY_URL_NO_CDN = 'https://epr-experimental.ea-web.elastic.dev/'; +// const SNAPSHOT_REGISTRY_URL_NO_CDN = 'https://epr-snapshot.ea-web.elastic.dev'; + +const getDefaultRegistryUrl = (): string => { + const branch = appContextService.getKibanaBranch(); + if (branch === 'master') { + return SNAPSHOT_REGISTRY_URL_CDN; + } else { + return PRODUCTION_REGISTRY_URL_CDN; + } +}; + export const getRegistryUrl = (): string => { const license = licenseService.getLicenseInformation(); const customUrl = appContextService.getConfig()?.registryUrl; + const isGoldPlus = license?.isAvailable && license?.isActive && license?.hasAtLeast('gold'); - if ( - customUrl && - license && - license.isAvailable && - license.hasAtLeast('gold') && - license.isActive - ) { + if (customUrl && isGoldPlus) { return customUrl; } @@ -24,5 +41,5 @@ export const getRegistryUrl = (): string => { appContextService.getLogger().warn('Gold license is required to use a custom registry url.'); } - return DEFAULT_REGISTRY_URL; + return getDefaultRegistryUrl(); }; diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts index 74adab09d12ebb..f6ca9e7bbbe71f 100644 --- a/x-pack/plugins/ingest_manager/server/services/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -9,6 +9,8 @@ import { AgentStatus, Agent } from '../types'; import * as settingsService from './settings'; export { ESIndexPatternSavedObjectService } from './es_index_pattern'; +export { getRegistryUrl } from './epm/registry/registry_url'; + /** * Service to return the index pattern of EPM packages */ diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts index d450debd562935..fad8ecc86277bd 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { EsDataTypeGeoPoint, diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index 26511f89c32b8d..76aa896a741f68 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -9,7 +9,7 @@ import * as t from 'io-ts'; import { DefaultNamespace } from '../types/default_namespace'; -import { DefaultStringArray, NonEmptyString } from '../../siem_common_deps'; +import { DefaultStringArray, NonEmptyString } from '../../shared_imports'; export const name = t.string; export type Name = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts index 7ac75b077acb56..d8e3793ac9bd63 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { SearchEsListItemSchema, searchEsListItemSchema } from './search_es_list_item_schema'; import { getSearchEsListItemMock } from './search_es_list_item_schema.mock'; @@ -22,7 +22,7 @@ describe('search_es_list_item_schema', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate with a madeup value', () => { + test('it should FAIL validation when a madeup value', () => { const payload: SearchEsListItemSchema & { madeupValue: string } = { ...getSearchEsListItemMock(), madeupValue: 'madeupvalue', diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts index 739f102e6a872c..27a6c5ef524609 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { SearchEsListSchema, searchEsListSchema } from './search_es_list_schema'; import { getSearchEsListMock } from './search_es_list_schema.mock'; @@ -22,7 +22,7 @@ describe('search_es_list_schema', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate with a madeup value', () => { + test('it should FAIL validation when a madeup value', () => { const payload: SearchEsListSchema & { madeupValue: string } = { ...getSearchEsListMock(), madeupValue: 'madeupvalue', diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts index 75e0410be610aa..e40a80a0d589df 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; import { CommentsArray } from '../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts index ab30e8e35548d1..626b9e3e624ef7 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts @@ -22,7 +22,7 @@ import { import { RequiredKeepUndefined } from '../../types'; import { CreateCommentsArray, DefaultCreateCommentsArray, nonEmptyEntriesArray } from '../types'; import { EntriesArray } from '../types/entries'; -import { DefaultUuid } from '../../siem_common_deps'; +import { DefaultUuid } from '../../shared_imports'; export const createEndpointListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts index cf4c1fea0306f5..d2ad69d1ee7b65 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; import { CommentsArray } from '../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts index c3f41cac90c640..039a38594a3677 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts @@ -29,7 +29,7 @@ import { nonEmptyEntriesArray, } from '../types'; import { EntriesArray } from '../types/entries'; -import { DefaultUuid } from '../../siem_common_deps'; +import { DefaultUuid } from '../../shared_imports'; export const createExceptionListItemSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts index 21270f526900b4..c9e2aa37a132bf 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { CreateExceptionListSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts index 94a4e1588f5ab7..7009fbd709e548 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts @@ -25,7 +25,7 @@ import { DefaultUuid, DefaultVersionNumber, DefaultVersionNumberDecoded, -} from '../../siem_common_deps'; +} from '../../shared_imports'; import { NamespaceType } from '../types'; export const createExceptionListSchema = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts index 8178d49690e399..813d5e349e7e64 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateListItemSchemaMock } from './create_list_item_schema.mock'; import { CreateListItemSchema, createListItemSchema } from './create_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts index 9b496a01045de3..82340453a98f11 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { CreateListSchema, createListSchema } from './create_list_schema'; import { getCreateListSchemaMock } from './create_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts index 18ed0f42ccd6f6..bfe3ecdcb623bd 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { description, deserializer, id, meta, name, serializer, type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; -import { DefaultVersionNumber, DefaultVersionNumberDecoded } from '../../siem_common_deps'; +import { DefaultVersionNumber, DefaultVersionNumberDecoded } from '../../shared_imports'; export const createListSchema = t.intersection([ t.exact( diff --git a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts index fa75be8bc541ea..fa3c1ef3b02f54 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteEndpointListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts index 042f62a8d129be..d249cd779e8621 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteExceptionListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts index 2bb0a23173bd6f..ec781d59af120d 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteExceptionListSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts index 9bc2825d774edf..7b2263863e1f62 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteListItemSchema, deleteListItemSchema } from './delete_list_item_schema'; import { getDeleteListItemSchemaMock } from './delete_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts index 278508305c6f0a..65ca2f3f457e93 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { DeleteListSchema, deleteListSchema } from './delete_list_schema'; import { getDeleteListSchemaMock } from './delete_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts index 1ffe2e2fc4ecc5..cd6f4c1b147db2 100644 --- a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { ExportListItemQuerySchema, diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts index 8249b1e2d49c27..79449b136d0669 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getFindEndpointListItemSchemaDecodedMock, diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts index f402f22b093adc..1e971a4eebc33d 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { LIST_ID } from '../../constants.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts index ef96346c732b85..6f5d34d6be73e9 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getFindExceptionListSchemaDecodedMock, diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts index 59d4b4485b5786..8c119aeb14e248 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { LIST_ID } from '../../constants.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts index 63f29a64b4bf95..086e457e8f6b83 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getFindListSchemaDecodedMock, getFindListSchemaMock } from './find_list_schema.mock'; import { FindListSchemaEncoded, findListSchema } from './find_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts index 9d03229b4d1d9d..9945dc03c2e143 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { ImportListItemQuerySchema, diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts index 7f7c6368a1c5e9..4de77b66610d36 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { ImportListItemSchema, importListItemSchema } from './import_list_item_schema'; import { getImportListItemSchemaMock } from './import_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts index 58c19e8f9cb4f2..b148f19da8a867 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getPathListItemSchemaMock } from './patch_list_item_schema.mock'; import { PatchListItemSchema, patchListItemSchema } from './patch_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts index 3ab658014bbfaf..dea48df3f17027 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getPathListSchemaMock } from './patch_list_schema.mock'; import { PatchListSchema, patchListSchema } from './patch_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts index 70a1d783c87d61..adec476ea5ad79 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadEndpointListItemSchemaMock } from './read_endpoint_list_item_schema.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts index 86c80a527be0d0..b7c2715f14e1c5 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadExceptionListItemSchemaMock } from './read_exception_list_item_schema.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts index 86cebc3cd3f8eb..3bc61e3a5e90a9 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadExceptionListSchemaMock } from './read_exception_list_schema.mock'; import { ReadExceptionListSchema, readExceptionListSchema } from './read_exception_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts index 5c71c9820cc1e4..1d140719ad9394 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadListItemSchemaMock } from './read_list_item_schema.mock'; import { ReadListItemSchema, readListItemSchema } from './read_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts index a1ba2655dd723b..0b7e92c23f77af 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getReadListSchemaMock } from './read_list_schema.mock'; import { ReadListSchema, readListSchema } from './read_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts index db5bc45ad028b8..ecbbb250a88f60 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateEndpointListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts index ce589fb097a601..a49a5552603fd0 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateExceptionListItemSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts index 892f277045a69d..650cbd439ad2bb 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateExceptionListSchema, diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts index 6127e203438347..cb6cd76dd3f037 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { UpdateListItemSchema, updateListItemSchema } from './update_list_item_schema'; import { getUpdateListItemSchemaMock } from './update_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts index 6e7fb158767b50..a59a93b06e34d7 100644 --- a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getAcknowledgeSchemaResponseMock } from './acknowledge_schema.mock'; import { AcknowledgeSchema, acknowledgeSchema } from './acknowledge_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts index 5fccaaac22e3ad..8c1392109979e4 100644 --- a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListSchemaMock } from './exception_list_schema.mock'; import { CreateEndpointListSchema, createEndpointListSchema } from './create_endpoint_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts index c8bf73cf842e1e..32b55104e4fdf5 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock'; import { ExceptionListItemSchema, exceptionListItemSchema } from './exception_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts index b773dd498ed01f..1b5ef08b02d5f2 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListSchemaMock } from './exception_list_schema.mock'; import { ExceptionListSchema, exceptionListSchema } from './exception_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts index 70fcf9a86122cc..5da3accccd9c2d 100644 --- a/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock'; import { getFoundExceptionListItemSchemaMock } from './found_exception_list_item_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts index a96ee07c4613b1..d4fa8ee0e3481b 100644 --- a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getExceptionListSchemaMock } from './exception_list_schema.mock'; import { getFoundExceptionListSchemaMock } from './found_exception_list_schema.mock'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts index 9cb130ec0e8ada..2b072d8f95cd82 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getListItemIndexExistSchemaResponseMock } from './list_item_index_exist_schema.mock'; import { ListItemIndexExistSchema, listItemIndexExistSchema } from './list_item_index_exist_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts index 8b73506d137509..ec4c8d2c2d1ead 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getListItemResponseMock } from './list_item_schema.mock'; import { ListItemSchema, listItemSchema } from './list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts index e7ae9b45a5e150..87e56e5dd95ac4 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.test.ts @@ -7,7 +7,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; import { getListResponseMock } from './list_schema.mock'; import { ListSchema, listSchema } from './list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/types/comment.test.ts b/x-pack/plugins/lists/common/schemas/types/comment.test.ts index c7c945277f7566..081bb9b4bae542 100644 --- a/x-pack/plugins/lists/common/schemas/types/comment.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DATE_NOW } from '../../constants.mock'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getCommentsArrayMock, getCommentsMock } from './comment.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/types/comment.ts b/x-pack/plugins/lists/common/schemas/types/comment.ts index 6b0b0166b9ee14..4d7aba3b3ad988 100644 --- a/x-pack/plugins/lists/common/schemas/types/comment.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { created_at, created_by, id, updated_at, updated_by } from '../common/schemas'; export const comment = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts index 366bf84d48bbf0..8bca8df437871d 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getCreateCommentsArrayMock, getCreateCommentsMock } from './create_comment.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/types/create_comment.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.ts index fd33313430ce6a..4ccc28b2c4a6d2 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comment.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.ts @@ -5,7 +5,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; export const createComment = t.exact( t.type({ diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts index 541b8ab1c799c0..ee2dc0cf2a478b 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultCommentsArray } from './default_comments_array'; import { CommentsArray } from './comment'; diff --git a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts index eb960b54119048..4aac3cc84a3a25 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultCreateCommentsArray } from './default_create_comments_array'; import { CreateCommentsArray } from './create_comment'; diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts index 152f85233aa1a4..8e7ffdbdaea7b8 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultNamespace } from './default_namespace'; @@ -48,7 +48,7 @@ describe('default_namespace', () => { expect(message.schema).toEqual('single'); }); - test('it should NOT validate if not "single" or "agnostic"', () => { + test('it should FAIL validation if not "single" or "agnostic"', () => { const payload = 'something else'; const decoded = DefaultNamespace.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts index 255c89959b6100..e377faae87947e 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultNamespaceArray, DefaultNamespaceArrayType } from './default_namespace_array'; @@ -21,7 +21,7 @@ describe('default_namespace_array', () => { expect(message.schema).toEqual(['single']); }); - test('it should NOT validate a numeric value', () => { + test('it should FAIL validation of numeric value', () => { const payload = 5; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -86,7 +86,7 @@ describe('default_namespace_array', () => { expect(message.schema).toEqual(['single', 'agnostic', 'single']); }); - test('it should not validate 3 elements of "single,agnostic,junk" since the 3rd value is junk', () => { + test('it should FAIL validation when given 3 elements of "single,agnostic,junk" since the 3rd value is junk', () => { const payload: DefaultNamespaceArrayType = 'single,agnostic,junk'; const decoded = DefaultNamespaceArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts index 612148dc4ccabc..25c84af8c9ee34 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { DefaultUpdateCommentsArray } from './default_update_comments_array'; import { UpdateCommentsArray } from './update_comment'; diff --git a/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts index b14afab327fb06..3ddeeebfceda7f 100644 --- a/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/empty_string_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { EmptyStringArray, EmptyStringArrayEncoded } from './empty_string_array'; @@ -57,7 +57,7 @@ describe('empty_string_array', () => { expect(message.schema).toEqual(['a', 'b', 'c']); }); - test('it should NOT validate a number', () => { + test('it should FAIL validation of number', () => { const payload: number = 5; const decoded = EmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts index 16794415138b2a..c0093ed750b628 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts @@ -12,21 +12,18 @@ import { getEntryExistsMock } from './entry_exists.mock'; import { getEntryNestedMock } from './entry_nested.mock'; export const getListAndNonListEntriesArrayMock = (): EntriesArray => [ - { ...getEntryMatchMock() }, - { ...getEntryMatchAnyMock() }, - { ...getEntryListMock() }, - { ...getEntryExistsMock() }, - { ...getEntryNestedMock() }, + getEntryMatchMock(), + getEntryMatchAnyMock(), + getEntryListMock(), + getEntryExistsMock(), + getEntryNestedMock(), ]; -export const getListEntriesArrayMock = (): EntriesArray => [ - { ...getEntryListMock() }, - { ...getEntryListMock() }, -]; +export const getListEntriesArrayMock = (): EntriesArray => [getEntryListMock(), getEntryListMock()]; export const getEntriesArrayMock = (): EntriesArray => [ - { ...getEntryMatchMock() }, - { ...getEntryMatchAnyMock() }, - { ...getEntryExistsMock() }, - { ...getEntryNestedMock() }, + getEntryMatchMock(), + getEntryMatchAnyMock(), + getEntryExistsMock(), + getEntryNestedMock(), ]; diff --git a/x-pack/plugins/lists/common/schemas/types/entries.test.ts b/x-pack/plugins/lists/common/schemas/types/entries.test.ts index cad94220a232c3..f5c022c7a394f4 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; @@ -20,7 +20,7 @@ import { entriesArray, entriesArrayOrUndefined, entry } from './entries'; describe('Entries', () => { describe('entry', () => { test('it should validate a match entry', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -29,7 +29,7 @@ describe('Entries', () => { }); test('it should validate a match_any entry', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -38,7 +38,7 @@ describe('Entries', () => { }); test('it should validate a exists entry', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -47,7 +47,7 @@ describe('Entries', () => { }); test('it should validate a list entry', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -55,8 +55,8 @@ describe('Entries', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate a nested entry', () => { - const payload = { ...getEntryNestedMock() }; + test('it should FAIL validation of nested entry', () => { + const payload = getEntryNestedMock(); const decoded = entry.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -79,7 +79,7 @@ describe('Entries', () => { describe('entriesArray', () => { test('it should validate an array with match entry', () => { - const payload = [{ ...getEntryMatchMock() }]; + const payload = [getEntryMatchMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -88,7 +88,7 @@ describe('Entries', () => { }); test('it should validate an array with match_any entry', () => { - const payload = [{ ...getEntryMatchAnyMock() }]; + const payload = [getEntryMatchAnyMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -97,7 +97,7 @@ describe('Entries', () => { }); test('it should validate an array with exists entry', () => { - const payload = [{ ...getEntryExistsMock() }]; + const payload = [getEntryExistsMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -106,7 +106,7 @@ describe('Entries', () => { }); test('it should validate an array with list entry', () => { - const payload = [{ ...getEntryListMock() }]; + const payload = [getEntryListMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -115,7 +115,7 @@ describe('Entries', () => { }); test('it should validate an array with nested entry', () => { - const payload = [{ ...getEntryNestedMock() }]; + const payload = [getEntryNestedMock()]; const decoded = entriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -144,7 +144,7 @@ describe('Entries', () => { }); test('it should validate an array with nested entry', () => { - const payload = [{ ...getEntryNestedMock() }]; + const payload = [getEntryNestedMock()]; const decoded = entriesArrayOrUndefined.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts index 9d5b669333db8c..0eb35b0768cf43 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_exists.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryExistsMock } from './entry_exists.mock'; import { EntryExists, entriesExists } from './entry_exists'; describe('entriesExists', () => { test('it should validate an entry', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesExists', () => { }); test('it should validate when "operator" is "included"', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesExists', () => { }); test('it should validate when "operator" is "excluded"', () => { - const payload = { ...getEntryExistsMock() }; + const payload = getEntryExistsMock(); payload.operator = 'excluded'; const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesExists', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when "field" is empty string', () => { + test('it should FAIL validation when "field" is empty string', () => { const payload: Omit & { field: string } = { ...getEntryExistsMock(), field: '', @@ -56,16 +56,16 @@ describe('entriesExists', () => { test('it should strip out extra keys', () => { const payload: EntryExists & { extraKey?: string; - } = { ...getEntryExistsMock() }; + } = getEntryExistsMock(); payload.extraKey = 'some extra key'; const decoded = entriesExists.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryExistsMock() }); + expect(message.schema).toEqual(getEntryExistsMock()); }); - test('it should not validate when "type" is not "exists"', () => { + test('it should FAIL validation when "type" is not "exists"', () => { const payload: Omit & { type: string } = { ...getEntryExistsMock(), type: 'match', diff --git a/x-pack/plugins/lists/common/schemas/types/entry_exists.ts b/x-pack/plugins/lists/common/schemas/types/entry_exists.ts index 05c82d2532218e..4d9c09cc935744 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_exists.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_exists.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator } from '../common/schemas'; export const entriesExists = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts index 14857edad5e3ba..834fed3550e3f4 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_list.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryListMock } from './entry_list.mock'; import { EntryList, entriesList } from './entry_list'; describe('entriesList', () => { test('it should validate an entry', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesList', () => { }); test('it should validate when operator is "included"', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesList', () => { }); test('it should validate when "operator" is "excluded"', () => { - const payload = { ...getEntryListMock() }; + const payload = getEntryListMock(); payload.operator = 'excluded'; const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesList', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when "list" is not expected value', () => { + test('it should FAIL validation when "list" is not expected value', () => { const payload: Omit & { list: string } = { ...getEntryListMock(), list: 'someListId', @@ -55,7 +55,7 @@ describe('entriesList', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "list.id" is empty string', () => { + test('it should FAIL validation when "list.id" is empty string', () => { const payload: Omit & { list: { id: string; type: 'ip' } } = { ...getEntryListMock(), list: { id: '', type: 'ip' }, @@ -67,7 +67,7 @@ describe('entriesList', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "type" is not "lists"', () => { + test('it should FAIL validation when "type" is not "lists"', () => { const payload: Omit & { type: 'match_any' } = { ...getEntryListMock(), type: 'match_any', @@ -84,12 +84,12 @@ describe('entriesList', () => { test('it should strip out extra keys', () => { const payload: EntryList & { extraKey?: string; - } = { ...getEntryListMock() }; + } = getEntryListMock(); payload.extraKey = 'some extra key'; const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryListMock() }); + expect(message.schema).toEqual(getEntryListMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_list.ts b/x-pack/plugins/lists/common/schemas/types/entry_list.ts index ae9de967db027c..fcfec5e0cccdf1 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_list.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_list.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator, type } from '../common/schemas'; export const entriesList = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts index 2c64592518eb7b..7b49c418b547f2 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { EntryMatch, entriesMatch } from './entry_match'; describe('entriesMatch', () => { test('it should validate an entry', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesMatch', () => { }); test('it should validate when operator is "included"', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesMatch', () => { }); test('it should validate when "operator" is "excluded"', () => { - const payload = { ...getEntryMatchMock() }; + const payload = getEntryMatchMock(); payload.operator = 'excluded'; const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when "field" is empty string', () => { + test('it should FAIL validation when "field" is empty string', () => { const payload: Omit & { field: string } = { ...getEntryMatchMock(), field: '', @@ -53,7 +53,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "value" is not string', () => { + test('it should FAIL validation when "value" is not string', () => { const payload: Omit & { value: string[] } = { ...getEntryMatchMock(), value: ['some value'], @@ -67,7 +67,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "value" is empty string', () => { + test('it should FAIL validation when "value" is empty string', () => { const payload: Omit & { value: string } = { ...getEntryMatchMock(), value: '', @@ -79,7 +79,7 @@ describe('entriesMatch', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "type" is not "match"', () => { + test('it should FAIL validation when "type" is not "match"', () => { const payload: Omit & { type: string } = { ...getEntryMatchMock(), type: 'match_any', @@ -96,12 +96,12 @@ describe('entriesMatch', () => { test('it should strip out extra keys', () => { const payload: EntryMatch & { extraKey?: string; - } = { ...getEntryMatchMock() }; + } = getEntryMatchMock(); payload.extraKey = 'some value'; const decoded = entriesMatch.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryMatchMock() }); + expect(message.schema).toEqual(getEntryMatchMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match.ts b/x-pack/plugins/lists/common/schemas/types/entry_match.ts index a21f83f317e354..247d64674e27da 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator } from '../common/schemas'; export const entriesMatch = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts index 4dab2f45711f0e..628ccfd74b6062 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_any.test.ts @@ -7,14 +7,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; import { EntryMatchAny, entriesMatchAny } from './entry_match_any'; describe('entriesMatchAny', () => { test('it should validate an entry', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('entriesMatchAny', () => { }); test('it should validate when operator is "included"', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -32,7 +32,7 @@ describe('entriesMatchAny', () => { }); test('it should validate when operator is "excluded"', () => { - const payload = { ...getEntryMatchAnyMock() }; + const payload = getEntryMatchAnyMock(); payload.operator = 'excluded'; const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -41,7 +41,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when field is empty string', () => { + test('it should FAIL validation when field is empty string', () => { const payload: Omit & { field: string } = { ...getEntryMatchAnyMock(), field: '', @@ -53,7 +53,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when value is empty array', () => { + test('it should FAIL validation when value is empty array', () => { const payload: Omit & { value: string[] } = { ...getEntryMatchAnyMock(), value: [], @@ -65,7 +65,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when value is not string array', () => { + test('it should FAIL validation when value is not string array', () => { const payload: Omit & { value: string } = { ...getEntryMatchAnyMock(), value: 'some string', @@ -79,7 +79,7 @@ describe('entriesMatchAny', () => { expect(message.schema).toEqual({}); }); - test('it should not validate when "type" is not "match_any"', () => { + test('it should FAIL validation when "type" is not "match_any"', () => { const payload: Omit & { type: string } = { ...getEntryMatchAnyMock(), type: 'match', @@ -94,12 +94,12 @@ describe('entriesMatchAny', () => { test('it should strip out extra keys', () => { const payload: EntryMatchAny & { extraKey?: string; - } = { ...getEntryMatchAnyMock() }; + } = getEntryMatchAnyMock(); payload.extraKey = 'some extra key'; const decoded = entriesMatchAny.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryMatchAnyMock() }); + expect(message.schema).toEqual(getEntryMatchAnyMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts index e93ad4aa131d19..b6c4ef509c4773 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { operator } from '../common/schemas'; import { nonEmptyOrNullableStringArray } from './non_empty_or_nullable_string_array'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts index f645bc9e40d789..d0e7712301ee13 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts @@ -11,7 +11,7 @@ import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; export const getEntryNestedMock = (): EntryNested => ({ - entries: [{ ...getEntryMatchMock() }, { ...getEntryMatchAnyMock() }], + entries: [getEntryMatchMock(), getEntryMatchAnyMock()], field: FIELD, type: NESTED, }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts index d9b58855413b1d..d77440b207d03b 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryNestedMock } from './entry_nested.mock'; import { EntryNested, entriesNested } from './entry_nested'; @@ -16,7 +16,7 @@ import { getEntryExistsMock } from './entry_exists.mock'; describe('entriesNested', () => { test('it should validate a nested entry', () => { - const payload = { ...getEntryNestedMock() }; + const payload = getEntryNestedMock(); const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -24,7 +24,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate when "type" is not "nested"', () => { + test('it should FAIL validation when "type" is not "nested"', () => { const payload: Omit & { type: 'match' } = { ...getEntryNestedMock(), type: 'match', @@ -36,7 +36,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate when "field" is empty string', () => { + test('it should FAIL validation when "field" is empty string', () => { const payload: Omit & { field: string; } = { ...getEntryNestedMock(), field: '' }; @@ -47,7 +47,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate when "field" is not a string', () => { + test('it should FAIL validation when "field" is not a string', () => { const payload: Omit & { field: number; } = { ...getEntryNestedMock(), field: 1 }; @@ -58,7 +58,7 @@ describe('entriesNested', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate when "entries" is not a an array', () => { + test('it should FAIL validation when "entries" is not a an array', () => { const payload: Omit & { entries: string; } = { ...getEntryNestedMock(), entries: 'im a string' }; @@ -72,7 +72,7 @@ describe('entriesNested', () => { }); test('it should validate when "entries" contains an entry item that is type "match"', () => { - const payload = { ...getEntryNestedMock(), entries: [{ ...getEntryMatchAnyMock() }] }; + const payload = { ...getEntryNestedMock(), entries: [getEntryMatchAnyMock()] }; const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -92,7 +92,7 @@ describe('entriesNested', () => { }); test('it should validate when "entries" contains an entry item that is type "exists"', () => { - const payload = { ...getEntryNestedMock(), entries: [{ ...getEntryExistsMock() }] }; + const payload = { ...getEntryNestedMock(), entries: [getEntryExistsMock()] }; const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -113,12 +113,12 @@ describe('entriesNested', () => { test('it should strip out extra keys', () => { const payload: EntryNested & { extraKey?: string; - } = { ...getEntryNestedMock() }; + } = getEntryNestedMock(); payload.extraKey = 'some extra key'; const decoded = entriesNested.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...getEntryNestedMock() }); + expect(message.schema).toEqual(getEntryNestedMock()); }); }); diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.ts index 9989f501d4338a..f9e8e4356b8112 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { nonEmptyNestedEntriesArray } from './non_empty_nested_entries_array'; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts index a2697286aa038d..42d476a9fefb28 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; @@ -22,7 +22,7 @@ import { nonEmptyEntriesArray } from './non_empty_entries_array'; import { EntriesArray } from './entries'; describe('non_empty_entries_array', () => { - test('it should NOT validate an empty array', () => { + test('it should FAIL validation when given an empty array', () => { const payload: EntriesArray = []; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -33,7 +33,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload = undefined; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -44,7 +44,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload = null; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -56,7 +56,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "match" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchMock() }, { ...getEntryMatchMock() }]; + const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -65,7 +65,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "match_any" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchAnyMock() }, { ...getEntryMatchAnyMock() }]; + const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -74,7 +74,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "exists" entries', () => { - const payload: EntriesArray = [{ ...getEntryExistsMock() }, { ...getEntryExistsMock() }]; + const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -92,7 +92,7 @@ describe('non_empty_entries_array', () => { }); test('it should validate an array of "nested" entries', () => { - const payload: EntriesArray = [{ ...getEntryNestedMock() }, { ...getEntryNestedMock() }]; + const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -109,7 +109,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate an array of entries of value list and non-value list entries', () => { + test('it should FAIL validation when given an array of entries of value list and non-value list entries', () => { const payload: EntriesArray = [...getListAndNonListEntriesArrayMock()]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -118,7 +118,7 @@ describe('non_empty_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate an array of non entries', () => { + test('it should FAIL validation when given an array of non entries', () => { const payload = [1]; const decoded = nonEmptyEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts index 1154f2b6098da6..7dbc3465610c00 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; @@ -17,7 +17,7 @@ import { nonEmptyNestedEntriesArray } from './non_empty_nested_entries_array'; import { EntriesArray } from './entries'; describe('non_empty_nested_entries_array', () => { - test('it should NOT validate an empty array', () => { + test('it should FAIL validation when given an empty array', () => { const payload: EntriesArray = []; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -28,7 +28,7 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload = undefined; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -39,7 +39,7 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload = null; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -51,7 +51,7 @@ describe('non_empty_nested_entries_array', () => { }); test('it should validate an array of "match" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchMock() }, { ...getEntryMatchMock() }]; + const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -60,7 +60,7 @@ describe('non_empty_nested_entries_array', () => { }); test('it should validate an array of "match_any" entries', () => { - const payload: EntriesArray = [{ ...getEntryMatchAnyMock() }, { ...getEntryMatchAnyMock() }]; + const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -69,7 +69,7 @@ describe('non_empty_nested_entries_array', () => { }); test('it should validate an array of "exists" entries', () => { - const payload: EntriesArray = [{ ...getEntryExistsMock() }, { ...getEntryExistsMock() }]; + const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -77,8 +77,8 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate an array of "nested" entries', () => { - const payload: EntriesArray = [{ ...getEntryNestedMock() }, { ...getEntryNestedMock() }]; + test('it should FAIL validation when given an array of "nested" entries', () => { + const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -105,9 +105,9 @@ describe('non_empty_nested_entries_array', () => { test('it should validate an array of entries', () => { const payload: EntriesArray = [ - { ...getEntryExistsMock() }, - { ...getEntryMatchAnyMock() }, - { ...getEntryMatchMock() }, + getEntryExistsMock(), + getEntryMatchAnyMock(), + getEntryMatchMock(), ]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -116,7 +116,7 @@ describe('non_empty_nested_entries_array', () => { expect(message.schema).toEqual(payload); }); - test('it should NOT validate an array of non entries', () => { + test('it should FAIL validation when given an array of non entries', () => { const payload = [1]; const decoded = nonEmptyNestedEntriesArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts index e3cc9104853e5e..4b31b649556b29 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.test.ts @@ -7,12 +7,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { nonEmptyOrNullableStringArray } from './non_empty_or_nullable_string_array'; describe('nonEmptyOrNullableStringArray', () => { - test('it should NOT validate an empty array', () => { + test('it should FAIL validation when given an empty array', () => { const payload: string[] = []; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload = undefined; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -34,7 +34,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload = null; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -45,7 +45,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate an array of with an empty string', () => { + test('it should FAIL validation when given an array of with an empty string', () => { const payload: string[] = ['im good', '']; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -56,7 +56,7 @@ describe('nonEmptyOrNullableStringArray', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate an array of non strings', () => { + test('it should FAIL validation when given an array of non strings', () => { const payload = [1]; const decoded = nonEmptyOrNullableStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts index fac088568f85e8..db81b0d469859a 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.test.ts @@ -7,12 +7,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { NonEmptyStringArray } from './non_empty_string_array'; describe('non_empty_string_array', () => { - test('it should NOT validate "null"', () => { + test('it should FAIL validation when given "null"', () => { const payload: NonEmptyStringArray | null = null; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -23,7 +23,7 @@ describe('non_empty_string_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate "undefined"', () => { + test('it should FAIL validation when given "undefined"', () => { const payload: NonEmptyStringArray | undefined = undefined; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -34,7 +34,7 @@ describe('non_empty_string_array', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate a single value of an empty string ""', () => { + test('it should FAIL validation of single value of an empty string ""', () => { const payload: NonEmptyStringArray = ''; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); @@ -72,7 +72,7 @@ describe('non_empty_string_array', () => { expect(message.schema).toEqual(['a', 'b', 'c']); }); - test('it should NOT validate a number', () => { + test('it should FAIL validation of number', () => { const payload: number = 5; const decoded = NonEmptyStringArray.decode(payload); const message = pipe(decoded, foldLeftRight); diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts index ac7716af40966d..ac4d0304cbb8ea 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.test.ts @@ -7,7 +7,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../siem_common_deps'; +import { foldLeftRight, getPaths } from '../../shared_imports'; import { getUpdateCommentMock, getUpdateCommentsArrayMock } from './update_comment.mock'; import { diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.ts index b95812cb35bf9a..dc14bf480857f7 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.ts @@ -5,7 +5,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../siem_common_deps'; +import { NonEmptyString } from '../../shared_imports'; import { id } from '../common/schemas'; export const updateComment = t.intersection([ diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index 455670098307fd..9add15c533d145 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -26,7 +26,7 @@ import { deleteExceptionListItemById, fetchExceptionListById, fetchExceptionListItemById, - fetchExceptionListItemsByListId, + fetchExceptionListsItemsByListIds, updateExceptionList, updateExceptionListItem, } from './api'; @@ -358,17 +358,18 @@ describe('Exceptions Lists API', () => { }); }); - describe('#fetchExceptionListItemsByListId', () => { + describe('#fetchExceptionListsItemsByListIds', () => { beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue(getFoundExceptionListItemSchemaMock()); }); - test('it invokes "fetchExceptionListItemsByListId" with expected url and body values', async () => { - await fetchExceptionListItemsByListId({ + test('it invokes "fetchExceptionListsItemsByListIds" with expected url and body values', async () => { + await fetchExceptionListsItemsByListIds({ + filterOptions: [], http: mockKibanaHttpService(), - listId: 'myList', - namespaceType: 'single', + listIds: ['myList', 'myOtherListId'], + namespaceTypes: ['single', 'single'], pagination: { page: 1, perPage: 20, @@ -379,8 +380,8 @@ describe('Exceptions Lists API', () => { expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { - list_id: 'myList', - namespace_type: 'single', + list_id: 'myList,myOtherListId', + namespace_type: 'single,single', page: '1', per_page: '20', }, @@ -389,14 +390,16 @@ describe('Exceptions Lists API', () => { }); test('it invokes with expected url and body values when a filter exists and "namespaceType" of "single"', async () => { - await fetchExceptionListItemsByListId({ - filterOptions: { - filter: 'hello world', - tags: [], - }, + await fetchExceptionListsItemsByListIds({ + filterOptions: [ + { + filter: 'hello world', + tags: [], + }, + ], http: mockKibanaHttpService(), - listId: 'myList', - namespaceType: 'single', + listIds: ['myList'], + namespaceTypes: ['single'], pagination: { page: 1, perPage: 20, @@ -418,14 +421,16 @@ describe('Exceptions Lists API', () => { }); test('it invokes with expected url and body values when a filter exists and "namespaceType" of "agnostic"', async () => { - await fetchExceptionListItemsByListId({ - filterOptions: { - filter: 'hello world', - tags: [], - }, + await fetchExceptionListsItemsByListIds({ + filterOptions: [ + { + filter: 'hello world', + tags: [], + }, + ], http: mockKibanaHttpService(), - listId: 'myList', - namespaceType: 'agnostic', + listIds: ['myList'], + namespaceTypes: ['agnostic'], pagination: { page: 1, perPage: 20, @@ -447,14 +452,16 @@ describe('Exceptions Lists API', () => { }); test('it invokes with expected url and body values when tags exists', async () => { - await fetchExceptionListItemsByListId({ - filterOptions: { - filter: '', - tags: ['malware'], - }, + await fetchExceptionListsItemsByListIds({ + filterOptions: [ + { + filter: '', + tags: ['malware'], + }, + ], http: mockKibanaHttpService(), - listId: 'myList', - namespaceType: 'agnostic', + listIds: ['myList'], + namespaceTypes: ['agnostic'], pagination: { page: 1, perPage: 20, @@ -476,14 +483,16 @@ describe('Exceptions Lists API', () => { }); test('it invokes with expected url and body values when filter and tags exists', async () => { - await fetchExceptionListItemsByListId({ - filterOptions: { - filter: 'host.name', - tags: ['malware'], - }, + await fetchExceptionListsItemsByListIds({ + filterOptions: [ + { + filter: 'host.name', + tags: ['malware'], + }, + ], http: mockKibanaHttpService(), - listId: 'myList', - namespaceType: 'agnostic', + listIds: ['myList'], + namespaceTypes: ['agnostic'], pagination: { page: 1, perPage: 20, @@ -506,10 +515,11 @@ describe('Exceptions Lists API', () => { }); test('it returns expected format when call succeeds', async () => { - const exceptionResponse = await fetchExceptionListItemsByListId({ + const exceptionResponse = await fetchExceptionListsItemsByListIds({ + filterOptions: [], http: mockKibanaHttpService(), - listId: 'endpoint_list_id', - namespaceType: 'single', + listIds: ['endpoint_list_id'], + namespaceTypes: ['single'], pagination: { page: 1, perPage: 20, @@ -521,16 +531,17 @@ describe('Exceptions Lists API', () => { test('it returns error and does not make request if request payload fails decode', async () => { const payload = ({ + filterOptions: [], http: mockKibanaHttpService(), - listId: '1', - namespaceType: 'not a namespace type', + listIds: ['myList'], + namespaceTypes: ['not a namespace type'], pagination: { page: 1, perPage: 20, }, signal: abortCtrl.signal, } as unknown) as ApiCallByListIdProps & { listId: number }; - await expect(fetchExceptionListItemsByListId(payload)).rejects.toEqual( + await expect(fetchExceptionListsItemsByListIds(payload)).rejects.toEqual( 'Invalid value "not a namespace type" supplied to "namespace_type"' ); }); @@ -541,10 +552,11 @@ describe('Exceptions Lists API', () => { fetchMock.mockResolvedValue(badPayload); await expect( - fetchExceptionListItemsByListId({ + fetchExceptionListsItemsByListIds({ + filterOptions: [], http: mockKibanaHttpService(), - listId: 'myList', - namespaceType: 'single', + listIds: ['myList'], + namespaceTypes: ['single'], pagination: { page: 1, perPage: 20, diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts index 4d9397ec0adc6c..203c84b2943fd5 100644 --- a/x-pack/plugins/lists/public/exceptions/api.ts +++ b/x-pack/plugins/lists/public/exceptions/api.ts @@ -29,7 +29,7 @@ import { updateExceptionListItemSchema, updateExceptionListSchema, } from '../../common/schemas'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { AddEndpointExceptionListProps, @@ -249,42 +249,46 @@ export const fetchExceptionListById = async ({ * Fetch an ExceptionList's ExceptionItems by providing a ExceptionList list_id * * @param http Kibana http service - * @param listId ExceptionList list_id (not ID) - * @param namespaceType ExceptionList namespace_type + * @param listIds ExceptionList list_ids (not ID) + * @param namespaceTypes ExceptionList namespace_types * @param filterOptions optional - filter by field or tags * @param pagination optional * @param signal to cancel request * * @throws An error if response is not OK */ -export const fetchExceptionListItemsByListId = async ({ +export const fetchExceptionListsItemsByListIds = async ({ http, - listId, - namespaceType, - filterOptions = { - filter: '', - tags: [], - }, + listIds, + namespaceTypes, + filterOptions, pagination, signal, }: ApiCallByListIdProps): Promise => { - const namespace = - namespaceType === 'agnostic' ? EXCEPTION_LIST_NAMESPACE_AGNOSTIC : EXCEPTION_LIST_NAMESPACE; - const filters = [ - ...(filterOptions.filter.length - ? [`${namespace}.attributes.entries.field:${filterOptions.filter}*`] - : []), - ...(filterOptions.tags.length - ? filterOptions.tags.map((t) => `${namespace}.attributes.tags:${t}`) - : []), - ]; + const filters: string = filterOptions + .map((filter, index) => { + const namespace = namespaceTypes[index]; + const filterNamespace = + namespace === 'agnostic' ? EXCEPTION_LIST_NAMESPACE_AGNOSTIC : EXCEPTION_LIST_NAMESPACE; + const formattedFilters = [ + ...(filter.filter.length + ? [`${filterNamespace}.attributes.entries.field:${filter.filter}*`] + : []), + ...(filter.tags.length + ? filter.tags.map((t) => `${filterNamespace}.attributes.tags:${t}`) + : []), + ]; + + return formattedFilters.join(' AND '); + }) + .join(','); const query = { - list_id: listId, - namespace_type: namespaceType, + list_id: listIds.join(','), + namespace_type: namespaceTypes.join(','), page: pagination.page ? `${pagination.page}` : '1', per_page: pagination.perPage ? `${pagination.perPage}` : '20', - ...(filters.length ? { filter: filters.join(' AND ') } : {}), + ...(filters.trim() !== '' ? { filter: filters } : {}), }; const [validatedRequest, errorsRequest] = validate(query, findExceptionListItemSchema); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts index 1e0f7e58a0f4cf..c93155274937e1 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts @@ -9,9 +9,10 @@ import { act, renderHook } from '@testing-library/react-hooks'; import * as api from '../api'; import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; +import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; import { HttpStart } from '../../../../../../src/core/public'; -import { ApiCallByIdProps } from '../types'; +import { ApiCallByIdProps, ApiCallByListIdProps } from '../types'; import { ExceptionsApi, useApi } from './use_api'; @@ -252,4 +253,116 @@ describe('useApi', () => { expect(onErrorMock).toHaveBeenCalledWith(mockError); }); }); + + test('it invokes "fetchExceptionListsItemsByListIds" when "getExceptionItem" used', async () => { + const output = getFoundExceptionListItemSchemaMock(); + const onSuccessMock = jest.fn(); + const spyOnFetchExceptionListsItemsByListIds = jest + .spyOn(api, 'fetchExceptionListsItemsByListIds') + .mockResolvedValue(output); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + await result.current.getExceptionListsItems({ + filterOptions: [], + lists: [{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }], + onError: jest.fn(), + onSuccess: onSuccessMock, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, + }); + + const expected: ApiCallByListIdProps = { + filterOptions: [], + http: mockKibanaHttpService, + listIds: ['list_id'], + namespaceTypes: ['single'], + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + signal: new AbortController().signal, + }; + + expect(spyOnFetchExceptionListsItemsByListIds).toHaveBeenCalledWith(expected); + expect(onSuccessMock).toHaveBeenCalled(); + }); + }); + + test('it does not invoke "fetchExceptionListsItemsByListIds" if no listIds', async () => { + const output = getFoundExceptionListItemSchemaMock(); + const onSuccessMock = jest.fn(); + const spyOnFetchExceptionListsItemsByListIds = jest + .spyOn(api, 'fetchExceptionListsItemsByListIds') + .mockResolvedValue(output); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + await result.current.getExceptionListsItems({ + filterOptions: [], + lists: [{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }], + onError: jest.fn(), + onSuccess: onSuccessMock, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: true, + }); + + expect(spyOnFetchExceptionListsItemsByListIds).not.toHaveBeenCalled(); + expect(onSuccessMock).toHaveBeenCalledWith({ + exceptions: [], + pagination: { + page: 0, + perPage: 20, + total: 0, + }, + }); + }); + }); + + test('invokes "onError" callback if "fetchExceptionListsItemsByListIds" fails', async () => { + const mockError = new Error('failed to delete item'); + jest.spyOn(api, 'fetchExceptionListsItemsByListIds').mockRejectedValue(mockError); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + await result.current.getExceptionListsItems({ + filterOptions: [], + lists: [{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }], + onError: onErrorMock, + onSuccess: jest.fn(), + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, + }); + + expect(onErrorMock).toHaveBeenCalledWith(mockError); + }); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts index 45e180d9d617c6..def2f2626b8ec3 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts @@ -9,7 +9,8 @@ import { useMemo } from 'react'; import * as Api from '../api'; import { HttpStart } from '../../../../../../src/core/public'; import { ExceptionListItemSchema, ExceptionListSchema } from '../../../common/schemas'; -import { ApiCallMemoProps } from '../types'; +import { ApiCallFindListsItemsMemoProps, ApiCallMemoProps } from '../types'; +import { getIdsAndNamespaces } from '../utils'; export interface ExceptionsApi { deleteExceptionItem: (arg: ApiCallMemoProps) => Promise; @@ -20,6 +21,7 @@ export interface ExceptionsApi { getExceptionList: ( arg: ApiCallMemoProps & { onSuccess: (arg: ExceptionListSchema) => void } ) => Promise; + getExceptionListsItems: (arg: ApiCallFindListsItemsMemoProps) => Promise; } export const useApi = (http: HttpStart): ExceptionsApi => { @@ -105,6 +107,59 @@ export const useApi = (http: HttpStart): ExceptionsApi => { onError(error); } }, + async getExceptionListsItems({ + lists, + filterOptions, + pagination, + showDetectionsListsOnly, + showEndpointListsOnly, + onSuccess, + onError, + }: ApiCallFindListsItemsMemoProps): Promise { + const abortCtrl = new AbortController(); + const { ids, namespaces } = getIdsAndNamespaces({ + lists, + showDetection: showDetectionsListsOnly, + showEndpoint: showEndpointListsOnly, + }); + + try { + if (ids.length > 0 && namespaces.length > 0) { + const { + data, + page, + per_page: perPage, + total, + } = await Api.fetchExceptionListsItemsByListIds({ + filterOptions, + http, + listIds: ids, + namespaceTypes: namespaces, + pagination, + signal: abortCtrl.signal, + }); + onSuccess({ + exceptions: data, + pagination: { + page, + perPage, + total, + }, + }); + } else { + onSuccess({ + exceptions: [], + pagination: { + page: 0, + perPage: pagination.perPage ?? 0, + total: 0, + }, + }); + } + } catch (error) { + onError(error); + } + }, }), [http] ); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts index f678ed4faeeda0..3a8b1713b901be 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts @@ -8,10 +8,9 @@ import { act, renderHook } from '@testing-library/react-hooks'; import * as api from '../api'; import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; -import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock'; import { ExceptionListItemSchema } from '../../../common/schemas'; -import { ExceptionList, UseExceptionListProps, UseExceptionListSuccess } from '../types'; +import { UseExceptionListProps, UseExceptionListSuccess } from '../types'; import { ReturnExceptionListAndItems, useExceptionList } from './use_exception_list'; @@ -21,9 +20,8 @@ describe('useExceptionList', () => { const onErrorMock = jest.fn(); beforeEach(() => { - jest.spyOn(api, 'fetchExceptionListById').mockResolvedValue(getExceptionListSchemaMock()); jest - .spyOn(api, 'fetchExceptionListItemsByListId') + .spyOn(api, 'fetchExceptionListsItemsByListIds') .mockResolvedValue(getFoundExceptionListItemSchemaMock()); }); @@ -39,17 +37,20 @@ describe('useExceptionList', () => { ReturnExceptionListAndItems >(() => useExceptionList({ - filterOptions: { filter: '', tags: [] }, + filterOptions: [], http: mockKibanaHttpService, lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], + matchFilters: false, onError: onErrorMock, pagination: { page: 1, perPage: 20, total: 0, }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, }) ); await waitForNextUpdate(); @@ -57,7 +58,6 @@ describe('useExceptionList', () => { expect(result.current).toEqual([ true, [], - [], { page: 1, perPage: 20, @@ -68,7 +68,7 @@ describe('useExceptionList', () => { }); }); - test('fetch exception list and items', async () => { + test('fetches exception items', async () => { await act(async () => { const onSuccessMock = jest.fn(); const { result, waitForNextUpdate } = renderHook< @@ -76,11 +76,12 @@ describe('useExceptionList', () => { ReturnExceptionListAndItems >(() => useExceptionList({ - filterOptions: { filter: '', tags: [] }, + filterOptions: [], http: mockKibanaHttpService, lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], + matchFilters: false, onError: onErrorMock, onSuccess: onSuccessMock, pagination: { @@ -88,56 +89,279 @@ describe('useExceptionList', () => { perPage: 20, total: 0, }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, }) ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params await waitForNextUpdate(); await waitForNextUpdate(); - const expectedListResult: ExceptionList[] = [ - { ...getExceptionListSchemaMock(), totalItems: 1 }, - ]; - const expectedListItemsResult: ExceptionListItemSchema[] = getFoundExceptionListItemSchemaMock() .data; const expectedResult: UseExceptionListSuccess = { exceptions: expectedListItemsResult, - lists: expectedListResult, pagination: { page: 1, perPage: 1, total: 1 }, }; expect(result.current).toEqual([ false, - expectedListResult, expectedListItemsResult, { page: 1, perPage: 1, total: 1, }, - result.current[4], + result.current[3], ]); expect(onSuccessMock).toHaveBeenCalledWith(expectedResult); }); }); - test('fetch a new exception list and its items', async () => { - const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById'); - const spyOnfetchExceptionListItemsByListId = jest.spyOn(api, 'fetchExceptionListItemsByListId'); + test('fetches only detection list items if "showDetectionsListsOnly" is true', async () => { + const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( + api, + 'fetchExceptionListsItemsByListIds' + ); + + await act(async () => { + const onSuccessMock = jest.fn(); + const { waitForNextUpdate } = renderHook( + () => + useExceptionList({ + filterOptions: [], + http: mockKibanaHttpService, + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + matchFilters: false, + onError: onErrorMock, + onSuccess: onSuccessMock, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showDetectionsListsOnly: true, + showEndpointListsOnly: false, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledWith({ + filterOptions: [], + http: mockKibanaHttpService, + listIds: ['list_id'], + namespaceTypes: ['single'], + pagination: { page: 1, perPage: 20 }, + signal: new AbortController().signal, + }); + }); + }); + + test('fetches only detection list items if "showEndpointListsOnly" is true', async () => { + const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( + api, + 'fetchExceptionListsItemsByListIds' + ); + + await act(async () => { + const onSuccessMock = jest.fn(); + const { waitForNextUpdate } = renderHook( + () => + useExceptionList({ + filterOptions: [], + http: mockKibanaHttpService, + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + matchFilters: false, + onError: onErrorMock, + onSuccess: onSuccessMock, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: true, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledWith({ + filterOptions: [], + http: mockKibanaHttpService, + listIds: ['list_id_endpoint'], + namespaceTypes: ['agnostic'], + pagination: { page: 1, perPage: 20 }, + signal: new AbortController().signal, + }); + }); + }); + + test('does not fetch items if no lists to fetch', async () => { + const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( + api, + 'fetchExceptionListsItemsByListIds' + ); + + await act(async () => { + const onSuccessMock = jest.fn(); + const { result, waitForNextUpdate } = renderHook< + UseExceptionListProps, + ReturnExceptionListAndItems + >(() => + useExceptionList({ + filterOptions: [], + http: mockKibanaHttpService, + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + ], + matchFilters: false, + onError: onErrorMock, + onSuccess: onSuccessMock, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: true, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionListsItemsByListIds).not.toHaveBeenCalled(); + expect(result.current).toEqual([ + false, + [], + { + page: 0, + perPage: 20, + total: 0, + }, + result.current[3], + ]); + }); + }); + + test('applies first filterOptions filter to all lists if "matchFilters" is true', async () => { + const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( + api, + 'fetchExceptionListsItemsByListIds' + ); + + await act(async () => { + const onSuccessMock = jest.fn(); + const { waitForNextUpdate } = renderHook( + () => + useExceptionList({ + filterOptions: [{ filter: 'host.name', tags: [] }], + http: mockKibanaHttpService, + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + matchFilters: true, + onError: onErrorMock, + onSuccess: onSuccessMock, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledWith({ + filterOptions: [ + { filter: 'host.name', tags: [] }, + { filter: 'host.name', tags: [] }, + ], + http: mockKibanaHttpService, + listIds: ['list_id', 'list_id_endpoint'], + namespaceTypes: ['single', 'agnostic'], + pagination: { page: 1, perPage: 20 }, + signal: new AbortController().signal, + }); + }); + }); + + test('fetches a new exception list and its items', async () => { + const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( + api, + 'fetchExceptionListsItemsByListIds' + ); const onSuccessMock = jest.fn(); await act(async () => { const { rerender, waitForNextUpdate } = renderHook< UseExceptionListProps, ReturnExceptionListAndItems >( - ({ filterOptions, http, lists, pagination, onError, onSuccess }) => - useExceptionList({ filterOptions, http, lists, onError, onSuccess, pagination }), + ({ + filterOptions, + http, + lists, + matchFilters, + pagination, + onError, + onSuccess, + showDetectionsListsOnly, + showEndpointListsOnly, + }) => + useExceptionList({ + filterOptions, + http, + lists, + matchFilters, + onError, + onSuccess, + pagination, + showDetectionsListsOnly, + showEndpointListsOnly, + }), { initialProps: { - filterOptions: { filter: '', tags: [] }, + filterOptions: [], http: mockKibanaHttpService, lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], + matchFilters: false, onError: onErrorMock, onSuccess: onSuccessMock, pagination: { @@ -145,16 +369,23 @@ describe('useExceptionList', () => { perPage: 20, total: 0, }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, }, } ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params await waitForNextUpdate(); + await waitForNextUpdate(); + rerender({ - filterOptions: { filter: '', tags: [] }, + filterOptions: [], http: mockKibanaHttpService, lists: [ { id: 'newListId', listId: 'new_list_id', namespaceType: 'single', type: 'detection' }, ], + matchFilters: false, onError: onErrorMock, onSuccess: onSuccessMock, pagination: { @@ -162,109 +393,92 @@ describe('useExceptionList', () => { perPage: 20, total: 0, }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, }); + // NOTE: Only need one call here because hook already initilaized await waitForNextUpdate(); - expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(2); - expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(2); + expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledTimes(2); }); }); test('fetches list and items when refreshExceptionList callback invoked', async () => { - const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById'); - const spyOnfetchExceptionListItemsByListId = jest.spyOn(api, 'fetchExceptionListItemsByListId'); + const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( + api, + 'fetchExceptionListsItemsByListIds' + ); await act(async () => { const { result, waitForNextUpdate } = renderHook< UseExceptionListProps, ReturnExceptionListAndItems >(() => useExceptionList({ - filterOptions: { filter: '', tags: [] }, + filterOptions: [], http: mockKibanaHttpService, lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], + matchFilters: false, onError: onErrorMock, pagination: { page: 1, perPage: 20, total: 0, }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, }) ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params await waitForNextUpdate(); await waitForNextUpdate(); - expect(typeof result.current[4]).toEqual('function'); + expect(typeof result.current[3]).toEqual('function'); - if (result.current[4] != null) { - result.current[4](); + if (result.current[3] != null) { + result.current[3](); } - + // NOTE: Only need one call here because hook already initilaized await waitForNextUpdate(); - expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(2); - expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(2); + expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledTimes(2); }); }); - test('invokes "onError" callback if "fetchExceptionListItemsByListId" fails', async () => { - const mockError = new Error('failed to fetch list items'); - const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById'); - const spyOnfetchExceptionListItemsByListId = jest - .spyOn(api, 'fetchExceptionListItemsByListId') + test('invokes "onError" callback if "fetchExceptionListsItemsByListIds" fails', async () => { + const mockError = new Error('failed to fetches list items'); + const spyOnfetchExceptionListsItemsByListIds = jest + .spyOn(api, 'fetchExceptionListsItemsByListIds') .mockRejectedValue(mockError); await act(async () => { const { waitForNextUpdate } = renderHook( () => useExceptionList({ - filterOptions: { filter: '', tags: [] }, - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - ], - onError: onErrorMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - }) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(1); - expect(onErrorMock).toHaveBeenCalledWith(mockError); - expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(1); - }); - }); - - test('invokes "onError" callback if "fetchExceptionListById" fails', async () => { - const mockError = new Error('failed to fetch list'); - jest.spyOn(api, 'fetchExceptionListById').mockRejectedValue(mockError); - - await act(async () => { - const { waitForNextUpdate } = renderHook( - () => - useExceptionList({ - filterOptions: { filter: '', tags: [] }, + filterOptions: [], http: mockKibanaHttpService, lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], + matchFilters: false, onError: onErrorMock, pagination: { page: 1, perPage: 20, total: 0, }, + showDetectionsListsOnly: false, + showEndpointListsOnly: false, }) ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params await waitForNextUpdate(); await waitForNextUpdate(); expect(onErrorMock).toHaveBeenCalledWith(mockError); + expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.ts index c639dcff8b5372..8097a7b8c5898e 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; -import { fetchExceptionListById, fetchExceptionListItemsByListId } from '../api'; -import { ExceptionIdentifiers, ExceptionList, Pagination, UseExceptionListProps } from '../types'; -import { ExceptionListItemSchema, NamespaceType } from '../../../common/schemas'; +import { fetchExceptionListsItemsByListIds } from '../api'; +import { FilterExceptionsOptions, Pagination, UseExceptionListProps } from '../types'; +import { ExceptionListItemSchema } from '../../../common/schemas'; +import { getIdsAndNamespaces } from '../utils'; type Func = () => void; export type ReturnExceptionListAndItems = [ boolean, - ExceptionList[], ExceptionListItemSchema[], Pagination, Func | null @@ -27,6 +27,10 @@ export type ReturnExceptionListAndItems = [ * @param onError error callback * @param onSuccess callback when all lists fetched successfully * @param filterOptions optional - filter by fields or tags + * @param showDetectionsListsOnly boolean, if true, only detection lists are searched + * @param showEndpointListsOnly boolean, if true, only endpoint lists are searched + * @param matchFilters boolean, if true, applies first filter in filterOptions to + * all lists * @param pagination optional * */ @@ -38,134 +42,112 @@ export const useExceptionList = ({ perPage: 20, total: 0, }, - filterOptions = { - filter: '', - tags: [], - }, + filterOptions, + showDetectionsListsOnly, + showEndpointListsOnly, + matchFilters, onError, onSuccess, }: UseExceptionListProps): ReturnExceptionListAndItems => { - const [exceptionLists, setExceptionLists] = useState([]); const [exceptionItems, setExceptionListItems] = useState([]); const [paginationInfo, setPagination] = useState(pagination); - const fetchExceptionList = useRef(null); + const fetchExceptionListsItems = useRef(null); const [loading, setLoading] = useState(true); - const tags = useMemo(() => filterOptions.tags.sort().join(), [filterOptions.tags]); - const listIds = useMemo( - () => - lists - .map((t) => t.id) - .sort() - .join(), - [lists] - ); + const { ids, namespaces } = getIdsAndNamespaces({ + lists, + showDetection: showDetectionsListsOnly, + showEndpoint: showEndpointListsOnly, + }); + const filters: FilterExceptionsOptions[] = + matchFilters && filterOptions.length > 0 ? ids.map(() => filterOptions[0]) : filterOptions; + const idsAsString: string = ids.join(','); + const namespacesAsString: string = namespaces.join(','); + const filterAsString: string = filterOptions.map(({ filter }) => filter).join(','); + const filterTagsAsString: string = filterOptions.map(({ tags }) => tags.join(',')).join(','); useEffect( () => { - let isSubscribed = false; - let abortCtrl: AbortController; - - const fetchLists = async (): Promise => { - isSubscribed = true; - abortCtrl = new AbortController(); - - // TODO: workaround until api updated, will be cleaned up - let exceptions: ExceptionListItemSchema[] = []; - let exceptionListsReturned: ExceptionList[] = []; - - const fetchData = async ({ - id, - namespaceType, - }: { - id: string; - namespaceType: NamespaceType; - }): Promise => { - try { - setLoading(true); - - const { - list_id, - namespace_type, - ...restOfExceptionList - } = await fetchExceptionListById({ - http, - id, - namespaceType, - signal: abortCtrl.signal, + let isSubscribed = true; + const abortCtrl = new AbortController(); + + const fetchData = async (): Promise => { + try { + setLoading(true); + + if (ids.length === 0 && isSubscribed) { + setPagination({ + page: 0, + perPage: pagination.perPage, + total: 0, }); - const fetchListItemsResult = await fetchExceptionListItemsByListId({ - filterOptions, + setExceptionListItems([]); + + if (onSuccess != null) { + onSuccess({ + exceptions: [], + pagination: { + page: 0, + perPage: pagination.perPage, + total: 0, + }, + }); + } + setLoading(false); + } else { + const { page, per_page, total, data } = await fetchExceptionListsItemsByListIds({ + filterOptions: filters, http, - listId: list_id, - namespaceType: namespace_type, - pagination, + listIds: ids, + namespaceTypes: namespaces, + pagination: { + page: pagination.page, + perPage: pagination.perPage, + }, signal: abortCtrl.signal, }); if (isSubscribed) { - exceptionListsReturned = [ - ...exceptionListsReturned, - { - list_id, - namespace_type, - ...restOfExceptionList, - totalItems: fetchListItemsResult.total, - }, - ]; - setExceptionLists(exceptionListsReturned); setPagination({ - page: fetchListItemsResult.page, - perPage: fetchListItemsResult.per_page, - total: fetchListItemsResult.total, + page, + perPage: per_page, + total, }); - - exceptions = [...exceptions, ...fetchListItemsResult.data]; - setExceptionListItems(exceptions); + setExceptionListItems(data); if (onSuccess != null) { onSuccess({ - exceptions, - lists: exceptionListsReturned, + exceptions: data, pagination: { - page: fetchListItemsResult.page, - perPage: fetchListItemsResult.per_page, - total: fetchListItemsResult.total, + page, + perPage: per_page, + total, }, }); } } - } catch (error) { - if (isSubscribed) { - setExceptionLists([]); - setExceptionListItems([]); - setPagination({ - page: 1, - perPage: 20, - total: 0, - }); - if (onError != null) { - onError(error); - } + } + } catch (error) { + if (isSubscribed) { + setExceptionListItems([]); + setPagination({ + page: 1, + perPage: 20, + total: 0, + }); + if (onError != null) { + onError(error); } } - }; - - // TODO: Workaround for now. Once api updated, we can pass in array of lists to fetch - await Promise.all( - lists.map( - ({ id, namespaceType }: ExceptionIdentifiers): Promise => - fetchData({ id, namespaceType }) - ) - ); + } if (isSubscribed) { setLoading(false); } }; - fetchLists(); + fetchData(); - fetchExceptionList.current = fetchLists; + fetchExceptionListsItems.current = fetchData; return (): void => { isSubscribed = false; abortCtrl.abort(); @@ -173,15 +155,15 @@ export const useExceptionList = ({ }, // eslint-disable-next-line react-hooks/exhaustive-deps [ http, - listIds, - setExceptionLists, + idsAsString, + namespacesAsString, setExceptionListItems, pagination.page, pagination.perPage, - filterOptions.filter, - tags, + filterAsString, + filterTagsAsString, ] ); - return [loading, exceptionLists, exceptionItems, paginationInfo, fetchExceptionList.current]; + return [loading, exceptionItems, paginationInfo, fetchExceptionListsItems.current]; }; diff --git a/x-pack/plugins/lists/public/exceptions/types.ts b/x-pack/plugins/lists/public/exceptions/types.ts index c0ec72e1c19eb2..ac21288848154c 100644 --- a/x-pack/plugins/lists/public/exceptions/types.ts +++ b/x-pack/plugins/lists/public/exceptions/types.ts @@ -44,7 +44,6 @@ export interface ExceptionList extends ExceptionListSchema { } export interface UseExceptionListSuccess { - lists: ExceptionList[]; exceptions: ExceptionListItemSchema[]; pagination: Pagination; } @@ -53,8 +52,11 @@ export interface UseExceptionListProps { http: HttpStart; lists: ExceptionIdentifiers[]; onError?: (arg: string[]) => void; - filterOptions?: FilterExceptionsOptions; + filterOptions: FilterExceptionsOptions[]; pagination?: Pagination; + showDetectionsListsOnly: boolean; + showEndpointListsOnly: boolean; + matchFilters: boolean; onSuccess?: (arg: UseExceptionListSuccess) => void; } @@ -67,9 +69,9 @@ export interface ExceptionIdentifiers { export interface ApiCallByListIdProps { http: HttpStart; - listId: string; - namespaceType: NamespaceType; - filterOptions?: FilterExceptionsOptions; + listIds: string[]; + namespaceTypes: NamespaceType[]; + filterOptions: FilterExceptionsOptions[]; pagination: Partial; signal: AbortSignal; } @@ -88,6 +90,16 @@ export interface ApiCallMemoProps { onSuccess: () => void; } +export interface ApiCallFindListsItemsMemoProps { + lists: ExceptionIdentifiers[]; + filterOptions: FilterExceptionsOptions[]; + pagination: Partial; + showDetectionsListsOnly: boolean; + showEndpointListsOnly: boolean; + onError: (arg: string[]) => void; + onSuccess: (arg: UseExceptionListSuccess) => void; +} + export interface AddExceptionListProps { http: HttpStart; list: CreateExceptionListSchema; diff --git a/x-pack/plugins/lists/public/exceptions/utils.test.ts b/x-pack/plugins/lists/public/exceptions/utils.test.ts new file mode 100644 index 00000000000000..cc1a96132b045f --- /dev/null +++ b/x-pack/plugins/lists/public/exceptions/utils.test.ts @@ -0,0 +1,105 @@ +/* + * 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 { getIdsAndNamespaces } from './utils'; + +describe('Exceptions utils', () => { + describe('#getIdsAndNamespaces', () => { + test('it returns empty arrays if no lists found', async () => { + const output = getIdsAndNamespaces({ + lists: [], + showDetection: false, + showEndpoint: false, + }); + + expect(output).toEqual({ ids: [], namespaces: [] }); + }); + + test('it returns all lists if "showDetection" and "showEndpoint" are "false"', async () => { + const output = getIdsAndNamespaces({ + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + showDetection: false, + showEndpoint: false, + }); + + expect(output).toEqual({ + ids: ['list_id', 'list_id_endpoint'], + namespaces: ['single', 'agnostic'], + }); + }); + + test('it returns only detections lists if "showDetection" is "true"', async () => { + const output = getIdsAndNamespaces({ + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + showDetection: true, + showEndpoint: false, + }); + + expect(output).toEqual({ + ids: ['list_id'], + namespaces: ['single'], + }); + }); + + test('it returns only endpoint lists if "showEndpoint" is "true"', async () => { + const output = getIdsAndNamespaces({ + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + showDetection: false, + showEndpoint: true, + }); + + expect(output).toEqual({ + ids: ['list_id_endpoint'], + namespaces: ['agnostic'], + }); + }); + + test('it returns only detection lists if both "showEndpoint" and "showDetection" are "true"', async () => { + const output = getIdsAndNamespaces({ + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + showDetection: true, + showEndpoint: true, + }); + + expect(output).toEqual({ + ids: ['list_id'], + namespaces: ['single'], + }); + }); + }); +}); diff --git a/x-pack/plugins/lists/public/exceptions/utils.ts b/x-pack/plugins/lists/public/exceptions/utils.ts new file mode 100644 index 00000000000000..2acb690d3822c2 --- /dev/null +++ b/x-pack/plugins/lists/public/exceptions/utils.ts @@ -0,0 +1,36 @@ +/* + * 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 { NamespaceType } from '../../common/schemas'; + +import { ExceptionIdentifiers } from './types'; + +export const getIdsAndNamespaces = ({ + lists, + showDetection, + showEndpoint, +}: { + lists: ExceptionIdentifiers[]; + showDetection: boolean; + showEndpoint: boolean; +}): { ids: string[]; namespaces: NamespaceType[] } => + lists + .filter((list) => { + if (showDetection) { + return list.type === 'detection'; + } else if (showEndpoint) { + return list.type === 'endpoint'; + } else { + return true; + } + }) + .reduce<{ ids: string[]; namespaces: NamespaceType[] }>( + (acc, { listId, namespaceType }) => ({ + ids: [...acc.ids, listId], + namespaces: [...acc.namespaces, namespaceType], + }), + { ids: [], namespaces: [] } + ); diff --git a/x-pack/plugins/lists/public/lists/api.ts b/x-pack/plugins/lists/public/lists/api.ts index 606109f1910c45..211b2445a0429c 100644 --- a/x-pack/plugins/lists/public/lists/api.ts +++ b/x-pack/plugins/lists/public/lists/api.ts @@ -29,7 +29,7 @@ import { listSchema, } from '../../common/schemas'; import { LIST_INDEX, LIST_ITEM_URL, LIST_PRIVILEGES_URL, LIST_URL } from '../../common/constants'; -import { validateEither } from '../../common/siem_common_deps'; +import { validateEither } from '../../common/shared_imports'; import { toError, toPromise } from '../common/fp_utils'; import { diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts index 22aa1fb59858b4..7fd07ed5fb8cd0 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateEndpointListItemSchemaDecoded, createEndpointListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts index b1e589be67cd14..91b6a328c86493 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_URL } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { createEndpointListSchema } from '../../common/schemas'; import { getExceptionListClient } from './utils/get_exception_list_client'; diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index ed58621dae973b..fc0473b2b37040 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateExceptionListItemSchemaDecoded, createExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts index fbe9c6ec9d83b6..08db0825e07bd9 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateExceptionListSchemaDecoded, createExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/create_list_index_route.ts b/x-pack/plugins/lists/server/routes/create_list_index_route.ts index 1bffdd6bd5b5fa..be08093dc7055e 100644 --- a/x-pack/plugins/lists/server/routes/create_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_index_route.ts @@ -7,7 +7,7 @@ import { IRouter } from 'kibana/server'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { LIST_INDEX } from '../../common/constants'; import { acknowledgeSchema } from '../../common/schemas'; diff --git a/x-pack/plugins/lists/server/routes/create_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_list_item_route.ts index 656d6af2c6c9ad..0a4a1c739ae7c0 100644 --- a/x-pack/plugins/lists/server/routes/create_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_item_route.ts @@ -9,7 +9,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { createListItemSchema, listItemSchema } from '../../common/schemas'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/create_list_route.ts b/x-pack/plugins/lists/server/routes/create_list_route.ts index 297dcfc49db345..90f5bf9b2c6509 100644 --- a/x-pack/plugins/lists/server/routes/create_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { CreateListSchemaDecoded, createListSchema, listSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts index 2d5028bd9525a1..380fdcf8620603 100644 --- a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { DeleteEndpointListItemSchemaDecoded, deleteEndpointListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts index 06ff0519254071..07e0fad20c900c 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { DeleteExceptionListItemSchemaDecoded, deleteExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts index f2bf517f55ae35..769ce732240b7a 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { DeleteExceptionListSchemaDecoded, deleteExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts index be58d8aeed17d5..aa587273036ae2 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_INDEX } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { acknowledgeSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts index 50313cd1294ae6..2284068552485c 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { deleteListItemSchema, listItemArraySchema, listItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_route.ts b/x-pack/plugins/lists/server/routes/delete_list_route.ts index 4eeb6d8f126ad2..f87645b79fc754 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { deleteListSchema, listSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts index 9f83761cc501a1..d6a459b3ac9611 100644 --- a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindEndpointListItemSchemaDecoded, findEndpointListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index 270aad85796b24..88643e53ff0a72 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindExceptionListItemSchemaDecoded, findExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts index c5cae7a1e0bb8b..41342261ef6817 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindExceptionListSchemaDecoded, findExceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/find_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_list_item_route.ts index 533dc74aa36949..454ea891857c34 100644 --- a/x-pack/plugins/lists/server/routes/find_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { FindListItemSchemaDecoded, findListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/find_list_route.ts b/x-pack/plugins/lists/server/routes/find_list_route.ts index 268eb36a5e26ec..d751214006dcc3 100644 --- a/x-pack/plugins/lists/server/routes/find_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { findListSchema, foundListSchema } from '../../common/schemas'; import { decodeCursor } from '../services/utils'; diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts index e162e7829e4562..ce5fdaccae2515 100644 --- a/x-pack/plugins/lists/server/routes/import_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { importListItemQuerySchema, listSchema } from '../../common/schemas'; import { ConfigType } from '../config'; diff --git a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts index d975e80079ab76..58cca0313006d2 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listItemSchema, patchListItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/patch_list_route.ts b/x-pack/plugins/lists/server/routes/patch_list_route.ts index 421f1279f26193..e33d8d7c9c5986 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listSchema, patchListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts index fd932746ce9902..e80347d97bb7a6 100644 --- a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { ReadEndpointListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts index fe8256fbda5cd6..0cfac6467f0899 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { ReadExceptionListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts index 0512876d298d49..d9359881616f4b 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { ReadExceptionListSchemaDecoded, exceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/read_list_index_route.ts index 87a4d85e0d254d..5524c1beeaa522 100644 --- a/x-pack/plugins/lists/server/routes/read_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_index_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_INDEX } from '../../common/constants'; import { buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listItemIndexExistSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_list_item_route.ts index b7cf2b9f7123b4..99d34d0fd84a62 100644 --- a/x-pack/plugins/lists/server/routes/read_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_item_route.ts @@ -9,7 +9,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; import { listItemArraySchema, listItemSchema, readListItemSchema } from '../../common/schemas'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/read_list_route.ts b/x-pack/plugins/lists/server/routes/read_list_route.ts index 4bce09ecd3bde9..da3cf73b56819b 100644 --- a/x-pack/plugins/lists/server/routes/read_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listSchema, readListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts index f717dc0fb33923..e0d6a0ffffa6b8 100644 --- a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { UpdateEndpointListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index f5e0e7ae757005..7e15f694aee13c 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { UpdateExceptionListItemSchemaDecoded, exceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts index 6fcee81ed573f2..bead10802df4f2 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { UpdateExceptionListSchemaDecoded, exceptionListSchema, diff --git a/x-pack/plugins/lists/server/routes/update_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_list_item_route.ts index d479bc63b64bd0..3490027b127478 100644 --- a/x-pack/plugins/lists/server/routes/update_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_item_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listItemSchema, updateListItemSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/update_list_route.ts b/x-pack/plugins/lists/server/routes/update_list_route.ts index 6206c0943a8f31..816ad13d3770ec 100644 --- a/x-pack/plugins/lists/server/routes/update_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_route.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; import { listSchema, updateListSchema } from '../../common/schemas'; import { getListClient } from '.'; diff --git a/x-pack/plugins/lists/server/routes/validate.ts b/x-pack/plugins/lists/server/routes/validate.ts index bbd4b0eaf0e33d..a7f5c96e13d7b0 100644 --- a/x-pack/plugins/lists/server/routes/validate.ts +++ b/x-pack/plugins/lists/server/routes/validate.ts @@ -8,7 +8,7 @@ import { ExceptionListClient } from '../services/exception_lists/exception_list_ import { MAX_EXCEPTION_LIST_SIZE } from '../../common/constants'; import { foundExceptionListItemSchema } from '../../common/schemas'; import { NamespaceType } from '../../common/schemas/types'; -import { validate } from '../../common/siem_common_deps'; +import { validate } from '../../common/shared_imports'; export const validateExceptionListSize = async ( exceptionLists: ExceptionListClient, diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_auto_id.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_auto_id.json index d63adc84a361db..f1281e2ea0560c 100644 --- a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_auto_id.json +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_auto_id.json @@ -1,5 +1,5 @@ { - "list_id": "endpoint_list", + "list_id": "simple_list", "_tags": ["endpoint", "process", "malware", "os:linux"], "tags": ["user added string for a tag", "malware"], "type": "simple", diff --git a/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts b/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts index 205d61f204ba6c..5c7243a1d15a3e 100644 --- a/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts +++ b/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts @@ -9,7 +9,7 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { CursorOrUndefined, SortFieldOrUndefined } from '../../../common/schemas'; -import { exactCheck } from '../../../common/siem_common_deps'; +import { exactCheck } from '../../../common/shared_imports'; /** * Used only internally for this ad-hoc opaque cursor structure to keep track of the diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index f91e272d625f69..4c829f8e75c201 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -6,8 +6,8 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { Dispatch } from 'redux'; -// @ts-ignore -import turf from 'turf'; +import bbox from '@turf/bbox'; +import { multiPoint } from '@turf/helpers'; import { FeatureCollection } from 'geojson'; import { MapStoreState } from '../reducers/store'; import { LAYER_TYPE, SOURCE_DATA_REQUEST_ID } from '../../common/constants'; @@ -368,7 +368,7 @@ export function fitToDataBounds() { return; } - const dataBounds = turfBboxToBounds(turf.bbox(turf.multiPoint(corners))); + const dataBounds = turfBboxToBounds(bbox(multiPoint(corners))); dispatch(setGotoWithBounds(scaleBounds(dataBounds, FIT_TO_BOUNDS_SCALE_FACTOR))); }; diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index ef0cfdf0b4742c..4914432f02de00 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -6,17 +6,19 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { Dispatch } from 'redux'; -// @ts-ignore -import turf from 'turf'; -import uuid from 'uuid/v4'; +import turfBboxPolygon from '@turf/bbox-polygon'; import turfBooleanContains from '@turf/boolean-contains'; +import uuid from 'uuid/v4'; + 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, @@ -124,13 +126,13 @@ export function mapExtentChanged(newMapConstants: { zoom: number; extent: MapExt if (extent) { let doesBufferContainExtent = false; if (buffer) { - const bufferGeometry = turf.bboxPolygon([ + const bufferGeometry = turfBboxPolygon([ buffer.minLon, buffer.minLat, buffer.maxLon, buffer.maxLat, ]); - const extentGeometry = turf.bboxPolygon([ + const extentGeometry = turfBboxPolygon([ extent.minLon, extent.minLat, extent.maxLon, @@ -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/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js index 33d5deef2e39f2..79eccf09b28889 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js @@ -6,7 +6,8 @@ import React from 'react'; import uuid from 'uuid/v4'; -import turf from 'turf'; +import turfBbox from '@turf/bbox'; +import { multiPoint } from '@turf/helpers'; import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; @@ -216,7 +217,7 @@ export class ESPewPewSource extends AbstractESAggSource { return null; } - return turfBboxToBounds(turf.bbox(turf.multiPoint(corners))); + return turfBboxToBounds(turfBbox(multiPoint(corners))); } canFormatFeatureProperties() { diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts index 8398bd7af39adc..147870dbef371f 100644 --- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts +++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import _ from 'lodash'; -// @ts-ignore -import turf from 'turf'; +import turfBboxPolygon from '@turf/bbox-polygon'; import turfBooleanContains from '@turf/boolean-contains'; import { isRefreshOnlyQuery } from './is_refresh_only_query'; import { ISource } from '../sources/source'; @@ -27,13 +26,13 @@ export function updateDueToExtent(prevMeta: DataMeta = {}, nextMeta: DataMeta = return NO_SOURCE_UPDATE_REQUIRED; } - const previousBufferGeometry = turf.bboxPolygon([ + const previousBufferGeometry = turfBboxPolygon([ previousBuffer.minLon, previousBuffer.minLat, previousBuffer.maxLon, previousBuffer.maxLat, ]); - const newBufferGeometry = turf.bboxPolygon([ + const newBufferGeometry = turfBboxPolygon([ newBuffer.minLon, newBuffer.minLat, newBuffer.maxLon, diff --git a/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts b/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts index aa78d7064fb0ae..76d305f0162d21 100644 --- a/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts +++ b/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore -import turf from 'turf'; +import turfBbox from '@turf/bbox'; import { FeatureCollection } from 'geojson'; import { MapExtent } from '../../../common/descriptor_types'; import { FEATURE_VISIBLE_PROPERTY_NAME } from '../../../common/constants'; @@ -28,7 +27,7 @@ export function getFeatureCollectionBounds( return null; } - const bbox = turf.bbox({ + const bbox = turfBbox({ type: 'FeatureCollection', features: visibleFeatures, }); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts index f2ceb8685d43e1..3e89d67e115047 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts +++ b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts @@ -6,9 +6,9 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -// @ts-ignore -import turf from 'turf'; -// @ts-ignore +// @ts-expect-error +import turfDistance from '@turf/distance'; +// @ts-expect-error import turfCircle from '@turf/circle'; type DrawCircleState = { @@ -75,7 +75,7 @@ export const DrawCircle = { // second click, finish draw // @ts-ignore this.updateUIClasses({ mouse: 'pointer' }); - state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, [ + state.circle.properties.radiusKm = turfDistance(state.circle.properties.center, [ e.lngLat.lng, e.lngLat.lat, ]); @@ -90,7 +90,7 @@ export const DrawCircle = { } const mouseLocation = [e.lngLat.lng, e.lngLat.lat]; - state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, mouseLocation); + state.circle.properties.radiusKm = turfDistance(state.circle.properties.center, mouseLocation); const newCircleFeature = turfCircle( state.circle.properties.center, state.circle.properties.radiusKm 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 8fb0ecb50b28bb..5b73cd9e772015 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 e65e37ef19809c..4b4bfb41990b92 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 12d6d75ac57ba0..b77bf208c58653 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 c839122ab90b19..e55160383a8f30 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 cf6f51c8aeacfe..5d02160fc3eb53 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 2575bbb9df9209..4692bb1db34778 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 2340e3716547ba..be474b43da81a6 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 b26c44df251047..d945aa9623b212 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 19118c6130805d..4cdba13bd85d26 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 69d6dbbe0c4d32..60e8dc9cd574cb 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 b466f254e4d08e..4e17241752f537 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/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index cf645404860f5a..cc3af9d7f49805 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -25,6 +25,7 @@ export type MlDependencies = Omit & MlStartDepende interface AppProps { coreStart: CoreStart; deps: MlDependencies; + appMountParams: AppMountParameters; } const localStorage = new Storage(window.localStorage); @@ -46,8 +47,9 @@ export interface MlServicesContext { export type MlGlobalServices = ReturnType; -const App: FC = ({ coreStart, deps }) => { +const App: FC = ({ coreStart, deps, appMountParams }) => { const pageDeps = { + history: appMountParams.history, indexPatterns: deps.data.indexPatterns, config: coreStart.uiSettings!, setBreadcrumbs: coreStart.chrome!.setBreadcrumbs, @@ -104,7 +106,11 @@ export const renderApp = ( appMountParams.onAppLeave((actions) => actions.default()); const mlLicense = setLicenseCache(deps.licensing, [ - () => ReactDOM.render(, appMountParams.element), + () => + ReactDOM.render( + , + appMountParams.element + ), ]); return () => { diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index 4850d583a626c8..f603264896cd32 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -62,6 +62,8 @@ class LinksMenuUI extends Component { const timestamp = record.timestamp; const configuredUrlValue = customUrl.url_value; const timeRangeInterval = parseInterval(customUrl.time_range); + const basePath = this.props.kibana.services.http.basePath.get(); + if (configuredUrlValue.includes('$earliest$')) { let earliestMoment = moment(timestamp); if (timeRangeInterval !== null) { @@ -117,7 +119,7 @@ class LinksMenuUI extends Component { // Replace any tokens in the configured url_value with values from the source record, // and then open link in a new tab/window. const urlPath = replaceStringTokens(customUrl.url_value, record, true); - openCustomUrlWindow(urlPath, customUrl); + openCustomUrlWindow(urlPath, customUrl, basePath); }) .catch((resp) => { console.log('openCustomUrl(): error loading categoryDefinition:', resp); @@ -136,7 +138,7 @@ class LinksMenuUI extends Component { // Replace any tokens in the configured url_value with values from the source record, // and then open link in a new tab/window. const urlPath = getUrlForRecord(customUrl, record); - openCustomUrlWindow(urlPath, customUrl); + openCustomUrlWindow(urlPath, customUrl, basePath); } }; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts index 0f071a42a56889..8a43ae12deb21c 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts @@ -5,6 +5,7 @@ */ export { useMlKibana, StartServices, MlKibanaReactContextValue } from './kibana_context'; +export { useNavigateToPath, NavigateToPath } from './use_navigate_to_path'; export { useUiSettings } from './use_ui_settings_context'; export { useTimefilter } from './use_timefilter'; export { useNotifications } from './use_notifications_context'; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts new file mode 100644 index 00000000000000..f2db970bf5057e --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts @@ -0,0 +1,34 @@ +/* + * 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 { useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; +import { PLUGIN_ID } from '../../../../common/constants/app'; + +import { useMlKibana } from './kibana_context'; + +export type NavigateToPath = ReturnType; + +export const useNavigateToPath = () => { + const { + services: { + application: { getUrlForApp, navigateToUrl }, + }, + } = useMlKibana(); + + const location = useLocation(); + + return useMemo( + () => (path: string | undefined, preserveSearch = false) => { + navigateToUrl( + getUrlForApp(PLUGIN_ID, { + path: `${path}${preserveSearch === true ? location.search : ''}`, + }) + ); + }, + [location] + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx index b6b335afa53f5c..183cbe084f9b35 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx @@ -7,17 +7,13 @@ import React, { FC, Fragment } from 'react'; import { EuiCard, EuiHorizontalRule, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useNavigateToPath } from '../../../../../contexts/kibana'; export const BackToListPanel: FC = () => { - const { - services: { - application: { navigateToUrl }, - }, - } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const redirectToAnalyticsManagementPage = async () => { - await navigateToUrl('#/data_frame_analytics?'); + await navigateToPath('/data_frame_analytics'); }; return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx index 6b99787a6c9a9b..010aa7b8513b5d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx @@ -13,7 +13,7 @@ import { DeepReadonly } from '../../../../../../../common/types/common'; import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; import { CreateAnalyticsFormProps, DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES, @@ -350,11 +350,11 @@ export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { export const useNavigateToWizardWithClonedJob = () => { const { services: { - application: { navigateToUrl }, notifications: { toasts }, savedObjects, }, } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const savedObjectsClient = savedObjects.client; @@ -395,8 +395,8 @@ export const useNavigateToWizardWithClonedJob = () => { } if (sourceIndexId) { - await navigateToUrl( - `ml#/data_frame_analytics/new_job?index=${encodeURIComponent(sourceIndexId)}&jobId=${ + await navigateToPath( + `/data_frame_analytics/new_job?index=${encodeURIComponent(sourceIndexId)}&jobId=${ item.config.id }` ); @@ -424,7 +424,7 @@ export const CloneButton: FC = ({ isDisabled, onClick }) => { iconType="copy" isDisabled={isDisabled} onClick={onClick} - size="s" + size="xs" > {buttonText} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button.tsx index c83fb6cbac387a..2bc3935c3b9f1d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button.tsx @@ -30,7 +30,7 @@ export const DeleteButton: FC = ({ isDisabled, item, onClick iconType="trash" isDisabled={isDisabled} onClick={onClick} - size="s" + size="xs" > {buttonText} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button.tsx index 764b421821ad0d..e17862bf326f1a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button.tsx @@ -29,7 +29,7 @@ export const EditButton: FC = ({ isDisabled, onClick }) => { iconType="pencil" isDisabled={isDisabled} onClick={onClick} - size="s" + size="xs" > {buttonText} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button.tsx index 3192a30f8312e0..98b9279d8469a1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button.tsx @@ -38,7 +38,7 @@ export const StartButton: FC = ({ iconType="play" isDisabled={isDisabled} onClick={onClick} - size="s" + size="xs" > {buttonText} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button.tsx index a3e8f16daf5efe..3bac183d9f3913 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button.tsx @@ -33,7 +33,7 @@ export const StopButton: FC = ({ isDisabled, item, onClick }) = iconType="stop" isDisabled={isDisabled} onClick={onClick} - size="s" + size="xs" > {buttonText} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx index e31670ea42ceba..e123af204b5154 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx @@ -12,11 +12,9 @@ import { DataFrameAnalyticsListRow } from '../analytics_list/common'; import { ViewButton } from './view_button'; -export const getViewAction = ( - isManagementTable: boolean = false -): EuiTableActionsColumnType['actions'][number] => ({ +export const getViewAction = (): EuiTableActionsColumnType< + DataFrameAnalyticsListRow +>['actions'][number] => ({ isPrimary: true, - render: (item: DataFrameAnalyticsListRow) => ( - - ), + render: (item: DataFrameAnalyticsListRow) => , }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx index 8a4509ebfd007c..9472a3af852fc5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import { getAnalysisType } from '../../../../common/analytics'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useNavigateToPath } from '../../../../../contexts/kibana'; import { getResultsUrl, DataFrameAnalyticsListRow } from '../analytics_list/common'; @@ -17,23 +17,15 @@ import { getViewLinkStatus } from './get_view_link_status'; interface ViewButtonProps { item: DataFrameAnalyticsListRow; - isManagementTable: boolean; } -export const ViewButton: FC = ({ item, isManagementTable }) => { - const { - services: { - application: { navigateToUrl, navigateToApp }, - }, - } = useMlKibana(); +export const ViewButton: FC = ({ item }) => { + const navigateToPath = useNavigateToPath(); const { disabled, tooltipContent } = getViewLinkStatus(item); const analysisType = getAnalysisType(item.config.analysis); - const url = getResultsUrl(item.id, analysisType); - const navigator = isManagementTable - ? () => navigateToApp('ml', { path: url }) - : () => navigateToUrl(url); + const onClickHandler = () => navigateToPath(getResultsUrl(item.id, analysisType)); const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.viewActionName', { defaultMessage: 'View', @@ -47,8 +39,8 @@ export const ViewButton: FC = ({ item, isManagementTable }) => flush="left" iconType="visTable" isDisabled={disabled} - onClick={navigator} - size="s" + onClick={onClickHandler} + size="xs" > {buttonText} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx index bc02c81bac0f05..373b9991d4d3c9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx @@ -43,7 +43,7 @@ export const useActions = ( let modals: JSX.Element | null = null; const actions: EuiTableActionsColumnType['actions'] = [ - getViewAction(isManagementTable), + getViewAction(), ]; // isManagementTable will be the same for the lifecycle of the component diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx index b03a58a02309db..29d495062e3093 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx @@ -17,7 +17,7 @@ import { } from '@elastic/eui'; import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/saved_objects/public'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; const fixedPageSize: number = 8; @@ -27,16 +27,13 @@ interface Props { export const SourceSelection: FC = ({ onClose }) => { const { - services: { - application: { navigateToUrl }, - savedObjects, - uiSettings, - }, + services: { savedObjects, uiSettings }, } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const onSearchSelected = async (id: string, type: string) => { - await navigateToUrl( - `ml#/data_frame_analytics/new_job?${ + await navigateToPath( + `/data_frame_analytics/new_job?${ type === 'index-pattern' ? 'index' : 'savedSearchId' }=${encodeURIComponent(id)}` ); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index fd86d9f48f46d5..769b83c03110b1 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { isFullLicense } from '../license'; -import { useTimefilter, useMlKibana } from '../contexts/kibana'; +import { useTimefilter, useMlKibana, useNavigateToPath } from '../contexts/kibana'; import { NavigationMenu } from '../components/navigation_menu'; import { getMaxBytesFormatted } from './file_based/components/utils'; @@ -54,6 +54,7 @@ export const DatavisualizerSelector: FC = () => { const { services: { licenseManagement }, } = useMlKibana(); + const navigateToPath = useNavigateToPath(); const startTrialVisible = licenseManagement !== undefined && @@ -124,7 +125,7 @@ export const DatavisualizerSelector: FC = () => { footer={ navigateToPath('/filedatavisualizer')} data-test-subj="mlDataVisualizerUploadFileButton" > { footer={ navigateToPath('/datavisualizer_index_select')} data-test-subj="mlDataVisualizerSelectIndexButton" > = ({ job, customUrls, setCustomUrls }) => { const { - services: { notifications }, + services: { http, notifications }, } = useMlKibana(); const [expandedUrlIndex, setExpandedUrlIndex] = useState(null); @@ -103,7 +103,7 @@ export const CustomUrlList: FC = ({ job, customUrls, setCust if (index < customUrls.length) { getTestUrl(job, customUrls[index]) .then((testUrl) => { - openCustomUrlWindow(testUrl, customUrls[index]); + openCustomUrlWindow(testUrl, customUrls[index], http.basePath.get()); }) .catch((resp) => { // eslint-disable-next-line no-console diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx index 7af27fc22e34c2..468efcf013e9bc 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx @@ -160,13 +160,16 @@ class CustomUrlsUI extends Component { }; onTestButtonClick = () => { - const { toasts } = this.props.kibana.services.notifications; + const { + http: { basePath }, + notifications: { toasts }, + } = this.props.kibana.services; const job = this.props.job; buildCustomUrlFromSettings(this.state.editorSettings as CustomUrlSettings) .then((customUrl) => { getTestUrl(job, customUrl) .then((testUrl) => { - openCustomUrlWindow(testUrl, customUrl); + openCustomUrlWindow(testUrl, customUrl, basePath.get()); }) .catch((resp) => { // eslint-disable-next-line no-console diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index 92e65e580fc01e..8ab45dc24aa178 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -5,8 +5,10 @@ */ import { i18n } from '@kbn/i18n'; + import { Job, Datafeed, Detector } from '../../../../../../../common/types/anomaly_detection_jobs'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; +import { NavigateToPath } from '../../../../../contexts/kibana'; import { ML_JOB_AGGREGATION, SPARSE_DATA_AGGREGATIONS, @@ -20,12 +22,7 @@ import { mlCategory, } from '../../../../../../../common/types/fields'; import { mlJobService } from '../../../../../services/job_service'; -import { - JobCreatorType, - isMultiMetricJobCreator, - isPopulationJobCreator, - isCategorizationJobCreator, -} from '../index'; +import { JobCreatorType } from '../index'; import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../../common/constants/new_job'; const getFieldByIdFactory = (additionalFields: Field[]) => (id: string) => { @@ -247,43 +244,33 @@ function stashCombinedJob( mlJobService.tempJobCloningObjects.calendars = jobCreator.calendars; } -export function convertToMultiMetricJob(jobCreator: JobCreatorType) { +export function convertToMultiMetricJob( + jobCreator: JobCreatorType, + navigateToPath: NavigateToPath +) { jobCreator.createdBy = CREATED_BY_LABEL.MULTI_METRIC; jobCreator.modelPlot = false; stashCombinedJob(jobCreator, true, true); - window.location.href = window.location.href.replace( - JOB_TYPE.SINGLE_METRIC, - JOB_TYPE.MULTI_METRIC - ); + navigateToPath(`jobs/new_job/${JOB_TYPE.MULTI_METRIC}`, true); } -export function convertToAdvancedJob(jobCreator: JobCreatorType) { +export function convertToAdvancedJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { jobCreator.createdBy = null; stashCombinedJob(jobCreator, true, true); - let jobType = JOB_TYPE.SINGLE_METRIC; - if (isMultiMetricJobCreator(jobCreator)) { - jobType = JOB_TYPE.MULTI_METRIC; - } else if (isPopulationJobCreator(jobCreator)) { - jobType = JOB_TYPE.POPULATION; - } else if (isCategorizationJobCreator(jobCreator)) { - jobType = JOB_TYPE.CATEGORIZATION; - } - - window.location.href = window.location.href.replace(jobType, JOB_TYPE.ADVANCED); + navigateToPath(`jobs/new_job/${JOB_TYPE.ADVANCED}`, true); } -export function resetJob(jobCreator: JobCreatorType) { +export function resetJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { jobCreator.jobId = ''; stashCombinedJob(jobCreator, true, true); - - window.location.href = '#/jobs/new_job'; + navigateToPath('/jobs/new_job'); } -export function advancedStartDatafeed(jobCreator: JobCreatorType) { +export function advancedStartDatafeed(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { stashCombinedJob(jobCreator, false, false); - window.location.href = '#/jobs'; + navigateToPath('/jobs'); } export function aggFieldPairsCanBeCharted(afs: AggFieldPair[]) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx index 60b034b516939a..7999ce46bc9ed4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx @@ -22,10 +22,18 @@ import { i18n } from '@kbn/i18n'; import { JobCreatorContext } from '../../../../../job_creator_context'; import { Description } from './description'; import { ml } from '../../../../../../../../../services/ml_api_service'; +import { PLUGIN_ID } from '../../../../../../../../../../../common/constants/app'; import { Calendar } from '../../../../../../../../../../../common/types/calendars'; +import { useMlKibana } from '../../../../../../../../../contexts/kibana'; import { GLOBAL_CALENDAR } from '../../../../../../../../../../../common/constants/calendars'; export const CalendarsSelection: FC = () => { + const { + services: { + application: { getUrlForApp }, + }, + } = useMlKibana(); + const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); const [selectedCalendars, setSelectedCalendars] = useState(jobCreator.calendars); const [selectedOptions, setSelectedOptions] = useState>>( @@ -64,7 +72,9 @@ export const CalendarsSelection: FC = () => { }, }; - const manageCalendarsHref = '#/settings/calendars_list'; + const manageCalendarsHref = getUrlForApp(PLUGIN_ID, { + path: '/settings/calendars_list', + }); return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx index 3bcac1cf6876ce..e14e29cc965d36 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx @@ -8,21 +8,25 @@ import React, { Fragment, FC, useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import { useNavigateToPath } from '../../../../../../../contexts/kibana'; + +import { convertToMultiMetricJob } from '../../../../../common/job_creator/util/general'; + import { JobCreatorContext } from '../../../job_creator_context'; + import { BucketSpan } from '../bucket_span'; import { SparseDataSwitch } from '../sparse_data'; -import { convertToMultiMetricJob } from '../../../../../common/job_creator/util/general'; - interface Props { setIsValid: (proceed: boolean) => void; } export const SingleMetricSettings: FC = ({ setIsValid }) => { const { jobCreator } = useContext(JobCreatorContext); + const navigateToPath = useNavigateToPath(); const convertToMultiMetric = () => { - convertToMultiMetricJob(jobCreator); + convertToMultiMetricJob(jobCreator, navigateToPath); }; return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index d8cd0f5e4f1f00..5ef59951c43cce 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; import { PreviousButton } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; @@ -42,6 +42,9 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => const { services: { notifications }, } = useMlKibana(); + + const navigateToPath = useNavigateToPath(); + const { jobCreator, jobValidator, jobValidatorUpdated, resultsLoader } = useContext( JobCreatorContext ); @@ -87,7 +90,7 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => try { await jobCreator.createJob(); await jobCreator.createDatafeed(); - advancedStartDatafeed(jobCreator); + advancedStartDatafeed(jobCreator, navigateToPath); } catch (error) { // catch and display all job creation errors const { toasts } = notifications; @@ -112,11 +115,11 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => } function clickResetJob() { - resetJob(jobCreator); + resetJob(jobCreator, navigateToPath); } const convertToAdvanced = () => { - convertToAdvancedJob(jobCreator); + convertToAdvancedJob(jobCreator, navigateToPath); }; useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx index 0f990a07aaf21f..0caf97b0006d4f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx @@ -16,7 +16,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { SavedObjectFinderUi } from '../../../../../../../../../src/plugins/saved_objects/public'; -import { useMlKibana } from '../../../../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../../../../contexts/kibana'; export interface PageProps { nextStepPath: string; @@ -25,11 +25,14 @@ export interface PageProps { export const Page: FC = ({ nextStepPath }) => { const RESULTS_PER_PAGE = 20; const { uiSettings, savedObjects } = useMlKibana().services; + const navigateToPath = useNavigateToPath(); const onObjectSelection = (id: string, type: string) => { - window.location.href = `${nextStepPath}?${ - type === 'index-pattern' ? 'index' : 'savedSearchId' - }=${encodeURIComponent(id)}`; + navigateToPath( + `${nextStepPath}?${type === 'index-pattern' ? 'index' : 'savedSearchId'}=${encodeURIComponent( + id + )}` + ); }; return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx index 3bfe0569e75be0..be0135ec3f1e09 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx @@ -18,6 +18,7 @@ import { EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useNavigateToPath } from '../../../../contexts/kibana'; import { useMlContext } from '../../../../contexts/ml'; import { isSavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { DataRecognizer } from '../../../../components/data_recognizer'; @@ -28,6 +29,8 @@ import { CategorizationIcon } from './categorization_job_icon'; export const Page: FC = () => { const mlContext = useMlContext(); + const navigateToPath = useNavigateToPath(); + const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); const { currentSavedSearch, currentIndexPattern } = mlContext; @@ -68,25 +71,23 @@ export const Page: FC = () => { }, }; - const getUrl = (basePath: string) => { + const getUrlParams = () => { return !isSavedSearchSavedObject(currentSavedSearch) - ? `${basePath}?index=${currentIndexPattern.id}` - : `${basePath}?savedSearchId=${currentSavedSearch.id}`; + ? `?index=${currentIndexPattern.id}` + : `?savedSearchId=${currentSavedSearch.id}`; }; const addSelectionToRecentlyAccessed = () => { const title = !isSavedSearchSavedObject(currentSavedSearch) ? currentIndexPattern.title : (currentSavedSearch.attributes.title as string); - const url = getUrl(''); - addItemToRecentlyAccessed('jobs/new_job/datavisualizer', title, url); - - window.location.href = getUrl('#jobs/new_job/datavisualizer'); + addItemToRecentlyAccessed('jobs/new_job/datavisualizer', title, ''); + navigateToPath(`/jobs/new_job/datavisualizer${getUrlParams()}`); }; const jobTypes = [ { - href: getUrl('#jobs/new_job/single_metric'), + onClick: () => navigateToPath(`/jobs/new_job/single_metric${getUrlParams()}`), icon: { type: 'createSingleMetricJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.singleMetricAriaLabel', { @@ -102,7 +103,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkSingleMetricJob', }, { - href: getUrl('#jobs/new_job/multi_metric'), + onClick: () => navigateToPath(`/jobs/new_job/multi_metric${getUrlParams()}`), icon: { type: 'createMultiMetricJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.multiMetricAriaLabel', { @@ -119,7 +120,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkMultiMetricJob', }, { - href: getUrl('#jobs/new_job/population'), + onClick: () => navigateToPath(`/jobs/new_job/population${getUrlParams()}`), icon: { type: 'createPopulationJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.populationAriaLabel', { @@ -136,7 +137,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkPopulationJob', }, { - href: getUrl('#jobs/new_job/advanced'), + onClick: () => navigateToPath(`/jobs/new_job/advanced${getUrlParams()}`), icon: { type: 'createAdvancedJob', ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.advancedAriaLabel', { @@ -153,7 +154,7 @@ export const Page: FC = () => { id: 'mlJobTypeLinkAdvancedJob', }, { - href: getUrl('#jobs/new_job/categorization'), + onClick: () => navigateToPath(`/jobs/new_job/categorization${getUrlParams()}`), icon: { type: CategorizationIcon, ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.categorizationAriaLabel', { @@ -247,11 +248,11 @@ export const Page: FC = () => { - {jobTypes.map(({ href, icon, title, description, id }) => ( + {jobTypes.map(({ onClick, icon, title, description, id }) => ( { /> } onClick={addSelectionToRecentlyAccessed} - href={getUrl('#jobs/new_job/datavisualizer')} /> diff --git a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts index 82e233745f9e4e..d0a4f999af7582 100644 --- a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts +++ b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts @@ -4,47 +4,82 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiBreadcrumb } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; + import { ChromeBreadcrumb } from 'kibana/public'; +import { NavigateToPath } from '../contexts/kibana'; + export const ML_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.machineLearningBreadcrumbLabel', { defaultMessage: 'Machine Learning', }), - href: '#/', + href: '/', }); -export const SETTINGS: ChromeBreadcrumb = Object.freeze({ +export const SETTINGS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.settingsBreadcrumbLabel', { defaultMessage: 'Settings', }), - href: '#/settings', + href: '/settings', }); export const ANOMALY_DETECTION_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.anomalyDetectionBreadcrumbLabel', { defaultMessage: 'Anomaly Detection', }), - href: '#/jobs', + href: '/jobs', }); export const DATA_FRAME_ANALYTICS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.dataFrameAnalyticsLabel', { defaultMessage: 'Data Frame Analytics', }), - href: '#/data_frame_analytics', + href: '/data_frame_analytics', }); export const DATA_VISUALIZER_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.datavisualizerBreadcrumbLabel', { defaultMessage: 'Data Visualizer', }), - href: '#/datavisualizer', + href: '/datavisualizer', }); export const CREATE_JOB_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.createJobsBreadcrumbLabel', { defaultMessage: 'Create job', }), - href: '#/jobs/new_job', + href: '/jobs/new_job', }); + +const breadcrumbs = { + ML_BREADCRUMB, + SETTINGS_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + DATA_FRAME_ANALYTICS_BREADCRUMB, + DATA_VISUALIZER_BREADCRUMB, + CREATE_JOB_BREADCRUMB, +}; +type Breadcrumb = keyof typeof breadcrumbs; + +export const breadcrumbOnClickFactory = ( + path: string | undefined, + navigateToPath: NavigateToPath +): EuiBreadcrumb['onClick'] => { + return (e) => { + e.preventDefault(); + navigateToPath(path); + }; +}; + +export const getBreadcrumbWithUrlForApp = ( + breadcrumbName: Breadcrumb, + navigateToPath: NavigateToPath +): EuiBreadcrumb => { + return { + ...breadcrumbs[breadcrumbName], + onClick: breadcrumbOnClickFactory(breadcrumbs[breadcrumbName].href, navigateToPath), + }; +}; diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx index f1b8083f19ccf9..56c9a19723fba1 100644 --- a/x-pack/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/plugins/ml/public/application/routing/router.tsx @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; -import { HashRouter, Route, RouteProps } from 'react-router-dom'; +import React, { useEffect, FC } from 'react'; +import { useHistory, useLocation, Router, Route, RouteProps } from 'react-router-dom'; import { Location } from 'history'; -import { IUiSettingsClient, ChromeStart } from 'kibana/public'; +import { AppMountParameters, IUiSettingsClient, ChromeStart } from 'kibana/public'; import { ChromeBreadcrumb } from 'kibana/public'; import { IndexPatternsContract } from 'src/plugins/data/public'; + +import { useNavigateToPath } from '../contexts/kibana'; import { MlContext, MlContextValue } from '../contexts/ml'; import { UrlStateProvider } from '../util/url_state'; @@ -33,9 +35,10 @@ export interface PageProps { } interface PageDependencies { - setBreadcrumbs: ChromeStart['setBreadcrumbs']; - indexPatterns: IndexPatternsContract; config: IUiSettingsClient; + history: AppMountParameters['history']; + indexPatterns: IndexPatternsContract; + setBreadcrumbs: ChromeStart['setBreadcrumbs']; } export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children }) => { @@ -44,28 +47,74 @@ export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children ); }; -export const MlRouter: FC<{ pageDeps: PageDependencies }> = ({ pageDeps }) => { - const setBreadcrumbs = pageDeps.setBreadcrumbs; +/** + * This component provides compatibility with the previous hash based + * URL format used by HashRouter. Even if we migrate all internal URLs + * to one without hashes, we should keep this redirect in place to + * support legacy bookmarks and as a fallback for unmigrated URLs + * from other plugins. + */ +const LegacyHashUrlRedirect: FC = ({ children }) => { + const history = useHistory(); + const location = useLocation(); + + useEffect(() => { + if (location.hash.startsWith('#/')) { + history.push(location.hash.replace('#', '')); + } + }, [location.hash]); + + return <>{children}; +}; + +/** + * `MlRoutes` creates a React Router Route for every routeFactory + * and passes on the `navigateToPath` helper. + */ +const MlRoutes: FC<{ + pageDeps: PageDependencies; +}> = ({ pageDeps }) => { + const navigateToPath = useNavigateToPath(); return ( - + <> + {Object.entries(routes).map(([name, routeFactory]) => { + const route = routeFactory(navigateToPath); + + return ( + { + window.setTimeout(() => { + pageDeps.setBreadcrumbs(route.breadcrumbs); + }); + return route.render(props, pageDeps); + }} + /> + ); + })} + + ); +}; + +/** + * `MlRouter` is based on `BrowserRouter` and takes in `ScopedHistory` provided + * by Kibana. `LegacyHashUrlRedirect` provides compatibility with legacy hash based URLs. + * `UrlStateProvider` manages state stored in `_g/_a` URL parameters which can be + * use in components further down via `useUrlState()`. + */ +export const MlRouter: FC<{ + pageDeps: PageDependencies; +}> = ({ pageDeps }) => ( + +
- {Object.entries(routes).map(([name, route]) => ( - { - window.setTimeout(() => { - setBreadcrumbs(route.breadcrumbs); - }); - return route.render(props, pageDeps); - }} - /> - ))} +
-
- ); -}; + + +); diff --git a/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx b/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx index bd7fc434b36ac3..42d9a59d15bfa2 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/access_denied.tsx @@ -19,11 +19,11 @@ const breadcrumbs = [ }, ]; -export const accessDeniedRoute: MlRoute = { +export const accessDeniedRouteFactory = (): MlRoute => ({ path: '/access-denied', render: (props, deps) => , breadcrumbs, -}; +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, {}); diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx index ebc7bd95fb0c36..8c45398098b2f7 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_creation.tsx @@ -5,29 +5,31 @@ */ import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; import { parse } from 'query-string'; + +import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_creation'; -import { ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel', { - defaultMessage: 'Data Frame Analytics', - }), - href: '#/data_frame_analytics', - }, -]; - -export const analyticsJobsCreationRoute: MlRoute = { +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const analyticsJobsCreationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/data_frame_analytics/new_job', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel', { + defaultMessage: 'Data Frame Analytics', + }), + onClick: breadcrumbOnClickFactory('/data_frame_analytics', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { index, jobId, savedSearchId }: Record = parse(location.search, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index 1ffea2c06faf40..47cc002ab4d830 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -4,33 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'query-string'; import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; +import { parse } from 'query-string'; import { decode } from 'rison-node'; + +import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_exploration'; import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common/analytics'; -import { ML_BREADCRUMB, DATA_FRAME_ANALYTICS_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_FRAME_ANALYTICS_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel', { - defaultMessage: 'Exploration', - }), - href: '', - }, -]; - -export const analyticsJobExplorationRoute: MlRoute = { +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const analyticsJobExplorationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/data_frame_analytics/exploration', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel', { + defaultMessage: 'Exploration', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx index 2623136d1e98fa..b6ef9ea81b4bab 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx @@ -7,28 +7,28 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_management'; -import { ML_BREADCRUMB, DATA_FRAME_ANALYTICS_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_FRAME_ANALYTICS_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel', { - defaultMessage: 'Job Management', - }), - href: '', - }, -]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const analyticsJobsListRoute: MlRoute = { +export const analyticsJobsListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/data_frame_analytics', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel', { + defaultMessage: 'Job Management', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx index fc2d517b2edb15..efe5c3cba04a55 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx @@ -11,21 +11,24 @@ import React, { FC } from 'react'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { DatavisualizerSelector } from '../../../datavisualizer'; import { checkBasicLicense } from '../../../license'; import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; -import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const selectorRoute: MlRoute = { +export const selectorRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/datavisualizer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx index 1115a38870821f..485af52c45a551 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { FileDataVisualizerPage } from '../../../datavisualizer/file_based'; @@ -20,24 +22,22 @@ import { checkBasicLicense } from '../../../license'; import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; -import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { - defaultMessage: 'File', - }), - href: '', - }, -]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const fileBasedRoute: MlRoute = { +export const fileBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/filedatavisualizer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { + defaultMessage: 'File', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index 1ec73fced82fec..358b8773e3460e 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'query-string'; import React, { FC } from 'react'; +import { parse } from 'query-string'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { Page } from '../../../datavisualizer/index_based'; @@ -15,24 +19,22 @@ import { checkBasicLicense } from '../../../license'; import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; -import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { - defaultMessage: 'Index', - }), - href: '', - }, -]; - -export const indexBasedRoute: MlRoute = { +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const indexBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/datavisualizer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { + defaultMessage: 'Index', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { index, savedSearchId }: Record = parse(location.search, { sort: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 7d09797a0ff1b4..a2030776773a9a 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -9,6 +9,8 @@ import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../contexts/kibana'; + import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { MlRoute, PageLoader, PageProps } from '../router'; @@ -27,26 +29,24 @@ import { useShowCharts } from '../../components/controls/checkbox_showcharts'; import { useTableInterval } from '../../components/controls/select_interval'; import { useTableSeverity } from '../../components/controls/select_severity'; import { useUrlState } from '../../util/url_state'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { useTimefilter } from '../../contexts/kibana'; import { isViewBySwimLaneData } from '../../explorer/swimlane_container'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { - defaultMessage: 'Anomaly Explorer', - }), - href: '', - }, -]; - -export const explorerRoute: MlRoute = { +export const explorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/explorer', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { + defaultMessage: 'Anomaly Explorer', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context, results } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/index.ts b/x-pack/plugins/ml/public/application/routing/routes/index.ts index 44111ae32cd305..fe7ecd129ebef1 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/index.ts +++ b/x-pack/plugins/ml/public/application/routing/routes/index.ts @@ -10,6 +10,6 @@ export * from './new_job'; export * from './datavisualizer'; export * from './settings'; export * from './data_frame_analytics'; -export { timeSeriesExplorerRoute } from './timeseriesexplorer'; +export { timeSeriesExplorerRouteFactory } from './timeseriesexplorer'; export * from './explorer'; export * from './access_denied'; diff --git a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx index c1d686d356dda6..db58b6a537e06f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx @@ -7,6 +7,9 @@ import React, { useEffect, FC } from 'react'; import { useObservable } from 'react-use'; import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../contexts/kibana'; + import { DEFAULT_REFRESH_INTERVAL_MS } from '../../../../common/constants/jobs_list'; import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; import { useUrlState } from '../../util/url_state'; @@ -15,24 +18,22 @@ import { useResolver } from '../use_resolver'; import { basicResolvers } from '../resolvers'; import { JobsPage } from '../../jobs/jobs_list'; import { useTimefilter } from '../../contexts/kibana'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { - defaultMessage: 'Job Management', - }), - href: '', - }, -]; - -export const jobListRoute: MlRoute = { +export const jobListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { + defaultMessage: 'Job Management', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps)); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index b630b09b1a46db..d8605c4cc91152 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -5,12 +5,16 @@ */ import React, { FC } from 'react'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page, preConfiguredJobRedirect } from '../../../jobs/new_job/pages/index_or_search'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; import { checkBasicLicense } from '../../../license'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; @@ -26,9 +30,9 @@ interface IndexOrSearchPageProps extends PageProps { mode: MODE; } -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, +const getBreadcrumbs = (navigateToPath: NavigateToPath) => [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', { defaultMessage: 'Create job', @@ -37,31 +41,31 @@ const breadcrumbs = [ }, ]; -export const indexOrSearchRoute: MlRoute = { +export const indexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/step/index_or_search', render: (props, deps) => ( ), - breadcrumbs, -}; + breadcrumbs: getBreadcrumbs(navigateToPath), +}); -export const dataVizIndexOrSearchRoute: MlRoute = { +export const dataVizIndexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/datavisualizer_index_select', render: (props, deps) => ( ), - breadcrumbs, -}; + breadcrumbs: getBreadcrumbs(navigateToPath), +}); const PageWrapper: FC = ({ nextStepPath, deps, mode }) => { const newJobResolvers = { diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx index f0a25d880a082d..b8ab29d40fa1f7 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx @@ -4,31 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'query-string'; import React, { FC } from 'react'; +import { parse } from 'query-string'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../jobs/new_job/pages/job_type'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectJobType', { - defaultMessage: 'Create job', - }), - href: '', - }, -]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const jobTypeRoute: MlRoute = { +export const jobTypeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/step/job_type', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectJobType', { + defaultMessage: 'Create job', + }), + href: '', + }, + ], +}); const PageWrapper: FC = ({ location, deps }) => { const { index, savedSearchId }: Record = parse(location.search, { sort: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx index b110434f6f0a8e..b230da44c8d6d0 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/new_job.tsx @@ -5,28 +5,16 @@ */ import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; import { MlRoute } from '../../router'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.jobWizardLabel', { - defaultMessage: 'Create job', - }), - href: '#/jobs/new_job', - }, -]; - -export const newJobRoute: MlRoute = { +export const newJobRouteFactory = (): MlRoute => ({ path: '/jobs/new_job', render: () => , - breadcrumbs, -}; + // no breadcrumbs since it's just a redirect + breadcrumbs: [], +}); const Page: FC = () => { return ; diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx index 2cd40cbcd95e6f..6be58828ee1a55 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx @@ -6,42 +6,41 @@ import { parse } from 'query-string'; import React, { FC } from 'react'; + import { i18n } from '@kbn/i18n'; + +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../jobs/new_job/recognize'; import { checkViewOrCreateJobs } from '../../../jobs/new_job/recognize/resolvers'; import { mlJobService } from '../../../services/job_service'; -import { - ANOMALY_DETECTION_BREADCRUMB, - CREATE_JOB_BREADCRUMB, - ML_BREADCRUMB, -} from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -const breadcrumbs = [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - CREATE_JOB_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabelRecognize', { - defaultMessage: 'Recognized index', - }), - href: '', - }, -]; - -export const recognizeRoute: MlRoute = { +export const recognizeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/recognize', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabelRecognize', { + defaultMessage: 'Recognized index', + }), + href: '', + }, + ], +}); -export const checkViewOrCreateRoute: MlRoute = { +export const checkViewOrCreateRouteFactory = (): MlRoute => ({ path: '/modules/check_view_or_create', render: (props, deps) => , + // no breadcrumbs since it's just a redirect breadcrumbs: [], -}; +}); const PageWrapper: FC = ({ location, deps }) => { const { id, index, savedSearchId }: Record = parse(location.search, { sort: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index 14df9a1d44a852..35085fd5575773 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -8,6 +8,8 @@ import { parse } from 'query-string'; import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { basicResolvers } from '../../resolvers'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -16,20 +18,20 @@ import { JOB_TYPE } from '../../../../../common/constants/new_job'; import { mlJobService } from '../../../services/job_service'; import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service'; import { checkCreateJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; -import { - ANOMALY_DETECTION_BREADCRUMB, - CREATE_JOB_BREADCRUMB, - ML_BREADCRUMB, -} from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; interface WizardPageProps extends PageProps { jobType: JOB_TYPE; } -const baseBreadcrumbs = [ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, CREATE_JOB_BREADCRUMB]; +const getBaseBreadcrumbs = (navigateToPath: NavigateToPath) => [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath), +]; -const singleMetricBreadcrumbs = [ - ...baseBreadcrumbs, +const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', { defaultMessage: 'Single metric', @@ -38,8 +40,8 @@ const singleMetricBreadcrumbs = [ }, ]; -const multiMetricBreadcrumbs = [ - ...baseBreadcrumbs, +const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', { defaultMessage: 'Multi-metric', @@ -48,8 +50,8 @@ const multiMetricBreadcrumbs = [ }, ]; -const populationBreadcrumbs = [ - ...baseBreadcrumbs, +const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', { defaultMessage: 'Population', @@ -58,8 +60,8 @@ const populationBreadcrumbs = [ }, ]; -const advancedBreadcrumbs = [ - ...baseBreadcrumbs, +const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', { defaultMessage: 'Advanced configuration', @@ -68,8 +70,8 @@ const advancedBreadcrumbs = [ }, ]; -const categorizationBreadcrumbs = [ - ...baseBreadcrumbs, +const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath) => [ + ...getBaseBreadcrumbs(navigateToPath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.categorizationLabel', { defaultMessage: 'Categorization', @@ -78,35 +80,35 @@ const categorizationBreadcrumbs = [ }, ]; -export const singleMetricRoute: MlRoute = { +export const singleMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/single_metric', render: (props, deps) => , - breadcrumbs: singleMetricBreadcrumbs, -}; + breadcrumbs: getSingleMetricBreadcrumbs(navigateToPath), +}); -export const multiMetricRoute: MlRoute = { +export const multiMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/multi_metric', render: (props, deps) => , - breadcrumbs: multiMetricBreadcrumbs, -}; + breadcrumbs: getMultiMetricBreadcrumbs(navigateToPath), +}); -export const populationRoute: MlRoute = { +export const populationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/population', render: (props, deps) => , - breadcrumbs: populationBreadcrumbs, -}; + breadcrumbs: getPopulationBreadcrumbs(navigateToPath), +}); -export const advancedRoute: MlRoute = { +export const advancedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/advanced', render: (props, deps) => , - breadcrumbs: advancedBreadcrumbs, -}; + breadcrumbs: getAdvancedBreadcrumbs(navigateToPath), +}); -export const categorizationRoute: MlRoute = { +export const categorizationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/jobs/new_job/categorization', render: (props, deps) => , - breadcrumbs: categorizationBreadcrumbs, -}; + breadcrumbs: getCategorizationBreadcrumbs(navigateToPath), +}); const PageWrapper: FC = ({ location, jobType, deps }) => { const { index, savedSearchId }: Record = parse(location.search, { sort: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx index 9b08bbf35c4488..174e9804b96893 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx @@ -8,6 +8,9 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; + +import { NavigateToPath } from '../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../router'; import { useResolver } from '../use_resolver'; import { OverviewPage } from '../../overview'; @@ -17,23 +20,21 @@ import { checkGetJobsCapabilitiesResolver } from '../../capabilities/check_capab import { getMlNodeCount } from '../../ml_nodes_check'; import { loadMlServerInfo } from '../../services/ml_server_info'; import { useTimefilter } from '../../contexts/kibana'; -import { ML_BREADCRUMB } from '../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - { - text: i18n.translate('xpack.ml.overview.overviewLabel', { - defaultMessage: 'Overview', - }), - href: '#/overview', - }, -]; - -export const overviewRoute: MlRoute = { +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../breadcrumbs'; + +export const overviewRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/overview', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.overview.overviewLabel', { + defaultMessage: 'Overview', + }), + onClick: breadcrumbOnClickFactory('/overview', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { @@ -51,11 +52,11 @@ const PageWrapper: FC = ({ deps }) => { ); }; -export const appRootRoute: MlRoute = { +export const appRootRouteFactory = (): MlRoute => ({ path: '/', render: () => , breadcrumbs: [], -}; +}); const Page: FC = () => { return ; diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx index e015a3292acc4e..f2ae57f1ec961b 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -23,24 +25,22 @@ import { } from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { CalendarsList } from '../../../settings/calendars'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { - defaultMessage: 'Calendar management', - }), - href: '#/settings/calendars_list', - }, -]; - -export const calendarListRoute: MlRoute = { +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; + +export const calendarListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/calendars_list', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { + defaultMessage: 'Calendar management', + }), + onClick: breadcrumbOnClickFactory('/settings/calendars_list', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx index ebd58120853a97..a5c30e1eaaacc3 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -23,7 +25,7 @@ import { } from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { NewCalendar } from '../../../settings/calendars'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; enum MODE { NEW, @@ -34,39 +36,35 @@ interface NewCalendarPageProps extends PageProps { mode: MODE; } -const newBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { - defaultMessage: 'Create', - }), - href: '#/settings/calendars_list/new_calendar', - }, -]; - -const editBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { - defaultMessage: 'Edit', - }), - href: '#/settings/calendars_list/edit_calendar', - }, -]; - -export const newCalendarRoute: MlRoute = { +export const newCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/calendars_list/new_calendar', render: (props, deps) => , - breadcrumbs: newBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { + defaultMessage: 'Create', + }), + onClick: breadcrumbOnClickFactory('/settings/calendars_list/new_calendar', navigateToPath), + }, + ], +}); -export const editCalendarRoute: MlRoute = { +export const editCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/calendars_list/edit_calendar/:calendarId', render: (props, deps) => , - breadcrumbs: editBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { + defaultMessage: 'Edit', + }), + onClick: breadcrumbOnClickFactory('/settings/calendars_list/edit_calendar', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ location, mode, deps }) => { let calendarId: string | undefined; diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx index 25bded1a52db10..d734e18d72babc 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -24,24 +26,22 @@ import { import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { FilterLists } from '../../../settings/filter_lists'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; - -const breadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { - defaultMessage: 'Filter lists', - }), - href: '#/settings/filter_lists', - }, -]; +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const filterListRoute: MlRoute = { +export const filterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/filter_lists', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { + defaultMessage: 'Filter lists', + }), + onClick: breadcrumbOnClickFactory('/settings/filter_lists', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx index 2f4ccecf2f1a21..c6f17bc7f6f683 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -12,6 +12,8 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -23,7 +25,8 @@ import { } from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { EditFilterList } from '../../../settings/filter_lists'; -import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; + +import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; enum MODE { NEW, @@ -34,39 +37,35 @@ interface NewFilterPageProps extends PageProps { mode: MODE; } -const newBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { - defaultMessage: 'Create', - }), - href: '#/settings/filter_lists/new', - }, -]; - -const editBreadcrumbs = [ - ML_BREADCRUMB, - SETTINGS, - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { - defaultMessage: 'Edit', - }), - href: '#/settings/filter_lists/edit', - }, -]; - -export const newFilterListRoute: MlRoute = { +export const newFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/filter_lists/new_filter_list', render: (props, deps) => , - breadcrumbs: newBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { + defaultMessage: 'Create', + }), + onClick: breadcrumbOnClickFactory('/settings/filter_lists/new', navigateToPath), + }, + ], +}); -export const editFilterListRoute: MlRoute = { +export const editFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings/filter_lists/edit_filter_list/:filterId', render: (props, deps) => , - breadcrumbs: editBreadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { + defaultMessage: 'Edit', + }), + onClick: breadcrumbOnClickFactory('/settings/filter_lists/edit', navigateToPath), + }, + ], +}); const PageWrapper: FC = ({ location, mode, deps }) => { let filterId: string | undefined; diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx index a80c173dbca341..3f4b2698514691 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -11,6 +11,8 @@ import React, { FC } from 'react'; +import { NavigateToPath } from '../../../contexts/kibana'; + import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -22,15 +24,16 @@ import { } from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { AnomalyDetectionSettingsContext, Settings } from '../../../settings'; -import { ML_BREADCRUMB, SETTINGS } from '../../breadcrumbs'; - -const breadcrumbs = [ML_BREADCRUMB, SETTINGS]; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const settingsRoute: MlRoute = { +export const settingsRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/settings', render: (props, deps) => , - breadcrumbs, -}; + breadcrumbs: [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + ], +}); const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index fdf29406893add..6486db818e1138 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -11,6 +11,8 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; +import { NavigateToPath } from '../../contexts/kibana'; + import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { TimeSeriesExplorer } from '../../timeseriesexplorer'; @@ -34,15 +36,15 @@ import { MlRoute, PageLoader, PageProps } from '../router'; import { useRefresh } from '../use_refresh'; import { useResolver } from '../use_resolver'; import { basicResolvers } from '../resolvers'; -import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { useTimefilter } from '../../contexts/kibana'; -export const timeSeriesExplorerRoute: MlRoute = { +export const timeSeriesExplorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ path: '/timeseriesexplorer', render: (props, deps) => , breadcrumbs: [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), { text: i18n.translate('xpack.ml.anomalyDetection.singleMetricViewerLabel', { defaultMessage: 'Single Metric Viewer', @@ -50,7 +52,7 @@ export const timeSeriesExplorerRoute: MlRoute = { href: '', }, ], -}; +}); const PageWrapper: FC = ({ deps }) => { const { context, results } = useResolver('', undefined, deps.config, { diff --git a/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts b/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts index b5c01a1c261444..428060dd2c31b4 100644 --- a/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts +++ b/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts @@ -9,6 +9,7 @@ import { getUrlForRecord, isValidLabel, isValidTimeRange, + openCustomUrlWindow, } from './custom_url_utils'; import { AnomalyRecordDoc } from '../../../common/types/anomalies'; import { @@ -474,4 +475,49 @@ describe('ML - custom URL utils', () => { expect(isValidTimeRange('AUTO')).toBe(false); }); }); + + describe('openCustomUrlWindow', () => { + const originalOpen = window.open; + + beforeEach(() => { + delete (window as any).open; + const mockOpen = jest.fn(); + window.open = mockOpen; + }); + + afterEach(() => { + window.open = originalOpen; + }); + + it('should add the base path to a relative non-kibana url', () => { + openCustomUrlWindow( + 'the-url', + { url_name: 'the-url-name', url_value: 'the-url-value' }, + 'the-base-path' + ); + expect(window.open).toHaveBeenCalledWith('the-base-path/the-url', '_blank'); + }); + + it('should add the base path and `app` prefix to a relative kibana url', () => { + openCustomUrlWindow( + 'discover#/the-url', + { url_name: 'the-url-name', url_value: 'discover#/the-url-value' }, + 'the-base-path' + ); + expect(window.open).toHaveBeenCalledWith('the-base-path/app/discover#/the-url', '_blank'); + }); + + it('should use an absolute url with protocol as is', () => { + openCustomUrlWindow( + 'http://example.com', + { url_name: 'the-url-name', url_value: 'http://example.com' }, + 'the-base-path' + ); + expect(window.open).toHaveBeenCalledWith( + 'http://example.com', + '_blank', + 'noopener,noreferrer' + ); + }); + }); }); diff --git a/x-pack/plugins/ml/public/application/util/custom_url_utils.ts b/x-pack/plugins/ml/public/application/util/custom_url_utils.ts index 20bb1c7f605972..9c843af36192eb 100644 --- a/x-pack/plugins/ml/public/application/util/custom_url_utils.ts +++ b/x-pack/plugins/ml/public/application/util/custom_url_utils.ts @@ -76,15 +76,20 @@ export function getUrlForRecord( // Opens the specified URL in a new window. The behaviour (for example whether // it opens in a new tab or window) is determined from the original configuration // object which indicates whether it is opening a Kibana page running on the same server. -// fullUrl is the complete URL, including the base path, with any dollar delimited tokens -// from the urlConfig having been substituted with values from an anomaly record. -export function openCustomUrlWindow(fullUrl: string, urlConfig: UrlConfig) { +// `url` is the URL with any dollar delimited tokens from the urlConfig +// having been substituted with values from an anomaly record. +export function openCustomUrlWindow(url: string, urlConfig: UrlConfig, basePath: string) { // Run through a regex to test whether the url_value starts with a protocol scheme. if (/^(?:[a-z]+:)?\/\//i.test(urlConfig.url_value) === false) { - window.open(fullUrl, '_blank'); + // If `url` is a relative path, we need to prefix the base path. + if (url.charAt(0) !== '/') { + url = `${basePath}${isKibanaUrl(urlConfig) ? '/app/' : '/'}${url}`; + } + + window.open(url, '_blank'); } else { // Add noopener and noreferrr properties for external URLs. - const newWindow = window.open(fullUrl, '_blank', 'noopener,noreferrer'); + const newWindow = window.open(url, '_blank', 'noopener,noreferrer'); // Expect newWindow to be null, but just in case if not, reset the opener link. if (newWindow !== undefined && newWindow !== null) { @@ -94,13 +99,24 @@ export function openCustomUrlWindow(fullUrl: string, urlConfig: UrlConfig) { } // Returns whether the url_value of the supplied config is for -// a Kibana Discover or Dashboard page running on the same server as this ML plugin. +// a Kibana Discover, Dashboard or supported solution page running +// on the same server as this ML plugin. This is necessary so we can have +// backwards compatibility with custom URLs created before the move to +// BrowserRouter and URLs without hashes. If we add another solution to +// recognize modules or with custom UI in the custom URL builder we'd +// need to add the solution here. Manually created custom URLs for other +// solution pages need to be prefixed with `app/` in the custom URL builder. function isKibanaUrl(urlConfig: UrlConfig) { const urlValue = urlConfig.url_value; return ( + // HashRouter based plugins urlValue.startsWith('discover#/') || urlValue.startsWith('dashboards#/') || - urlValue.startsWith('apm#/') + urlValue.startsWith('apm#/') || + // BrowserRouter based plugins + urlValue.startsWith('security/') || + // Legacy links + urlValue.startsWith('siem#/') ); } diff --git a/x-pack/plugins/ml/public/url_generator.test.ts b/x-pack/plugins/ml/public/url_generator.test.ts index 45e2932b7781a1..21dde124049577 100644 --- a/x-pack/plugins/ml/public/url_generator.test.ts +++ b/x-pack/plugins/ml/public/url_generator.test.ts @@ -19,7 +19,7 @@ describe('MlUrlGenerator', () => { mlExplorerSwimlane: { viewByFromPage: 2, viewByPerPage: 20 }, }); expect(url).toBe( - '/app/ml#/explorer?_g=(ml:(jobIds:!(test-job)))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFromPage:2,viewByPerPage:20))' + '/app/ml/explorer#?_g=(ml:(jobIds:!(test-job)))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFromPage:2,viewByPerPage:20))' ); }); diff --git a/x-pack/plugins/ml/public/url_generator.ts b/x-pack/plugins/ml/public/url_generator.ts index c2b57f6349d81d..b7cf64159a8274 100644 --- a/x-pack/plugins/ml/public/url_generator.ts +++ b/x-pack/plugins/ml/public/url_generator.ts @@ -83,7 +83,7 @@ export class MlUrlGenerator implements UrlGeneratorsDefinition('_g', queryState, { useHash: false }, url); url = setStateToKbnUrl('_a', appState, { useHash: false }, url); diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_review.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_review.test.js index 3dbbe70bfc5603..87fdabae182402 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_review.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_review.test.js @@ -25,7 +25,9 @@ jest.mock('../../kibana_services', () => { const { setup } = pageHelpers.jobCreate; -describe('Create Rollup Job, step 6: Review', () => { +// FLAKY: https://github.com/elastic/kibana/issues/69783 +// FLAKY: https://github.com/elastic/kibana/issues/70043 +describe.skip('Create Rollup Job, step 6: Review', () => { let find; let exists; let actions; diff --git a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts index 2cebaacc67681a..2d37d4a345fa14 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts @@ -15,86 +15,13 @@ import { getLanguageBooleanOperator, buildNested, } from './build_exceptions_query'; -import { - EntryNested, - EntryExists, - EntryMatch, - EntryMatchAny, - EntriesArray, - Operator, -} from '../../../lists/common/schemas'; +import { EntryNested, EntryMatchAny, EntriesArray } from '../../../lists/common/schemas'; import { getExceptionListItemSchemaMock } from '../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getEntryMatchMock } from '../../../lists/common/schemas/types/entry_match.mock'; import { getEntryMatchAnyMock } from '../../../lists/common/schemas/types/entry_match_any.mock'; import { getEntryExistsMock } from '../../../lists/common/schemas/types/entry_exists.mock'; describe('build_exceptions_query', () => { - const makeMatchEntry = ({ - field, - value = 'value-1', - operator = 'included', - }: { - field: string; - value?: string; - operator?: Operator; - }): EntryMatch => { - return { - field, - operator, - type: 'match', - value, - }; - }; - const makeMatchAnyEntry = ({ - field, - operator = 'included', - value = ['value-1', 'value-2'], - }: { - field: string; - operator?: Operator; - value?: string[]; - }): EntryMatchAny => { - return { - field, - operator, - value, - type: 'match_any', - }; - }; - const makeExistsEntry = ({ - field, - operator = 'included', - }: { - field: string; - operator?: Operator; - }): EntryExists => { - return { - field, - operator, - type: 'exists', - }; - }; - const matchEntryWithIncluded: EntryMatch = makeMatchEntry({ - field: 'host.name', - value: 'suricata', - }); - const matchEntryWithExcluded: EntryMatch = makeMatchEntry({ - field: 'host.name', - value: 'suricata', - operator: 'excluded', - }); - const matchAnyEntryWithIncludedAndTwoValues: EntryMatchAny = makeMatchAnyEntry({ - field: 'host.name', - value: ['suricata', 'auditd'], - }); - const existsEntryWithIncluded: EntryExists = makeExistsEntry({ - field: 'host.name', - }); - const existsEntryWithExcluded: EntryExists = makeExistsEntry({ - field: 'host.name', - operator: 'excluded', - }); - describe('getLanguageBooleanOperator', () => { test('it returns value as uppercase if language is "lucene"', () => { const result = getLanguageBooleanOperator({ language: 'lucene', value: 'not' }); @@ -137,14 +64,14 @@ describe('build_exceptions_query', () => { describe('kuery', () => { test('it returns formatted wildcard string when operator is "excluded"', () => { const query = buildExists({ - entry: existsEntryWithExcluded, + entry: { ...getEntryExistsMock(), operator: 'excluded' }, language: 'kuery', }); expect(query).toEqual('not host.name:*'); }); test('it returns formatted wildcard string when operator is "included"', () => { const query = buildExists({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'kuery', }); expect(query).toEqual('host.name:*'); @@ -154,14 +81,14 @@ describe('build_exceptions_query', () => { describe('lucene', () => { test('it returns formatted wildcard string when operator is "excluded"', () => { const query = buildExists({ - entry: existsEntryWithExcluded, + entry: { ...getEntryExistsMock(), operator: 'excluded' }, language: 'lucene', }); expect(query).toEqual('NOT _exists_host.name'); }); test('it returns formatted wildcard string when operator is "included"', () => { const query = buildExists({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'lucene', }); expect(query).toEqual('_exists_host.name'); @@ -173,52 +100,55 @@ describe('build_exceptions_query', () => { describe('kuery', () => { test('it returns formatted string when operator is "included"', () => { const query = buildMatch({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'kuery', }); - expect(query).toEqual('host.name:"suricata"'); + expect(query).toEqual('host.name:"some host name"'); }); test('it returns formatted string when operator is "excluded"', () => { const query = buildMatch({ - entry: matchEntryWithExcluded, + entry: { ...getEntryMatchMock(), operator: 'excluded' }, language: 'kuery', }); - expect(query).toEqual('not host.name:"suricata"'); + expect(query).toEqual('not host.name:"some host name"'); }); }); describe('lucene', () => { test('it returns formatted string when operator is "included"', () => { const query = buildMatch({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'lucene', }); - expect(query).toEqual('host.name:"suricata"'); + expect(query).toEqual('host.name:"some host name"'); }); test('it returns formatted string when operator is "excluded"', () => { const query = buildMatch({ - entry: matchEntryWithExcluded, + entry: { ...getEntryMatchMock(), operator: 'excluded' }, language: 'lucene', }); - expect(query).toEqual('NOT host.name:"suricata"'); + expect(query).toEqual('NOT host.name:"some host name"'); }); }); }); describe('buildMatchAny', () => { - const entryWithIncludedAndNoValues: EntryMatchAny = makeMatchAnyEntry({ + const entryWithIncludedAndNoValues: EntryMatchAny = { + ...getEntryMatchAnyMock(), field: 'host.name', value: [], - }); - const entryWithIncludedAndOneValue: EntryMatchAny = makeMatchAnyEntry({ + }; + const entryWithIncludedAndOneValue: EntryMatchAny = { + ...getEntryMatchAnyMock(), field: 'host.name', - value: ['suricata'], - }); - const entryWithExcludedAndTwoValues: EntryMatchAny = makeMatchAnyEntry({ + value: ['some host name'], + }; + const entryWithExcludedAndTwoValues: EntryMatchAny = { + ...getEntryMatchAnyMock(), field: 'host.name', - value: ['suricata', 'auditd'], + value: ['some host name', 'auditd'], operator: 'excluded', - }); + }; describe('kuery', () => { test('it returns empty string if given an empty array for "values"', () => { @@ -235,16 +165,16 @@ describe('build_exceptions_query', () => { language: 'kuery', }); - expect(exceptionSegment).toEqual('host.name:("suricata")'); + expect(exceptionSegment).toEqual('host.name:("some host name")'); }); test('it returns formatted string when operator is "included"', () => { const exceptionSegment = buildMatchAny({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'kuery', }); - expect(exceptionSegment).toEqual('host.name:("suricata" or "auditd")'); + expect(exceptionSegment).toEqual('host.name:("some host name" or "auditd")'); }); test('it returns formatted string when operator is "excluded"', () => { @@ -253,18 +183,18 @@ describe('build_exceptions_query', () => { language: 'kuery', }); - expect(exceptionSegment).toEqual('not host.name:("suricata" or "auditd")'); + expect(exceptionSegment).toEqual('not host.name:("some host name" or "auditd")'); }); }); describe('lucene', () => { test('it returns formatted string when operator is "included"', () => { const exceptionSegment = buildMatchAny({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'lucene', }); - expect(exceptionSegment).toEqual('host.name:("suricata" OR "auditd")'); + expect(exceptionSegment).toEqual('host.name:("some host name" OR "auditd")'); }); test('it returns formatted string when operator is "excluded"', () => { const exceptionSegment = buildMatchAny({ @@ -272,7 +202,7 @@ describe('build_exceptions_query', () => { language: 'lucene', }); - expect(exceptionSegment).toEqual('NOT host.name:("suricata" OR "auditd")'); + expect(exceptionSegment).toEqual('NOT host.name:("some host name" OR "auditd")'); }); test('it returns formatted string when "values" includes only one item', () => { const exceptionSegment = buildMatchAny({ @@ -280,7 +210,7 @@ describe('build_exceptions_query', () => { language: 'lucene', }); - expect(exceptionSegment).toEqual('host.name:("suricata")'); + expect(exceptionSegment).toEqual('host.name:("some host name")'); }); }); }); @@ -394,7 +324,7 @@ describe('build_exceptions_query', () => { describe('kuery', () => { test('it returns formatted wildcard string when "type" is "exists"', () => { const result = buildEntry({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'kuery', }); expect(result).toEqual('host.name:*'); @@ -402,25 +332,25 @@ describe('build_exceptions_query', () => { test('it returns formatted string when "type" is "match"', () => { const result = buildEntry({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'kuery', }); - expect(result).toEqual('host.name:"suricata"'); + expect(result).toEqual('host.name:"some host name"'); }); test('it returns formatted string when "type" is "match_any"', () => { const result = buildEntry({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'kuery', }); - expect(result).toEqual('host.name:("suricata" or "auditd")'); + expect(result).toEqual('host.name:("some host name" or "auditd")'); }); }); describe('lucene', () => { test('it returns formatted wildcard string when "type" is "exists"', () => { const result = buildEntry({ - entry: existsEntryWithIncluded, + entry: { ...getEntryExistsMock(), operator: 'included' }, language: 'lucene', }); expect(result).toEqual('_exists_host.name'); @@ -428,18 +358,18 @@ describe('build_exceptions_query', () => { test('it returns formatted string when "type" is "match"', () => { const result = buildEntry({ - entry: matchEntryWithIncluded, + entry: { ...getEntryMatchMock(), operator: 'included' }, language: 'lucene', }); - expect(result).toEqual('host.name:"suricata"'); + expect(result).toEqual('host.name:"some host name"'); }); test('it returns formatted string when "type" is "match_any"', () => { const result = buildEntry({ - entry: matchAnyEntryWithIncludedAndTwoValues, + entry: { ...getEntryMatchAnyMock(), value: ['some host name', 'auditd'] }, language: 'lucene', }); - expect(result).toEqual('host.name:("suricata" OR "auditd")'); + expect(result).toEqual('host.name:("some host name" OR "auditd")'); }); }); }); @@ -456,26 +386,31 @@ describe('build_exceptions_query', () => { test('it returns expected query when more than one item in exception item', () => { const payload: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'value-3' }), + { ...getEntryMatchAnyMock(), field: 'b' }, + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'value-3' }, ]; const query = buildExceptionItem({ language: 'kuery', entries: payload, }); - const expectedQuery = 'b:("value-1" or "value-2") and not c:"value-3"'; + const expectedQuery = 'b:("some host name") and not c:"value-3"'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when exception item includes nested value', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'included', value: 'value-3' }), + { + ...getEntryMatchMock(), + field: 'nestedField', + operator: 'included', + value: 'value-3', + }, ], }, ]; @@ -483,56 +418,65 @@ describe('build_exceptions_query', () => { language: 'kuery', entries, }); - const expectedQuery = 'b:("value-1" or "value-2") and parent:{ nestedField:"value-3" }'; + const expectedQuery = 'b:("some host name") and parent:{ nestedField:"value-3" }'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when exception item includes multiple items and nested "and" values', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'included', value: 'value-3' }), + { + ...getEntryMatchMock(), + field: 'nestedField', + operator: 'included', + value: 'value-3', + }, ], }, - makeExistsEntry({ field: 'd' }), + { ...getEntryExistsMock(), field: 'd' }, ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = - 'b:("value-1" or "value-2") and parent:{ nestedField:"value-3" } and d:*'; + const expectedQuery = 'b:("some host name") and parent:{ nestedField:"value-3" } and d:*'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when language is "lucene"', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'excluded', value: 'value-3' }), + { + ...getEntryMatchMock(), + field: 'nestedField', + operator: 'excluded', + value: 'value-3', + }, ], }, - makeExistsEntry({ field: 'e', operator: 'excluded' }), + { ...getEntryExistsMock(), field: 'e', operator: 'excluded' }, ]; const query = buildExceptionItem({ language: 'lucene', entries, }); const expectedQuery = - 'b:("value-1" OR "value-2") AND parent:{ NOT nestedField:"value-3" } AND NOT _exists_e'; + 'b:("some host name") AND parent:{ NOT nestedField:"value-3" } AND NOT _exists_e'; expect(query).toEqual(expectedQuery); }); describe('exists', () => { test('it returns expected query when list includes single list item with operator of "included"', () => { - const entries: EntriesArray = [makeExistsEntry({ field: 'b' })]; + const entries: EntriesArray = [{ ...getEntryExistsMock(), field: 'b' }]; const query = buildExceptionItem({ language: 'kuery', entries, @@ -543,7 +487,9 @@ describe('build_exceptions_query', () => { }); test('it returns expected query when list includes single list item with operator of "excluded"', () => { - const entries: EntriesArray = [makeExistsEntry({ field: 'b', operator: 'excluded' })]; + const entries: EntriesArray = [ + { ...getEntryExistsMock(), field: 'b', operator: 'excluded' }, + ]; const query = buildExceptionItem({ language: 'kuery', entries, @@ -555,11 +501,13 @@ describe('build_exceptions_query', () => { test('it returns expected query when exception item includes entry item with "and" values', () => { const entries: EntriesArray = [ - makeExistsEntry({ field: 'b', operator: 'excluded' }), + { ...getEntryExistsMock(), field: 'b', operator: 'excluded' }, { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'c', operator: 'included', value: 'value-1' })], + entries: [ + { ...getEntryMatchMock(), field: 'c', operator: 'included', value: 'value-1' }, + ], }, ]; const query = buildExceptionItem({ @@ -573,16 +521,16 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items', () => { const entries: EntriesArray = [ - makeExistsEntry({ field: 'b' }), + { ...getEntryExistsMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'value-1' }), - makeMatchEntry({ field: 'd', value: 'value-2' }), + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'value-1' }, + { ...getEntryMatchMock(), field: 'd', value: 'value-2' }, ], }, - makeExistsEntry({ field: 'e' }), + { ...getEntryExistsMock(), field: 'e' }, ]; const query = buildExceptionItem({ language: 'kuery', @@ -596,7 +544,7 @@ describe('build_exceptions_query', () => { describe('match', () => { test('it returns expected query when list includes single list item with operator of "included"', () => { - const entries: EntriesArray = [makeMatchEntry({ field: 'b', value: 'value' })]; + const entries: EntriesArray = [{ ...getEntryMatchMock(), field: 'b', value: 'value' }]; const query = buildExceptionItem({ language: 'kuery', entries, @@ -608,7 +556,7 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes single list item with operator of "excluded"', () => { const entries: EntriesArray = [ - makeMatchEntry({ field: 'b', operator: 'excluded', value: 'value' }), + { ...getEntryMatchMock(), field: 'b', operator: 'excluded', value: 'value' }, ]; const query = buildExceptionItem({ language: 'kuery', @@ -621,11 +569,13 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes list item with "and" values', () => { const entries: EntriesArray = [ - makeMatchEntry({ field: 'b', operator: 'excluded', value: 'value' }), + { ...getEntryMatchMock(), field: 'b', operator: 'excluded', value: 'value' }, { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'c', operator: 'included', value: 'valueC' })], + entries: [ + { ...getEntryMatchMock(), field: 'c', operator: 'included', value: 'valueC' }, + ], }, ]; const query = buildExceptionItem({ @@ -639,16 +589,16 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items', () => { const entries: EntriesArray = [ - makeMatchEntry({ field: 'b', value: 'value' }), + { ...getEntryMatchMock(), field: 'b', value: 'value' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' }), - makeMatchEntry({ field: 'd', operator: 'excluded', value: 'valueD' }), + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'valueC' }, + { ...getEntryMatchMock(), field: 'd', operator: 'excluded', value: 'valueD' }, ], }, - makeMatchEntry({ field: 'e', value: 'valueE' }), + { ...getEntryMatchMock(), field: 'e', value: 'valueE' }, ]; const query = buildExceptionItem({ language: 'kuery', @@ -663,55 +613,59 @@ describe('build_exceptions_query', () => { describe('match_any', () => { test('it returns expected query when list includes single list item with operator of "included"', () => { - const entries: EntriesArray = [makeMatchAnyEntry({ field: 'b' })]; + const entries: EntriesArray = [{ ...getEntryMatchAnyMock(), field: 'b' }]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'b:("value-1" or "value-2")'; + const expectedQuery = 'b:("some host name")'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes single list item with operator of "excluded"', () => { - const entries: EntriesArray = [makeMatchAnyEntry({ field: 'b', operator: 'excluded' })]; + const entries: EntriesArray = [ + { ...getEntryMatchAnyMock(), field: 'b', operator: 'excluded' }, + ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'not b:("value-1" or "value-2")'; + const expectedQuery = 'not b:("some host name")'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes list item with nested values', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b', operator: 'excluded' }), + { ...getEntryMatchAnyMock(), field: 'b', operator: 'excluded' }, { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' })], + entries: [ + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'valueC' }, + ], }, ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'not b:("value-1" or "value-2") and parent:{ not c:"valueC" }'; + const expectedQuery = 'not b:("some host name") and parent:{ not c:"valueC" }'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes multiple items', () => { const entries: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), - makeMatchAnyEntry({ field: 'c' }), + { ...getEntryMatchAnyMock(), field: 'b' }, + { ...getEntryMatchAnyMock(), field: 'c' }, ]; const query = buildExceptionItem({ language: 'kuery', entries, }); - const expectedQuery = 'b:("value-1" or "value-2") and c:("value-1" or "value-2")'; + const expectedQuery = 'b:("some host name") and c:("some host name")'; expect(query).toEqual(expectedQuery); }); @@ -735,16 +689,16 @@ describe('build_exceptions_query', () => { const payload = getExceptionListItemSchemaMock(); const payload2 = getExceptionListItemSchemaMock(); payload2.entries = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'c', operator: 'included', value: 'valueC' }), - makeMatchEntry({ field: 'd', operator: 'included', value: 'valueD' }), + { ...getEntryMatchMock(), field: 'c', operator: 'included', value: 'valueC' }, + { ...getEntryMatchMock(), field: 'd', operator: 'included', value: 'valueD' }, ], }, - makeMatchAnyEntry({ field: 'e', operator: 'excluded' }), + { ...getEntryMatchAnyMock(), field: 'e', operator: 'excluded' }, ]; const queries = buildExceptionListQueries({ language: 'kuery', @@ -758,7 +712,7 @@ describe('build_exceptions_query', () => { }, { query: - 'b:("value-1" or "value-2") and parent:{ c:"valueC" and d:"valueD" } and not e:("value-1" or "value-2")', + 'b:("some host name") and parent:{ c:"valueC" and d:"valueD" } and not e:("some host name")', language: 'kuery', }, ]; @@ -768,20 +722,26 @@ describe('build_exceptions_query', () => { test('it returns expected query when lists exist and language is "lucene"', () => { const payload = getExceptionListItemSchemaMock(); - payload.entries = [makeMatchAnyEntry({ field: 'a' }), makeMatchAnyEntry({ field: 'b' })]; + payload.entries = [ + { ...getEntryMatchAnyMock(), field: 'a' }, + { ...getEntryMatchAnyMock(), field: 'b' }, + ]; const payload2 = getExceptionListItemSchemaMock(); - payload2.entries = [makeMatchAnyEntry({ field: 'c' }), makeMatchAnyEntry({ field: 'd' })]; + payload2.entries = [ + { ...getEntryMatchAnyMock(), field: 'c' }, + { ...getEntryMatchAnyMock(), field: 'd' }, + ]; const queries = buildExceptionListQueries({ language: 'lucene', lists: [payload, payload2], }); const expectedQueries = [ { - query: 'a:("value-1" OR "value-2") AND b:("value-1" OR "value-2")', + query: 'a:("some host name") AND b:("some host name")', language: 'lucene', }, { - query: 'c:("value-1" OR "value-2") AND d:("value-1" OR "value-2")', + query: 'c:("some host name") AND d:("some host name")', language: 'lucene', }, ]; @@ -793,17 +753,17 @@ describe('build_exceptions_query', () => { const payload = getExceptionListItemSchemaMock(); const payload2 = getExceptionListItemSchemaMock(); payload2.entries = [ - makeMatchAnyEntry({ field: 'b' }), + { ...getEntryMatchAnyMock(), field: 'b' }, { field: 'parent', type: 'nested', entries: [ // TODO: these operators are not being respected. buildNested needs to be updated - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' }), - makeMatchEntry({ field: 'd', operator: 'excluded', value: 'valueD' }), + { ...getEntryMatchMock(), field: 'c', operator: 'excluded', value: 'valueC' }, + { ...getEntryMatchMock(), field: 'd', operator: 'excluded', value: 'valueD' }, ], }, - makeMatchAnyEntry({ field: 'e' }), + { ...getEntryMatchAnyMock(), field: 'e' }, ]; const queries = buildExceptionListQueries({ language: 'kuery', @@ -817,7 +777,7 @@ describe('build_exceptions_query', () => { }, { query: - 'b:("value-1" or "value-2") and parent:{ not c:"valueC" and not d:"valueD" } and e:("value-1" or "value-2")', + 'b:("some host name") and parent:{ not c:"valueC" and not d:"valueD" } and e:("some host name")', language: 'kuery', }, ]; 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 9a92270fc9c14d..aa3f0bf287fcae 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 a982f9ffe8f21f..1c24e1abe5a57e 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/common/components/autocomplete/field_value_match.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx index 72467a62f57c1a..998ed1f3351c80 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx @@ -232,7 +232,6 @@ describe('AutocompleteFieldMatchComponent', () => { fields, }, value: 'value 1', - signal: new AbortController().signal, }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx index 137f6803dc54e7..dfb3761bb34978 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx @@ -72,14 +72,13 @@ export const AutocompleteFieldMatchComponent: React.FC { - const signal = new AbortController().signal; - - updateSuggestions({ - fieldSelected: selectedField, - value: `${searchVal}`, - patterns: indexPattern, - signal, - }); + if (updateSuggestions != null) { + updateSuggestions({ + fieldSelected: selectedField, + value: searchVal, + patterns: indexPattern, + }); + } }; const isValid = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx index f3f0f2e2a44b1f..0a0281a9c4a51a 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx @@ -232,7 +232,6 @@ describe('AutocompleteFieldMatchAnyComponent', () => { fields, }, value: 'value 1', - signal: new AbortController().signal, }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx index 5a15c1f7238dea..1952ef865e045a 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx @@ -65,14 +65,13 @@ export const AutocompleteFieldMatchAnyComponent: React.FC { - const signal = new AbortController().signal; - - updateSuggestions({ - fieldSelected: selectedField, - value: `${searchVal}`, - patterns: indexPattern, - signal, - }); + if (updateSuggestions != null) { + updateSuggestions({ + fieldSelected: selectedField, + value: searchVal, + patterns: indexPattern, + }); + } }; const onCreateOption = (option: string) => onChange([...(selectedValue || []), option]); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts index def2a303f6038a..a76b50d11a8757 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts @@ -199,12 +199,17 @@ describe('useFieldValueAutocomplete', () => { await waitForNextUpdate(); await waitForNextUpdate(); - result.current[2]({ - fieldSelected: getField('@tags'), - value: 'hello', - patterns: stubIndexPatternWithFields, - signal: new AbortController().signal, - }); + expect(result.current[2]).not.toBeNull(); + + // Added check for typescripts sake, if null, + // would not reach below logic as test would stop above + if (result.current[2] != null) { + result.current[2]({ + fieldSelected: getField('@tags'), + value: 'hello', + patterns: stubIndexPatternWithFields, + }); + } await waitForNextUpdate(); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts index 541c0a8d3fbae5..a53914da93f279 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts @@ -11,16 +11,13 @@ import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/d import { useKibana } from '../../../../common/lib/kibana'; import { OperatorTypeEnum } from '../../../../lists_plugin_deps'; -export type UseFieldValueAutocompleteReturn = [ - boolean, - string[], - (args: { - fieldSelected: IFieldType | undefined; - value: string | string[] | undefined; - patterns: IIndexPattern | undefined; - signal: AbortSignal; - }) => void -]; +type Func = (args: { + fieldSelected: IFieldType | undefined; + value: string | string[] | undefined; + patterns: IIndexPattern | undefined; +}) => void; + +export type UseFieldValueAutocompleteReturn = [boolean, string[], Func | null]; export interface UseFieldValueAutocompleteProps { selectedField: IFieldType | undefined; @@ -41,62 +38,77 @@ export const useFieldValueAutocomplete = ({ const { services } = useKibana(); const [isLoading, setIsLoading] = useState(false); const [suggestions, setSuggestions] = useState([]); - const updateSuggestions = useRef( - debounce( + const updateSuggestions = useRef(null); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + + const fetchSuggestions = debounce( async ({ fieldSelected, value, patterns, - signal, }: { fieldSelected: IFieldType | undefined; value: string | string[] | undefined; patterns: IIndexPattern | undefined; - signal: AbortSignal; }) => { - if (fieldSelected == null || patterns == null) { - return; - } + const inputValue: string | string[] = value ?? ''; + const userSuggestion: string = Array.isArray(inputValue) + ? inputValue[inputValue.length - 1] ?? '' + : inputValue; - setIsLoading(true); + try { + if (isSubscribed) { + if (fieldSelected == null || patterns == null) { + return; + } - // Fields of type boolean should only display two options - if (fieldSelected.type === 'boolean') { - setIsLoading(false); - setSuggestions(['true', 'false']); - return; - } + setIsLoading(true); - const newSuggestions = await services.data.autocomplete.getValueSuggestions({ - indexPattern: patterns, - field: fieldSelected, - query: '', - signal, - }); + // Fields of type boolean should only display two options + if (fieldSelected.type === 'boolean') { + setIsLoading(false); + setSuggestions(['true', 'false']); + return; + } - setIsLoading(false); - setSuggestions(newSuggestions); + const newSuggestions = await services.data.autocomplete.getValueSuggestions({ + indexPattern: patterns, + field: fieldSelected, + query: userSuggestion.trim(), + signal: abortCtrl.signal, + }); + + setIsLoading(false); + setSuggestions([...newSuggestions]); + } + } catch (error) { + if (isSubscribed) { + setSuggestions([]); + setIsLoading(false); + } + } }, 500 - ) - ); - - useEffect(() => { - const abortCtrl = new AbortController(); + ); if (operatorType !== OperatorTypeEnum.EXISTS) { - updateSuggestions.current({ + fetchSuggestions({ fieldSelected: selectedField, value: fieldValue, patterns: indexPattern, - signal: abortCtrl.signal, }); } + updateSuggestions.current = fetchSuggestions; + return (): void => { + isSubscribed = false; abortCtrl.abort(); }; - }, [updateSuggestions, selectedField, operatorType, fieldValue, indexPattern]); + }, [services.data.autocomplete, selectedField, operatorType, fieldValue, indexPattern]); return [isLoading, suggestions, updateSuggestions.current]; }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index bb547f05090b7e..e6eaa4947e4040 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -40,6 +40,7 @@ import { AddExceptionComments } from '../add_exception_comments'; import { enrichNewExceptionItemsWithComments, enrichExceptionItemsWithOS, + lowercaseHashValues, defaultEndpointExceptionItems, entryHasListType, entryHasNonEcsType, @@ -256,7 +257,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ : exceptionItemsToAdd; if (exceptionListType === 'endpoint') { const osTypes = retrieveAlertOsTypes(); - enriched = enrichExceptionItemsWithOS(enriched, osTypes); + enriched = lowercaseHashValues(enrichExceptionItemsWithOS(enriched, osTypes)); } return enriched; }, [comment, exceptionItemsToAdd, exceptionListType, retrieveAlertOsTypes]); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx index 3dcc3eb5a8329f..0f54ec29cc5400 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.test.tsx @@ -55,6 +55,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -81,6 +82,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -111,6 +113,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -143,6 +146,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -175,6 +179,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -207,6 +212,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -239,6 +245,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -271,6 +278,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -306,6 +314,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -331,6 +340,62 @@ describe('BuilderEntryItem', () => { ).toBeTruthy(); }); + test('it uses "correspondingKeywordField" if it exists', () => { + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').prop('selectedField') + ).toEqual({ + name: 'extension', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); + }); + test('it invokes "onChange" when new field is selected and resets operator and value fields', () => { const mockOnChange = jest.fn(); const wrapper = mount( @@ -342,6 +407,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -376,6 +442,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -410,6 +477,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -444,6 +512,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', @@ -478,6 +547,7 @@ describe('BuilderEntryItem', () => { nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }} indexPattern={{ id: '1234', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx index 5939a5a1b576eb..3883a2fad2cf2c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_entry_item.tsx @@ -95,7 +95,7 @@ export const BuilderEntryItem: React.FC = ({ const renderFieldInput = useCallback( (isFirst: boolean): JSX.Element => { - const filteredIndexPatterns = getFilteredIndexPatterns(indexPattern, entry); + const filteredIndexPatterns = getFilteredIndexPatterns(indexPattern, entry, listType); const comboBox = ( = ({ return comboBox; } }, - [handleFieldChange, indexPattern, entry] + [handleFieldChange, indexPattern, entry, listType] ); const renderOperatorInput = (isFirst: boolean): JSX.Element => { @@ -170,7 +170,11 @@ export const BuilderEntryItem: React.FC = ({ return ( = ({ return ( { + const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); + + beforeAll(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + autocomplete: { + getValueSuggestions: getValueSuggestionsMock, + }, + }, + }, + }); + }); + + afterEach(() => { + getValueSuggestionsMock.mockClear(); + }); + describe('and badge logic', () => { test('it renders "and" badge with extra top padding for the first exception item when "andLogicIncluded" is "true"', () => { const exceptionItem = { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx index 17c94adf42648f..224c99756eb5c7 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.test.tsx @@ -41,6 +41,7 @@ import { getOperatorOptions, getUpdatedEntriesOnDelete, isEntryNested, + getCorrespondingKeywordField, } from './helpers'; import { OperatorOption } from '../../autocomplete/types'; @@ -57,6 +58,7 @@ const getMockBuilderEntry = (): FormattedBuilderEntry => ({ nested: undefined, parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }); const getMockNestedBuilderEntry = (): FormattedBuilderEntry => ({ @@ -73,6 +75,7 @@ const getMockNestedBuilderEntry = (): FormattedBuilderEntry => ({ parentIndex: 0, }, entryIndex: 0, + correspondingKeywordField: undefined, }); const getMockNestedParentBuilderEntry = (): FormattedBuilderEntry => ({ @@ -82,69 +85,305 @@ const getMockNestedParentBuilderEntry = (): FormattedBuilderEntry => ({ nested: 'parent', parent: undefined, entryIndex: 0, + correspondingKeywordField: undefined, }); +const mockEndpointFields = [ + { + name: 'file.path.text', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + name: 'file.Ext.code_signature.status', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'file.Ext.code_signature' } }, + }, +]; + +export const getEndpointField = (name: string) => + mockEndpointFields.find((field) => field.name === name) as IFieldType; + describe('Exception builder helpers', () => { + describe('#getCorrespondingKeywordField', () => { + test('it returns matching keyword field if "selectedFieldIsTextType" is true and keyword field exists', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: 'machine.os.raw.text', + }); + + expect(output).toEqual(getField('machine.os.raw')); + }); + + test('it returns undefined if "selectedFieldIsTextType" is false', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: 'machine.os.raw', + }); + + expect(output).toEqual(undefined); + }); + + test('it returns undefined if "selectedField" is empty string', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: '', + }); + + expect(output).toEqual(undefined); + }); + + test('it returns undefined if "selectedField" is undefined', () => { + const output = getCorrespondingKeywordField({ + fields, + selectedField: undefined, + }); + + expect(output).toEqual(undefined); + }); + }); + describe('#getFilteredIndexPatterns', () => { - test('it returns nested fields that match parent value when "item.nested" is "child"', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = getMockNestedBuilderEntry(); - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { - fields: [ - { ...getField('nestedField.child') }, - { ...getField('nestedField.nestedChild.doublyNestedChild') }, - ], - id: '1234', - title: 'logstash-*', - }; - expect(output).toEqual(expected); + describe('list type detections', () => { + test('it returns nested fields that match parent value when "item.nested" is "child"', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = getMockNestedBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [ + { ...getField('nestedField.child') }, + { ...getField('nestedField.nestedChild.doublyNestedChild') }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only parent nested field when "item.nested" is "parent" and nested parent field is not undefined', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = getMockNestedParentBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [{ ...getField('nestedField.child'), name: 'nestedField', esTypes: ['nested'] }], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only nested fields when "item.nested" is "parent" and nested parent field is undefined', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = { + ...getMockNestedParentBuilderEntry(), + field: undefined, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [ + { ...getField('nestedField.child') }, + { ...getField('nestedField.nestedChild.doublyNestedChild') }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns all fields unfiletered if "item.nested" is not "child" or "parent"', () => { + const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + const payloadItem: FormattedBuilderEntry = getMockBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'detection'); + const expected: IIndexPattern = { + fields: [...fields], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); }); - test('it returns only parent nested field when "item.nested" is "parent" and nested parent field is not undefined', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = getMockNestedParentBuilderEntry(); - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { - fields: [{ ...getField('nestedField.child'), name: 'nestedField', esTypes: ['nested'] }], - id: '1234', - title: 'logstash-*', - }; - expect(output).toEqual(expected); + describe('list type endpoint', () => { + let payloadIndexPattern: IIndexPattern = getMockIndexPattern(); + + beforeAll(() => { + payloadIndexPattern = { + ...payloadIndexPattern, + fields: [...payloadIndexPattern.fields, ...mockEndpointFields], + }; + }); + + test('it returns nested fields that match parent value when "item.nested" is "child"', () => { + const payloadItem: FormattedBuilderEntry = { + field: getEndpointField('file.Ext.code_signature.status'), + operator: isOperator, + value: 'some value', + nested: 'child', + parent: { + parent: { + ...getEntryNestedMock(), + field: 'file.Ext.code_signature', + entries: [{ ...getEntryMatchMock(), field: 'child' }], + }, + parentIndex: 0, + }, + entryIndex: 0, + correspondingKeywordField: undefined, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [getEndpointField('file.Ext.code_signature.status')], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only parent nested field when "item.nested" is "parent" and nested parent field is not undefined', () => { + const payloadItem: FormattedBuilderEntry = { + ...getMockNestedParentBuilderEntry(), + field: { + ...getEndpointField('file.Ext.code_signature.status'), + name: 'file.Ext.code_signature', + esTypes: ['nested'], + }, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [ + { + aggregatable: false, + count: 0, + esTypes: ['nested'], + name: 'file.Ext.code_signature', + readFromDocValues: false, + scripted: false, + searchable: true, + subType: { + nested: { + path: 'file.Ext.code_signature', + }, + }, + type: 'string', + }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns only nested fields when "item.nested" is "parent" and nested parent field is undefined', () => { + const payloadItem: FormattedBuilderEntry = { + ...getMockNestedParentBuilderEntry(), + field: undefined, + }; + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [getEndpointField('file.Ext.code_signature.status')], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); + + test('it returns all fields that matched those in "exceptionable_fields.json" with no further filtering if "item.nested" is not "child" or "parent"', () => { + const payloadItem: FormattedBuilderEntry = getMockBuilderEntry(); + const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem, 'endpoint'); + const expected: IIndexPattern = { + fields: [ + { + aggregatable: false, + count: 0, + esTypes: ['text'], + name: 'file.path.text', + readFromDocValues: false, + scripted: false, + searchable: true, + type: 'string', + }, + { + name: 'file.Ext.code_signature.status', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'file.Ext.code_signature' } }, + }, + ], + id: '1234', + title: 'logstash-*', + }; + expect(output).toEqual(expected); + }); }); + }); - test('it returns only nested fields when "item.nested" is "parent" and nested parent field is undefined', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = { - ...getMockNestedParentBuilderEntry(), - field: undefined, - }; - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { + describe('#getFormattedBuilderEntry', () => { + test('it returns entry with a value for "correspondingKeywordField" when "item.field" is of type "text" and matching keyword field exists', () => { + const payloadIndexPattern: IIndexPattern = { + ...getMockIndexPattern(), fields: [ - { ...getField('nestedField.child') }, - { ...getField('nestedField.nestedChild.doublyNestedChild') }, + ...fields, + { + name: 'machine.os.raw.text', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: true, + }, ], - id: '1234', - title: 'logstash-*', }; - expect(output).toEqual(expected); - }); - - test('it returns all fields unfiletered if "item.nested" is not "child" or "parent"', () => { - const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItem: FormattedBuilderEntry = getMockBuilderEntry(); - const output = getFilteredIndexPatterns(payloadIndexPattern, payloadItem); - const expected: IIndexPattern = { - fields: [...fields], - id: '1234', - title: 'logstash-*', + const payloadItem: BuilderEntry = { + ...getEntryMatchMock(), + field: 'machine.os.raw.text', + value: 'some os', + }; + const output = getFormattedBuilderEntry( + payloadIndexPattern, + payloadItem, + 0, + undefined, + undefined + ); + const expected: FormattedBuilderEntry = { + entryIndex: 0, + field: { + name: 'machine.os.raw.text', + type: 'string', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: true, + }, + nested: undefined, + operator: isOperator, + parent: undefined, + value: 'some os', + correspondingKeywordField: getField('machine.os.raw'), }; expect(output).toEqual(expected); }); - }); - describe('#getFormattedBuilderEntry', () => { test('it returns "FormattedBuilderEntry" with value "nested" of "child" when "parent" and "parentIndex" are defined', () => { const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); const payloadItem: BuilderEntry = { ...getEntryMatchMock(), field: 'child' }; @@ -188,6 +427,7 @@ describe('Exception builder helpers', () => { parentIndex: 1, }, value: 'some host name', + correspondingKeywordField: undefined, }; expect(output).toEqual(expected); }); @@ -218,6 +458,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some ip', + correspondingKeywordField: undefined, }; expect(output).toEqual(expected); }); @@ -225,7 +466,7 @@ describe('Exception builder helpers', () => { describe('#isEntryNested', () => { test('it returns "false" if payload is not of type EntryNested', () => { - const payload: BuilderEntry = { ...getEntryMatchMock() }; + const payload: BuilderEntry = getEntryMatchMock(); const output = isEntryNested(payload); const expected = false; expect(output).toEqual(expected); @@ -242,7 +483,7 @@ describe('Exception builder helpers', () => { describe('#getFormattedBuilderEntries', () => { test('it returns formatted entry with field undefined if it unable to find a matching index pattern field', () => { const payloadIndexPattern: IIndexPattern = getMockIndexPattern(); - const payloadItems: BuilderEntry[] = [{ ...getEntryMatchMock() }]; + const payloadItems: BuilderEntry[] = [getEntryMatchMock()]; const output = getFormattedBuilderEntries(payloadIndexPattern, payloadItems); const expected: FormattedBuilderEntry[] = [ { @@ -252,6 +493,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some host name', + correspondingKeywordField: undefined, }, ]; expect(output).toEqual(expected); @@ -281,6 +523,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some ip', + correspondingKeywordField: undefined, }, { entryIndex: 1, @@ -298,6 +541,7 @@ describe('Exception builder helpers', () => { operator: isOneOfOperator, parent: undefined, value: ['some extension'], + correspondingKeywordField: undefined, }, ]; expect(output).toEqual(expected); @@ -333,6 +577,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: 'some ip', + correspondingKeywordField: undefined, }, { entryIndex: 1, @@ -347,6 +592,7 @@ describe('Exception builder helpers', () => { operator: isOperator, parent: undefined, value: undefined, + correspondingKeywordField: undefined, }, { entryIndex: 0, @@ -383,6 +629,7 @@ describe('Exception builder helpers', () => { parentIndex: 1, }, value: 'some host name', + correspondingKeywordField: undefined, }, ]; expect(output).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx index 93bae091885c14..8585f58504e313 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx @@ -33,6 +33,8 @@ import { EmptyNestedEntry, } from '../types'; import { getEntryValue, getExceptionOperatorSelect } from '../helpers'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import exceptionableFields from '../exceptionable_fields.json'; /** * Returns filtered index patterns based on the field - if a user selects to @@ -45,13 +47,21 @@ import { getEntryValue, getExceptionOperatorSelect } from '../helpers'; */ export const getFilteredIndexPatterns = ( patterns: IIndexPattern, - item: FormattedBuilderEntry + item: FormattedBuilderEntry, + type: ExceptionListType ): IIndexPattern => { + const indexPatterns = { + ...patterns, + fields: patterns.fields.filter(({ name }) => + type === 'endpoint' ? exceptionableFields.includes(name) : true + ), + }; + if (item.nested === 'child' && item.parent != null) { // when user has selected a nested entry, only fields with the common parent are shown return { - ...patterns, - fields: patterns.fields.filter( + ...indexPatterns, + fields: indexPatterns.fields.filter( (field) => field.subType != null && field.subType.nested != null && @@ -61,20 +71,53 @@ export const getFilteredIndexPatterns = ( }; } else if (item.nested === 'parent' && item.field != null) { // when user has selected a nested entry, right above it we show the common parent - return { ...patterns, fields: [item.field] }; + return { ...indexPatterns, fields: [item.field] }; } else if (item.nested === 'parent' && item.field == null) { // when user selects to add a nested entry, only nested fields are shown as options return { - ...patterns, - fields: patterns.fields.filter( + ...indexPatterns, + fields: indexPatterns.fields.filter( (field) => field.subType != null && field.subType.nested != null ), }; } else { - return patterns; + return indexPatterns; } }; +/** + * Fields of type 'text' do not generate autocomplete values, we want + * to find it's corresponding keyword type (if available) which does + * generate autocomplete values + * + * @param fields IFieldType fields + * @param selectedField the field name that was selected + * @param isTextType we only want a corresponding keyword field if + * the selected field is of type 'text' + * + */ +export const getCorrespondingKeywordField = ({ + fields, + selectedField, +}: { + fields: IFieldType[]; + selectedField: string | undefined; +}): IFieldType | undefined => { + const selectedFieldBits = + selectedField != null && selectedField !== '' ? selectedField.split('.') : []; + const selectedFieldIsTextType = selectedFieldBits.slice(-1)[0] === 'text'; + + if (selectedFieldIsTextType && selectedFieldBits.length > 0) { + const keywordField = selectedFieldBits.slice(0, selectedFieldBits.length - 1).join('.'); + const [foundKeywordField] = fields.filter( + ({ name }) => keywordField !== '' && keywordField === name + ); + return foundKeywordField; + } + + return undefined; +}; + /** * Formats the entry into one that is easily usable for the UI, most of the * complexity was introduced with nested fields @@ -95,11 +138,16 @@ export const getFormattedBuilderEntry = ( ): FormattedBuilderEntry => { const { fields } = indexPattern; const field = parent != null ? `${parent.field}.${item.field}` : item.field; - const [selectedField] = fields.filter(({ name }) => field != null && field === name); + const [foundField] = fields.filter(({ name }) => field != null && field === name); + const correspondingKeywordField = getCorrespondingKeywordField({ + fields, + selectedField: field, + }); if (parent != null && parentIndex != null) { return { - field: selectedField, + field: foundField, + correspondingKeywordField, operator: getExceptionOperatorSelect(item), value: getEntryValue(item), nested: 'child', @@ -108,7 +156,8 @@ export const getFormattedBuilderEntry = ( }; } else { return { - field: selectedField, + field: foundField, + correspondingKeywordField, operator: getExceptionOperatorSelect(item), value: getEntryValue(item), nested: undefined, @@ -167,6 +216,7 @@ export const getFormattedBuilderEntries = ( value: undefined, entryIndex: index, parent: undefined, + correspondingKeywordField: undefined, }; // User has selected to add a nested field, but not yet selected the field diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx index 734434484fb4cc..b82607a541aaac 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useMemo, useReducer } from 'react'; +import React, { useCallback, useEffect, useReducer } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; @@ -29,8 +29,6 @@ import { getDefaultEmptyEntry, getDefaultNestedEmptyEntry, } from './helpers'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import exceptionableFields from '../exceptionable_fields.json'; const MyInvisibleAndBadge = styled(EuiFlexItem)` visibility: hidden; @@ -244,17 +242,6 @@ export const ExceptionBuilder = ({ setUpdateExceptions([...exceptions, { ...newException }]); }, [setUpdateExceptions, exceptions, listType, listId, listNamespaceType, ruleName]); - // Filters index pattern fields by exceptionable fields if list type is endpoint - const filterIndexPatterns = useMemo((): IIndexPattern => { - if (listType === 'endpoint') { - return { - ...indexPatterns, - fields: indexPatterns.fields.filter(({ name }) => exceptionableFields.includes(name)), - }; - } - return indexPatterns; - }, [indexPatterns, listType]); - // The builder can have existing exception items, or new exception items that have yet // to be created (and thus lack an id), this was creating some React bugs with relying // on the index, as a result, created a temporary id when new exception items are first @@ -368,7 +355,7 @@ export const ExceptionBuilder = ({ key={getExceptionListItemId(exceptionListItem, index)} exceptionItem={exceptionListItem} exceptionId={getExceptionListItemId(exceptionListItem, index)} - indexPattern={filterIndexPatterns} + indexPattern={indexPatterns} listType={listType} addNested={addNested} exceptionItemIndex={index} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 341d2f2bab37a5..6109b85f2da5a8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -40,6 +40,7 @@ import { getOperatingSystems, entryHasListType, entryHasNonEcsType, + lowercaseHashValues, } from '../helpers'; import { Loader } from '../../loader'; @@ -195,7 +196,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ ]; if (exceptionListType === 'endpoint') { const osTypes = exceptionItem._tags ? getOperatingSystems(exceptionItem._tags) : []; - enriched = enrichExceptionItemsWithOS(enriched, osTypes); + enriched = lowercaseHashValues(enrichExceptionItemsWithOS(enriched, osTypes)); } return enriched; }, [exceptionItemsToAdd, exceptionItem, comment, exceptionListType]); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json index fdf0ea60ecf6a8..037e340ee7fa2c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json @@ -6,32 +6,25 @@ "Target.process.Ext.code_signature.valid", "Target.process.Ext.services", "Target.process.Ext.user", - "Target.process.command_line", "Target.process.command_line.text", - "Target.process.executable", "Target.process.executable.text", "Target.process.hash.md5", "Target.process.hash.sha1", "Target.process.hash.sha256", "Target.process.hash.sha512", - "Target.process.name", "Target.process.name.text", "Target.process.parent.Ext.code_signature.status", "Target.process.parent.Ext.code_signature.subject_name", "Target.process.parent.Ext.code_signature.trusted", "Target.process.parent.Ext.code_signature.valid", - "Target.process.parent.command_line", "Target.process.parent.command_line.text", - "Target.process.parent.executable", "Target.process.parent.executable.text", "Target.process.parent.hash.md5", "Target.process.parent.hash.sha1", "Target.process.parent.hash.sha256", "Target.process.parent.hash.sha512", - "Target.process.parent.name", "Target.process.parent.name.text", "Target.process.parent.pgid", - "Target.process.parent.working_directory", "Target.process.parent.working_directory.text", "Target.process.pe.company", "Target.process.pe.description", @@ -39,7 +32,6 @@ "Target.process.pe.original_file_name", "Target.process.pe.product", "Target.process.pgid", - "Target.process.working_directory", "Target.process.working_directory.text", "agent.id", "agent.type", @@ -74,7 +66,6 @@ "file.mode", "file.name", "file.owner", - "file.path", "file.path.text", "file.pe.company", "file.pe.description", @@ -82,7 +73,6 @@ "file.pe.original_file_name", "file.pe.product", "file.size", - "file.target_path", "file.target_path.text", "file.type", "file.uid", @@ -94,10 +84,8 @@ "host.id", "host.os.Ext.variant", "host.os.family", - "host.os.full", "host.os.full.text", "host.os.kernel", - "host.os.name", "host.os.name.text", "host.os.platform", "host.os.version", @@ -108,32 +96,25 @@ "process.Ext.code_signature.valid", "process.Ext.services", "process.Ext.user", - "process.command_line", "process.command_line.text", - "process.executable", "process.executable.text", "process.hash.md5", "process.hash.sha1", "process.hash.sha256", "process.hash.sha512", - "process.name", "process.name.text", "process.parent.Ext.code_signature.status", "process.parent.Ext.code_signature.subject_name", "process.parent.Ext.code_signature.trusted", "process.parent.Ext.code_signature.valid", - "process.parent.command_line", "process.parent.command_line.text", - "process.parent.executable", "process.parent.executable.text", "process.parent.hash.md5", "process.parent.hash.sha1", "process.parent.hash.sha256", "process.parent.hash.sha512", - "process.parent.name", "process.parent.name.text", "process.parent.pgid", - "process.parent.working_directory", "process.parent.working_directory.text", "process.pe.company", "process.pe.description", @@ -141,7 +122,10 @@ "process.pe.original_file_name", "process.pe.product", "process.pgid", - "process.working_directory", "process.working_directory.text", - "rule.uuid" + "rule.uuid", + "user.domain", + "user.email", + "user.hash", + "user.id" ] \ No newline at end of file diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 5cb65ee6db8ffc..4236f347ac7ffd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -24,6 +24,7 @@ import { entryHasListType, entryHasNonEcsType, prepareExceptionItemsForBulkClose, + lowercaseHashValues, } from './helpers'; import { EmptyEntry } from './types'; import { @@ -364,7 +365,7 @@ describe('Exception helpers', () => { const mockEmptyException: EntryNested = { field: '', type: OperatorTypeEnum.NESTED, - entries: [{ ...getEntryMatchMock() }], + entries: [getEntryMatchMock()], }; const output: Array< ExceptionListItemSchema | CreateExceptionListItemSchema @@ -663,4 +664,48 @@ describe('Exception helpers', () => { expect(result).toEqual(expected); }); }); + + describe('#lowercaseHashValues', () => { + test('it should return an empty array with an empty array', () => { + const payload: ExceptionListItemSchema[] = []; + const result = lowercaseHashValues(payload); + expect(result).toEqual([]); + }); + + test('it should return all list items with entry hashes lowercased', () => { + const payload = [ + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'DDDFFF' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'aaabbb' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'user.hash', type: 'match_any', value: ['aaabbb', 'DDDFFF'] }, + ] as EntriesArray, + }, + ]; + const result = lowercaseHashValues(payload); + expect(result).toEqual([ + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'dddfff' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'aaabbb' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'user.hash', type: 'match_any', value: ['aaabbb', 'dddfff'] }, + ] as EntriesArray, + }, + ]); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 3abb788312ff43..2b526ede12acfd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -335,6 +335,36 @@ export const enrichExceptionItemsWithOS = ( }); }; +/** + * Returns given exceptionItems with all hash-related entries lowercased + */ +export const lowercaseHashValues = ( + exceptionItems: Array +): Array => { + return exceptionItems.map((item) => { + const newEntries = item.entries.map((itemEntry) => { + if (itemEntry.field.includes('.hash')) { + if (itemEntry.type === 'match') { + return { + ...itemEntry, + value: itemEntry.value.toLowerCase(), + }; + } else if (itemEntry.type === 'match_any') { + return { + ...itemEntry, + value: itemEntry.value.map((val) => val.toLowerCase()), + }; + } + } + return itemEntry; + }); + return { + ...item, + entries: newEntries, + }; + }); +}; + /** * Returns the value for the given fieldname within TimelineNonEcsData if it exists */ @@ -413,7 +443,7 @@ export const defaultEndpointExceptionItems = ( data: alertData, fieldName: 'file.Ext.code_signature.trusted', }); - const [sha1Hash] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.hash.sha1' }); + const [sha256Hash] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.hash.sha256' }); const [eventCode] = getMappedNonEcsValue({ data: alertData, fieldName: 'event.code' }); const namespaceType = 'agnostic'; @@ -446,10 +476,10 @@ export const defaultEndpointExceptionItems = ( value: filePath ?? '', }, { - field: 'file.hash.sha1', + field: 'file.hash.sha256', operator: 'included', type: 'match', - value: sha1Hash ?? '', + value: sha256Hash ?? '', }, { field: 'event.code', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts index 87d2f9dcda9351..b826c1e49f2749 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts @@ -95,6 +95,13 @@ export const EXCEPTION_EMPTY_PROMPT_TITLE = i18n.translate( } ); +export const EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody', + { + defaultMessage: 'No search results found.', + } +); + export const EXCEPTION_EMPTY_PROMPT_BODY = i18n.translate( 'xpack.securitySolution.exceptions.viewer.emptyPromptBody', { @@ -176,3 +183,10 @@ export const ADD_TO_CLIPBOARD = i18n.translate( export const DESCRIPTION = i18n.translate('xpack.securitySolution.exceptions.descriptionLabel', { defaultMessage: 'Description', }); + +export const TOTAL_ITEMS_FETCH_ERROR = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.fetchTotalsError', + { + defaultMessage: 'Error getting exception item totals', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts index 54caab03e615a2..9b7c68848c41fb 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts @@ -38,14 +38,14 @@ export interface ExceptionListItemIdentifiers { export interface FilterOptions { filter: string; - showDetectionsList: boolean; - showEndpointList: boolean; tags: string[]; } export interface Filter { filter: Partial; pagination: Partial; + showDetectionsListsOnly: boolean; + showEndpointListsOnly: boolean; } export interface ExceptionsPagination { @@ -62,6 +62,7 @@ export interface FormattedBuilderEntry { nested: 'parent' | 'child' | undefined; entryIndex: number; parent: { parent: EntryNested; parentIndex: number } | undefined; + correspondingKeywordField: IFieldType | undefined; } export interface EmptyEntry { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx index 7069e99943f7b4..bcc4adf54c9d7e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx @@ -52,6 +52,14 @@ const MyActionButton = styled(EuiFlexItem)` align-self: flex-end; `; +const MyNestedValueContainer = styled.div` + margin-left: ${({ theme }) => theme.eui.euiSizeL}; +`; + +const MyNestedValue = styled.span` + margin-left: ${({ theme }) => theme.eui.euiSizeS}; +`; + interface ExceptionEntriesComponentProps { entries: FormattedEntry[]; disableDelete: boolean; @@ -78,10 +86,10 @@ const ExceptionEntriesComponent = ({ render: (value: string | null, data: FormattedEntry) => { if (value != null && data.isNested) { return ( - <> - - {value} - + + + {value} + ); } else { return value ?? getEmptyValue(); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx index dcc8611cd7298b..768af7b837d9b5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx @@ -80,7 +80,6 @@ describe('ExceptionsViewerPagination', () => { wrapper.find('button[data-test-subj="exceptionsPerPageItem"]').at(0).simulate('click'); expect(mockOnPaginationChange).toHaveBeenCalledWith({ - filter: {}, pagination: { pageIndex: 0, pageSize: 20, totalItemCount: 1 }, }); }); @@ -127,8 +126,7 @@ describe('ExceptionsViewerPagination', () => { wrapper.find('[data-test-subj="pagination-button-next"]').at(1).simulate('click'); expect(mockOnPaginationChange).toHaveBeenCalledWith({ - filter: {}, - pagination: { pageIndex: 2, pageSize: 50, totalItemCount: 160 }, + pagination: { pageIndex: 1, pageSize: 50, totalItemCount: 160 }, }); }); @@ -151,8 +149,7 @@ describe('ExceptionsViewerPagination', () => { wrapper.find('button[data-test-subj="pagination-button-3"]').simulate('click'); expect(mockOnPaginationChange).toHaveBeenCalledWith({ - filter: {}, - pagination: { pageIndex: 4, pageSize: 50, totalItemCount: 160 }, + pagination: { pageIndex: 3, pageSize: 50, totalItemCount: 160 }, }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx index afc6d55de364d7..ae1a7771164410 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx @@ -20,7 +20,7 @@ import { ExceptionsPagination, Filter } from '../types'; interface ExceptionsViewerPaginationProps { pagination: ExceptionsPagination; - onPaginationChange: (arg: Filter) => void; + onPaginationChange: (arg: Partial) => void; } const ExceptionsViewerPaginationComponent = ({ @@ -39,9 +39,8 @@ const ExceptionsViewerPaginationComponent = ({ const handlePageClick = useCallback( (pageIndex: number): void => { onPaginationChange({ - filter: {}, pagination: { - pageIndex: pageIndex + 1, + pageIndex, pageSize: pagination.pageSize, totalItemCount: pagination.totalItemCount, }, @@ -57,9 +56,8 @@ const ExceptionsViewerPaginationComponent = ({ icon="empty" onClick={() => { onPaginationChange({ - filter: {}, pagination: { - pageIndex: pagination.pageIndex, + pageIndex: 0, pageSize: rows, totalItemCount: pagination.totalItemCount, }, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx index d697023b2ced4e..6927ecec788fb2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx @@ -22,12 +22,8 @@ describe('ExceptionsViewerUtility', () => { totalItemCount: 2, pageSizeOptions: [5, 10, 20, 50, 100], }} - filterOptions={{ - filter: '', - showEndpointList: false, - showDetectionsList: false, - tags: [], - }} + showEndpointListsOnly={false} + showDetectionsListsOnly={false} ruleSettingsUrl={'some/url'} onRefreshClick={jest.fn()} /> @@ -49,12 +45,8 @@ describe('ExceptionsViewerUtility', () => { totalItemCount: 1, pageSizeOptions: [5, 10, 20, 50, 100], }} - filterOptions={{ - filter: '', - showEndpointList: false, - showDetectionsList: false, - tags: [], - }} + showEndpointListsOnly={false} + showDetectionsListsOnly={false} ruleSettingsUrl={'some/url'} onRefreshClick={jest.fn()} /> @@ -77,12 +69,8 @@ describe('ExceptionsViewerUtility', () => { totalItemCount: 1, pageSizeOptions: [5, 10, 20, 50, 100], }} - filterOptions={{ - filter: '', - showEndpointList: false, - showDetectionsList: false, - tags: [], - }} + showEndpointListsOnly={false} + showDetectionsListsOnly={false} ruleSettingsUrl={'some/url'} onRefreshClick={mockOnRefreshClick} /> @@ -104,12 +92,8 @@ describe('ExceptionsViewerUtility', () => { totalItemCount: 1, pageSizeOptions: [5, 10, 20, 50, 100], }} - filterOptions={{ - filter: '', - showEndpointList: false, - showDetectionsList: false, - tags: [], - }} + showEndpointListsOnly={false} + showDetectionsListsOnly={false} ruleSettingsUrl={'some/url'} onRefreshClick={jest.fn()} /> @@ -130,12 +114,8 @@ describe('ExceptionsViewerUtility', () => { totalItemCount: 1, pageSizeOptions: [5, 10, 20, 50, 100], }} - filterOptions={{ - filter: '', - showEndpointList: false, - showDetectionsList: true, - tags: [], - }} + showEndpointListsOnly={false} + showDetectionsListsOnly ruleSettingsUrl={'some/url'} onRefreshClick={jest.fn()} /> @@ -156,12 +136,8 @@ describe('ExceptionsViewerUtility', () => { totalItemCount: 1, pageSizeOptions: [5, 10, 20, 50, 100], }} - filterOptions={{ - filter: '', - showEndpointList: true, - showDetectionsList: false, - tags: [], - }} + showEndpointListsOnly + showDetectionsListsOnly={false} ruleSettingsUrl={'some/url'} onRefreshClick={jest.fn()} /> diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx index 9ab4e170f4090f..206983f3d82d97 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from 'react-intl'; import styled from 'styled-components'; import * as i18n from '../translations'; -import { ExceptionsPagination, FilterOptions } from '../types'; +import { ExceptionsPagination } from '../types'; import { UtilityBar, UtilityBarSection, @@ -29,14 +29,16 @@ const MyUtilities = styled(EuiFlexGroup)` interface ExceptionsViewerUtilityProps { pagination: ExceptionsPagination; - filterOptions: FilterOptions; + showEndpointListsOnly: boolean; + showDetectionsListsOnly: boolean; ruleSettingsUrl: string; onRefreshClick: () => void; } const ExceptionsViewerUtilityComponent: React.FC = ({ pagination, - filterOptions, + showEndpointListsOnly, + showDetectionsListsOnly, ruleSettingsUrl, onRefreshClick, }): JSX.Element => ( @@ -65,7 +67,7 @@ const ExceptionsViewerUtilityComponent: React.FC = - {filterOptions.showEndpointList && ( + {showEndpointListsOnly && ( = }} /> )} - {filterOptions.showDetectionsList && ( + {showDetectionsListsOnly && ( { expect(mockOnFilterChange).toHaveBeenCalledWith({ filter: { filter: '', - showDetectionsList: true, - showEndpointList: false, tags: [], }, - pagination: {}, + pagination: { + pageIndex: 0, + }, + showDetectionsListsOnly: true, + showEndpointListsOnly: false, }); }); @@ -175,11 +177,13 @@ describe('ExceptionsViewerHeader', () => { expect(mockOnFilterChange).toHaveBeenCalledWith({ filter: { filter: '', - showDetectionsList: false, - showEndpointList: true, tags: [], }, - pagination: {}, + pagination: { + pageIndex: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: true, }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx index c207f91f651ed3..9cbe5f0f36891a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx @@ -26,7 +26,7 @@ interface ExceptionsViewerHeaderProps { supportedListTypes: ExceptionListTypeEnum[]; detectionsListItems: number; endpointListItems: number; - onFilterChange: (arg: Filter) => void; + onFilterChange: (arg: Partial) => void; onAddExceptionClick: (type: ExceptionListTypeEnum) => void; } @@ -43,16 +43,20 @@ const ExceptionsViewerHeaderComponent = ({ }: ExceptionsViewerHeaderProps): JSX.Element => { const [filter, setFilter] = useState(''); const [tags, setTags] = useState([]); - const [showDetectionsList, setShowDetectionsList] = useState(false); - const [showEndpointList, setShowEndpointList] = useState(false); + const [showDetectionsListsOnly, setShowDetectionsList] = useState(false); + const [showEndpointListsOnly, setShowEndpointList] = useState(false); const [isAddExceptionMenuOpen, setAddExceptionMenuOpen] = useState(false); useEffect((): void => { onFilterChange({ - filter: { filter, showDetectionsList, showEndpointList, tags }, - pagination: {}, + filter: { filter, tags }, + pagination: { + pageIndex: 0, + }, + showDetectionsListsOnly, + showEndpointListsOnly, }); - }, [filter, tags, showDetectionsList, showEndpointList, onFilterChange]); + }, [filter, tags, showDetectionsListsOnly, showEndpointListsOnly, onFilterChange]); const onAddExceptionDropdownClick = useCallback( (): void => setAddExceptionMenuOpen(!isAddExceptionMenuOpen), @@ -60,14 +64,14 @@ const ExceptionsViewerHeaderComponent = ({ ); const handleDetectionsListClick = useCallback((): void => { - setShowDetectionsList(!showDetectionsList); + setShowDetectionsList(!showDetectionsListsOnly); setShowEndpointList(false); - }, [showDetectionsList, setShowDetectionsList, setShowEndpointList]); + }, [showDetectionsListsOnly, setShowDetectionsList, setShowEndpointList]); const handleEndpointListClick = useCallback((): void => { - setShowEndpointList(!showEndpointList); + setShowEndpointList(!showEndpointListsOnly); setShowDetectionsList(false); - }, [showEndpointList, setShowEndpointList, setShowDetectionsList]); + }, [showEndpointListsOnly, setShowEndpointList, setShowDetectionsList]); const handleOnSearch = useCallback( (searchValue: string): void => { @@ -148,7 +152,7 @@ const ExceptionsViewerHeaderComponent = ({ @@ -157,7 +161,7 @@ const ExceptionsViewerHeaderComponent = ({ diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx similarity index 78% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_item.test.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx index 7ccb8d251eae10..3024ae0f141448 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx @@ -9,6 +9,7 @@ import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import * as i18n from '../translations'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { ExceptionsViewerItems } from './exceptions_viewer_items'; @@ -17,7 +18,8 @@ describe('ExceptionsViewerItems', () => { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptTitle"]').text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_TITLE + ); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_BODY + ); + }); + + it('it renders no search results found prompt if "showNoResults" is "true"', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptTitle"]').text()).toEqual(''); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').text()).toEqual( + i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY + ); }); it('it renders exceptions if "showEmpty" and "isInitLoading" is "false", and exceptions exist', () => { @@ -37,6 +69,7 @@ describe('ExceptionsViewerItems', () => { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> { ({ eui: euiLightVars, darkMode: false })}> = ({ showEmpty, + showNoResults, isInitLoading, exceptions, loadingItemIds, @@ -51,12 +53,22 @@ const ExceptionsViewerItemsComponent: React.FC = ({ onEditExceptionItem, }): JSX.Element => ( - {showEmpty || isInitLoading ? ( + {showEmpty || showNoResults || isInitLoading ? ( {i18n.EXCEPTION_EMPTY_PROMPT_TITLE}} - body={

{i18n.EXCEPTION_EMPTY_PROMPT_BODY}

} + iconType={showNoResults ? 'searchProfilerApp' : 'list'} + title={ +

+ {showNoResults ? '' : i18n.EXCEPTION_EMPTY_PROMPT_TITLE} +

+ } + body={ +

+ {showNoResults + ? i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY + : i18n.EXCEPTION_EMPTY_PROMPT_BODY} +

+ } data-test-subj="exceptionsEmptyPrompt" />
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx index 5d4340db9a4488..5f6e54b0d3cffd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx @@ -106,13 +106,13 @@ describe('Exception viewer helpers', () => { value: undefined, }, { - fieldName: 'host.name.host.name', + fieldName: 'host.name', isNested: true, operator: 'is', value: 'some host name', }, { - fieldName: 'host.name.host.name', + fieldName: 'host.name', isNested: true, operator: 'is one of', value: ['some host name'], @@ -138,9 +138,9 @@ describe('Exception viewer helpers', () => { test('it formats as expected when "isNested" is "true"', () => { const payload = getEntryMatchMock(); - const formattedEntry = formatEntry({ isNested: true, parent: 'parent', item: payload }); + const formattedEntry = formatEntry({ isNested: true, item: payload }); const expected: FormattedEntry = { - fieldName: 'parent.host.name', + fieldName: 'host.name', isNested: true, operator: 'is', value: 'some host name', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx index 345db5bf1e75ef..86b0512410e6f4 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx @@ -20,18 +20,16 @@ import * as i18n from '../translations'; */ export const formatEntry = ({ isNested, - parent, item, }: { isNested: boolean; - parent?: string; item: BuilderEntry; }): FormattedEntry => { const operator = getExceptionOperatorSelect(item); const value = getEntryValue(item); return { - fieldName: isNested ? `${parent}.${item.field}` : item.field ?? '', + fieldName: item.field ?? '', operator: operator.message, value, isNested, @@ -57,7 +55,6 @@ export const getFormattedEntries = (entries: BuilderEntry[]): FormattedEntry[] = (acc, nestedEntry) => { const formattedEntry = formatEntry({ isNested: true, - parent: item.field, item: nestedEntry, }); return [...acc, { ...formattedEntry }]; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index 7f4d8252764060..7482068454a97c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo, useEffect, useReducer } from 'react'; +import React, { useCallback, useEffect, useReducer } from 'react'; import { EuiSpacer } from '@elastic/eui'; import uuid from 'uuid'; @@ -31,23 +31,23 @@ import { EditExceptionModal } from '../edit_exception_modal'; import { AddExceptionModal } from '../add_exception_modal'; const initialState: State = { - filterOptions: { filter: '', showEndpointList: false, showDetectionsList: false, tags: [] }, + filterOptions: { filter: '', tags: [] }, pagination: { pageIndex: 0, pageSize: 20, totalItemCount: 0, pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], }, - endpointList: null, - detectionsList: null, - allExceptions: [], exceptions: [], exceptionToEdit: null, - loadingLists: [], loadingItemIds: [], isInitLoading: true, currentModal: null, exceptionListTypeToEdit: null, + totalEndpointItems: 0, + totalDetectionsItems: 0, + showEndpointListsOnly: false, + showDetectionsListsOnly: false, }; interface ExceptionsViewerProps { @@ -87,46 +87,47 @@ const ExceptionsViewerComponent = ({ ); const [ { - endpointList, - detectionsList, exceptions, filterOptions, pagination, - loadingLists, loadingItemIds, isInitLoading, currentModal, exceptionToEdit, exceptionListTypeToEdit, + totalEndpointItems, + totalDetectionsItems, + showDetectionsListsOnly, + showEndpointListsOnly, }, dispatch, - ] = useReducer(allExceptionItemsReducer(), { ...initialState, loadingLists: exceptionListsMeta }); - const { deleteExceptionItem } = useApi(services.http); + ] = useReducer(allExceptionItemsReducer(), { ...initialState }); + const { deleteExceptionItem, getExceptionListsItems } = useApi(services.http); const setExceptions = useCallback( - ({ - lists: newLists, - exceptions: newExceptions, - pagination: newPagination, - }: UseExceptionListSuccess): void => { + ({ exceptions: newExceptions, pagination: newPagination }: UseExceptionListSuccess): void => { dispatch({ type: 'setExceptions', - lists: newLists, + lists: exceptionListsMeta, exceptions: newExceptions, pagination: newPagination, }); }, - [dispatch] + [dispatch, exceptionListsMeta] ); - const [loadingList, , , , fetchList] = useExceptionList({ + const [loadingList, , , fetchListItems] = useExceptionList({ http: services.http, - lists: loadingLists, - filterOptions, + lists: exceptionListsMeta, + filterOptions: + filterOptions.filter !== '' || filterOptions.tags.length > 0 ? [filterOptions] : [], pagination: { page: pagination.pageIndex + 1, perPage: pagination.pageSize, total: pagination.totalItemCount, }, + showDetectionsListsOnly, + showEndpointListsOnly, + matchFilters: true, onSuccess: setExceptions, onError: onDispatchToaster({ color: 'danger', @@ -145,22 +146,81 @@ const ExceptionsViewerComponent = ({ [dispatch] ); + const setExceptionItemTotals = useCallback( + (endpointItemTotals: number | null, detectionItemTotals: number | null): void => { + dispatch({ + type: 'setExceptionItemTotals', + totalEndpointItems: endpointItemTotals, + totalDetectionsItems: detectionItemTotals, + }); + }, + [dispatch] + ); + + const handleGetTotals = useCallback(async (): Promise => { + await getExceptionListsItems({ + lists: exceptionListsMeta, + filterOptions: [], + pagination: { + page: 0, + perPage: 1, + total: 0, + }, + showDetectionsListsOnly: true, + showEndpointListsOnly: false, + onSuccess: ({ pagination: detectionPagination }) => { + setExceptionItemTotals(null, detectionPagination.total ?? 0); + }, + onError: () => { + const dispatchToasterError = onDispatchToaster({ + color: 'danger', + title: i18n.TOTAL_ITEMS_FETCH_ERROR, + iconType: 'alert', + }); + + dispatchToasterError(); + }, + }); + await getExceptionListsItems({ + lists: exceptionListsMeta, + filterOptions: [], + pagination: { + page: 0, + perPage: 1, + total: 0, + }, + showDetectionsListsOnly: false, + showEndpointListsOnly: true, + onSuccess: ({ pagination: endpointPagination }) => { + setExceptionItemTotals(endpointPagination.total ?? 0, null); + }, + onError: () => { + const dispatchToasterError = onDispatchToaster({ + color: 'danger', + title: i18n.TOTAL_ITEMS_FETCH_ERROR, + iconType: 'alert', + }); + + dispatchToasterError(); + }, + }); + }, [setExceptionItemTotals, exceptionListsMeta, getExceptionListsItems, onDispatchToaster]); + const handleFetchList = useCallback((): void => { - if (fetchList != null) { - fetchList(); + if (fetchListItems != null) { + fetchListItems(); + handleGetTotals(); } - }, [fetchList]); + }, [fetchListItems, handleGetTotals]); const handleFilterChange = useCallback( - ({ filter, pagination: pag }: Filter): void => { + (filters: Partial): void => { dispatch({ type: 'updateFilterOptions', - filterOptions: filter, - pagination: pag, - allLists: exceptionListsMeta, + filters, }); }, - [dispatch, exceptionListsMeta] + [dispatch] ); const handleAddException = useCallback( @@ -178,12 +238,13 @@ const ExceptionsViewerComponent = ({ (exception: ExceptionListItemSchema): void => { dispatch({ type: 'updateExceptionToEdit', + lists: exceptionListsMeta, exception, }); setCurrentModal('editModal'); }, - [setCurrentModal] + [setCurrentModal, exceptionListsMeta] ); const handleOnCancelExceptionModal = useCallback((): void => { @@ -235,23 +296,24 @@ const ExceptionsViewerComponent = ({ // Logic for initial render useEffect((): void => { if (isInitLoading && !loadingList && (exceptions.length === 0 || exceptions != null)) { + handleGetTotals(); dispatch({ type: 'updateIsInitLoading', loading: false, }); } - }, [isInitLoading, exceptions, loadingList, dispatch]); + }, [handleGetTotals, isInitLoading, exceptions, loadingList, dispatch]); // Used in utility bar info text - const ruleSettingsUrl = useMemo((): string => { - return services.application.getUrlForApp( - `security/detections/rules/id/${encodeURI(ruleId)}/edit` - ); - }, [ruleId, services.application]); + const ruleSettingsUrl = services.application.getUrlForApp( + `security/detections/rules/id/${encodeURI(ruleId)}/edit` + ); + + const showEmpty: boolean = + !isInitLoading && !loadingList && totalEndpointItems === 0 && totalDetectionsItems === 0; - const showEmpty = useMemo((): boolean => { - return !isInitLoading && !loadingList && exceptions.length === 0; - }, [isInitLoading, exceptions.length, loadingList]); + const showNoResults: boolean = + exceptions.length === 0 && (totalEndpointItems > 0 || totalDetectionsItems > 0); return ( <> @@ -288,8 +350,8 @@ const ExceptionsViewerComponent = ({ @@ -298,13 +360,15 @@ const ExceptionsViewerComponent = ({ ; - pagination: Partial; - allLists: ExceptionIdentifiers[]; + filters: Partial; } | { type: 'updateIsInitLoading'; loading: boolean } | { type: 'updateModalOpen'; modalName: ViewerModalName } - | { type: 'updateExceptionToEdit'; exception: ExceptionListItemSchema } + | { + type: 'updateExceptionToEdit'; + lists: ExceptionIdentifiers[]; + exception: ExceptionListItemSchema; + } | { type: 'updateLoadingItemIds'; items: ExceptionListItemIdentifiers[] } - | { type: 'updateExceptionListTypeToEdit'; exceptionListType: ExceptionListType | null }; + | { type: 'updateExceptionListTypeToEdit'; exceptionListType: ExceptionListType | null } + | { + type: 'setExceptionItemTotals'; + totalEndpointItems: number | null; + totalDetectionsItems: number | null; + }; export const allExceptionItemsReducer = () => (state: State, action: Action): State => { switch (action.type) { case 'setExceptions': { - const endpointList = action.lists.filter((t) => t.type === 'endpoint'); - const detectionsList = action.lists.filter((t) => t.type === 'detection'); + const { exceptions, pagination } = action; return { ...state, - endpointList: state.filterOptions.showDetectionsList - ? state.endpointList - : endpointList[0] ?? null, - detectionsList: state.filterOptions.showEndpointList - ? state.detectionsList - : detectionsList[0] ?? null, pagination: { ...state.pagination, - pageIndex: action.pagination.page - 1, - pageSize: action.pagination.perPage, - totalItemCount: action.pagination.total ?? 0, + pageIndex: pagination.page - 1, + pageSize: pagination.perPage, + totalItemCount: pagination.total ?? 0, }, - allExceptions: action.exceptions, - exceptions: action.exceptions, + exceptions, }; } case 'updateFilterOptions': { - const returnState = { + const { filter, pagination, showEndpointListsOnly, showDetectionsListsOnly } = action.filters; + return { ...state, filterOptions: { ...state.filterOptions, - ...action.filterOptions, + ...filter, }, pagination: { ...state.pagination, - ...action.pagination, + ...pagination, }, + showEndpointListsOnly: showEndpointListsOnly ?? state.showEndpointListsOnly, + showDetectionsListsOnly: showDetectionsListsOnly ?? state.showDetectionsListsOnly, + }; + } + case 'setExceptionItemTotals': { + return { + ...state, + totalEndpointItems: + action.totalEndpointItems == null ? state.totalEndpointItems : action.totalEndpointItems, + totalDetectionsItems: + action.totalDetectionsItems == null + ? state.totalDetectionsItems + : action.totalDetectionsItems, }; - - if (action.filterOptions.showEndpointList) { - const list = action.allLists.filter((t) => t.type === 'endpoint'); - - return { - ...returnState, - loadingLists: list, - exceptions: list.length === 0 ? [] : [...state.exceptions], - }; - } else if (action.filterOptions.showDetectionsList) { - const list = action.allLists.filter((t) => t.type === 'detection'); - - return { - ...returnState, - loadingLists: list, - exceptions: list.length === 0 ? [] : [...state.exceptions], - }; - } else { - return { - ...returnState, - loadingLists: action.allLists, - }; - } } case 'updateIsInitLoading': { return { @@ -121,13 +115,13 @@ export const allExceptionItemsReducer = () => (state: State, action: Action): St }; } case 'updateExceptionToEdit': { - const exception = action.exception; - const exceptionListToEdit = [state.endpointList, state.detectionsList].find((list) => { - return list !== null && exception.list_id === list.list_id; + const { exception, lists } = action; + const exceptionListToEdit = lists.find((list) => { + return list !== null && exception.list_id === list.listId; }); return { ...state, - exceptionToEdit: action.exception, + exceptionToEdit: exception, exceptionListTypeToEdit: exceptionListToEdit ? exceptionListToEdit.type : null, }; } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx index cbbe43cc03568f..0ba9764cf24af4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx @@ -5,14 +5,15 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; -import { AlertsUtilityBar } from './index'; +import { AlertsUtilityBar, AlertsUtilityBarProps } from './index'; +import { TestProviders } from '../../../../common/mock/test_providers'; jest.mock('../../../../common/lib/kibana'); describe('AlertsUtilityBar', () => { - it('renders correctly', () => { + test('renders correctly', () => { const wrapper = shallow( { expect(wrapper.find('[dataTestSubj="alertActionPopover"]')).toBeTruthy(); }); + + describe('UtilityBarAdditionalFiltersContent', () => { + test('does not show the showBuildingBlockAlerts checked if the showBuildingBlockAlerts is false', () => { + const onShowBuildingBlockAlertsChanged = jest.fn(); + const wrapper = mount( + + + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should be false + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(false); + }); + + test('does show the showBuildingBlockAlerts checked if the showBuildingBlockAlerts is true', () => { + const onShowBuildingBlockAlertsChanged = jest.fn(); + const wrapper = mount( + + + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should be true + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(true); + }); + + test('calls the onShowBuildingBlockAlertsChanged when the check box is clicked', () => { + const onShowBuildingBlockAlertsChanged = jest.fn(); + const wrapper = mount( + + + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // check the box + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .simulate('change', { target: { checked: true } }); + + // Make sure our callback is called + expect(onShowBuildingBlockAlertsChanged).toHaveBeenCalled(); + }); + + test('can update showBuildingBlockAlerts from false to true', () => { + const Proxy = (props: AlertsUtilityBarProps) => ( + + + + ); + + const wrapper = mount( + + ); + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should false now since we initially set the showBuildingBlockAlerts to false + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(false); + + wrapper.setProps({ showBuildingBlockAlerts: true }); + wrapper.update(); + + // click the filters button to popup the checkbox to make it visible + wrapper + .find('[data-test-subj="additionalFilters"] button') + .first() + .simulate('click') + .update(); + + // The check box should be true now since we changed the showBuildingBlockAlerts from false to true + expect( + wrapper + .find('[data-test-subj="showBuildingBlockAlertsCheckbox"] input') + .first() + .prop('checked') + ).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx index bedc23790541c2..bdad380f59ae95 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx @@ -28,7 +28,7 @@ import { TimelineNonEcsData } from '../../../../graphql/types'; import { UpdateAlertsStatus } from '../types'; import { FILTER_CLOSED, FILTER_IN_PROGRESS, FILTER_OPEN } from '../alerts_filter_group'; -interface AlertsUtilityBarProps { +export interface AlertsUtilityBarProps { canUserCRUD: boolean; hasIndexWrite: boolean; areEventsLoading: boolean; @@ -223,5 +223,6 @@ export const AlertsUtilityBar = React.memo( prevProps.areEventsLoading === nextProps.areEventsLoading && prevProps.selectedEventIds === nextProps.selectedEventIds && prevProps.totalCount === nextProps.totalCount && - prevProps.showClearSelection === nextProps.showClearSelection + prevProps.showClearSelection === nextProps.showClearSelection && + prevProps.showBuildingBlockAlerts === nextProps.showBuildingBlockAlerts ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 010129d2d45933..f38a9107afca98 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -202,7 +202,7 @@ export const requiredFieldsForActions = [ 'file.path', 'file.Ext.code_signature.subject_name', 'file.Ext.code_signature.trusted', - 'file.hash.sha1', + 'file.hash.sha256', 'host.os.family', 'event.code', ]; diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index f40161ff9b4c20..84096e242cbbdf 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 d7a2535fb1f54e..90d1b8bd54df53 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 aa562b9a202017..54d9131209d0d8 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 621fab2e4ee113..4a4326d5b29193 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 b6e18506b61113..8ff4ad5a043b5f 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 edeca5659ee38c..74bebf211258ae 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 05af1ee062de6f..355c2bb5c19fc8 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 7f68baa4b85bdc..e54f7df4d4f75a 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 6e0823a920413f..ca006f21c29ac2 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 582a59cfd7605c..6c949e9700b9a0 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/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx index 212c8977a88526..b22ff406a1605e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx @@ -18,7 +18,7 @@ import { import { useHistory } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { useToasts } from '../../../../../common/lib/kibana'; import { useHostSelector } from '../hooks'; import { urlFromQueryParams } from '../url_from_query_params'; import { @@ -44,7 +44,7 @@ import { useFormatUrl } from '../../../../../common/components/link_to'; export const HostDetailsFlyout = memo(() => { const history = useHistory(); - const { notifications } = useKibana(); + const toasts = useToasts(); const queryParams = useHostSelector(uiQueryParams); const { selected_host: selectedHost, ...queryParamsWithoutSelectedHost } = queryParams; const details = useHostSelector(detailsData); @@ -58,23 +58,16 @@ export const HostDetailsFlyout = memo(() => { useEffect(() => { if (error !== undefined) { - notifications.toasts.danger({ - title: ( - - ), - body: ( - - ), - toastLifeTimeMs: 10000, + toasts.addDanger({ + title: i18n.translate('xpack.securitySolution.endpoint.host.details.errorTitle', { + defaultMessage: 'Could not find host', + }), + text: i18n.translate('xpack.securitySolution.endpoint.host.details.errorBody', { + defaultMessage: 'Please exit the flyout and select an available host.', + }), }); } - }, [error, notifications.toasts]); + }, [error, toasts]); return ( 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 4cdfaad69eb726..3a1dd180405e04 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 9d49c8705affe2..3e00a5cc33db1d 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 2692f7791b7c05..58442ab417b606 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 8203aae244f246..6ee6a4232f7cfb 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 b4e1da4e43da32..6fe555113617d9 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 7daa500ef18844..83cd8558306a0e 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 c6e6146f4d5e4b..266faf9eae32c9 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 b5c67cc2c20140..3c9d5fde9b826b 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 6ed4e06ee51c5c..c81ffb0060c888 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]()); } } @@ -255,11 +257,11 @@ describe('Policy Details', () => { policyView.update(); // Toast notification should be shown - const toastAddMock = coreStart.notifications.toasts.add.mock; + const toastAddMock = coreStart.notifications.toasts.addSuccess.mock; expect(toastAddMock.calls).toHaveLength(1); expect(toastAddMock.calls[0][0]).toMatchObject({ - color: 'success', - iconType: 'check', + title: 'Success!', + text: expect.any(Function), }); }); it('should show an error notification toast if update fails', async () => { @@ -270,11 +272,11 @@ describe('Policy Details', () => { policyView.update(); // Toast notification should be shown - const toastAddMock = coreStart.notifications.toasts.add.mock; + const toastAddMock = coreStart.notifications.toasts.addDanger.mock; expect(toastAddMock.calls).toHaveLength(1); expect(toastAddMock.calls[0][0]).toMatchObject({ - color: 'danger', - iconType: 'alert', + title: 'Failed!', + text: expect.any(String), }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index cd63991dbac93f..d309faf59d0443 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -31,11 +31,12 @@ import { isLoading, apiError, } from '../store/policy_details/selectors'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useKibana, toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; +import { useToasts } from '../../../../common/lib/kibana'; import { AppAction } from '../../../../common/store/actions'; import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; import { PageViewHeaderTitle } from '../../../../common/components/endpoint/page_view'; @@ -51,11 +52,11 @@ import { PolicyDetailsRouteState } from '../../../../../common/endpoint/types'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); const { - notifications, services: { application: { navigateToApp }, }, } = useKibana(); + const toasts = useToasts(); const { formatUrl } = useFormatUrl(SecurityPageName.administration); const { state: locationRouteState } = useLocation(); @@ -76,15 +77,14 @@ export const PolicyDetails = React.memo(() => { useEffect(() => { if (policyUpdateStatus) { if (policyUpdateStatus.success) { - notifications.toasts.success({ - toastLifeTimeMs: 10000, + toasts.addSuccess({ title: i18n.translate( 'xpack.securitySolution.endpoint.policy.details.updateSuccessTitle', { defaultMessage: 'Success!', } ), - body: ( + text: toMountPoint( { navigateToApp(...routeState.onSaveNavigateTo); } } else { - notifications.toasts.danger({ - toastLifeTimeMs: 10000, + toasts.addDanger({ title: i18n.translate('xpack.securitySolution.endpoint.policy.details.updateErrorTitle', { defaultMessage: 'Failed!', }), - body: <>{policyUpdateStatus.error!.message}, + text: policyUpdateStatus.error!.message, }); } } - }, [navigateToApp, notifications.toasts, policyName, policyUpdateStatus, routeState]); + }, [navigateToApp, toasts, policyName, policyUpdateStatus, routeState]); const handleBackToListOnClick = useNavigateByRouterEventHandler(hostListRouterPath); 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 9ceade5d0264c4..76077831c670b4 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 84d4bf5355cd98..1698f5bc3fd0c2 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 e35c97698f5cbf..97eaceff91e9c4 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 667aacd9df3bf9..39b77d259add1b 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 @@ -36,6 +36,7 @@ import { CreateStructuredSelector } from '../../../../common/store'; import * as selectors from '../store/policy_list/selectors'; import { usePolicyListSelector } from './policy_hooks'; import { PolicyListAction } from '../store/policy_list'; +import { useToasts } from '../../../../common/lib/kibana'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { Immutable, PolicyData } from '../../../../../common/endpoint/types'; import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; @@ -124,7 +125,8 @@ const PolicyLink: React.FC<{ name: string; route: string; href: string }> = ({ const selector = (createStructuredSelector as CreateStructuredSelector)(selectors); export const PolicyList = React.memo(() => { - const { services, notifications } = useKibana(); + const { services } = useKibana(); + const toasts = useToasts(); const history = useHistory(); const location = useLocation(); const { formatUrl, search } = useFormatUrl(SecurityPageName.administration); @@ -167,13 +169,12 @@ export const PolicyList = React.memo(() => { useEffect(() => { if (apiError) { - notifications.toasts.danger({ + toasts.addDanger({ title: apiError.error, - body: apiError.message, - toastLifeTimeMs: 10000, + text: apiError.message, }); } - }, [apiError, dispatch, notifications.toasts]); + }, [apiError, dispatch, toasts]); // Handle showing update statuses useEffect(() => { @@ -181,31 +182,29 @@ export const PolicyList = React.memo(() => { if (deleteStatus === true) { setPolicyIdToDelete(''); setShowDelete(false); - notifications.toasts.success({ - toastLifeTimeMs: 10000, + toasts.addSuccess({ title: i18n.translate('xpack.securitySolution.endpoint.policyList.deleteSuccessToast', { defaultMessage: 'Success!', }), - body: ( - + text: i18n.translate( + 'xpack.securitySolution.endpoint.policyList.deleteSuccessToastDetails', + { + defaultMessage: 'Policy has been deleted.', + } ), }); } else { - notifications.toasts.danger({ - toastLifeTimeMs: 10000, + toasts.addDanger({ title: i18n.translate('xpack.securitySolution.endpoint.policyList.deleteFailedToast', { defaultMessage: 'Failed!', }), - body: i18n.translate('xpack.securitySolution.endpoint.policyList.deleteFailedToastBody', { + text: i18n.translate('xpack.securitySolution.endpoint.policyList.deleteFailedToastBody', { defaultMessage: 'Failed to delete policy', }), }); } } - }, [notifications.toasts, deleteStatus]); + }, [toasts, deleteStatus]); const paginationSetup = useMemo(() => { return { @@ -378,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 && ( @@ -450,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 30f7df940fa994..b72b9e5c73977c 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 62ffad515f4240..b5595daa9cf474 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 d2ddaae47d1e3a..188b8979f613c2 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 6bea5a7b7635ec..b64ca0ccc0b356 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 7ecbc9a53cb213..ff103aa7d2c5a7 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 cd03e43938b442..b06f0ed4f25f7c 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 3cf33048007e31..59d7fd69456377 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 00000000000000..1c0692db344c4f --- /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 00000000000000..a2636c7cd38cb5 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 00000000000000..9914031a3becb1 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 00000000000000..42b691616a6794 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 00000000000000..5a6accb2797608 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 00000000000000..f05963c0f0b677 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 9bf16dac791d74..7dd47658bc4c14 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 9e47f4eb94485b..3f941851a4143c 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/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts index 592ffb0eae62a5..34e18c5fe85fc8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts @@ -8,6 +8,7 @@ import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks import { Logger } from 'src/core/server'; import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server'; import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks'; +import { ExceptionListClient } from '../../../../../../lists/server'; import { listMock } from '../../../../../../lists/server/mocks'; import LRU from 'lru-cache'; import { getArtifactClientMock } from '../artifact_client.mock'; @@ -23,22 +24,29 @@ import { export enum ManifestManagerMockType { InitialSystemState, + ListClientPromiseRejection, NormalFlow, } export const getManifestManagerMock = (opts?: { mockType?: ManifestManagerMockType; cache?: LRU; + exceptionListClient?: ExceptionListClient; packageConfigService?: jest.Mocked; savedObjectsClient?: ReturnType; }): ManifestManager => { let cache = new LRU({ max: 10, maxAge: 1000 * 60 * 60 }); - if (opts?.cache !== undefined) { + if (opts?.cache != null) { cache = opts.cache; } + let exceptionListClient = listMock.getExceptionListClient(); + if (opts?.exceptionListClient != null) { + exceptionListClient = opts.exceptionListClient; + } + let packageConfigService = createPackageConfigServiceMock(); - if (opts?.packageConfigService !== undefined) { + if (opts?.packageConfigService != null) { packageConfigService = opts.packageConfigService; } packageConfigService.list = jest.fn().mockResolvedValue({ @@ -51,7 +59,7 @@ export const getManifestManagerMock = (opts?: { }); let savedObjectsClient = savedObjectsClientMock.create(); - if (opts?.savedObjectsClient !== undefined) { + if (opts?.savedObjectsClient != null) { savedObjectsClient = opts.savedObjectsClient; } @@ -61,6 +69,11 @@ export const getManifestManagerMock = (opts?: { switch (mockType) { case ManifestManagerMockType.InitialSystemState: return getEmptyMockArtifacts(); + case ManifestManagerMockType.ListClientPromiseRejection: + exceptionListClient.findExceptionListItem = jest + .fn() + .mockRejectedValue(new Error('unexpected thing happened')); + return super.buildExceptionListArtifacts('v1'); case ManifestManagerMockType.NormalFlow: return getMockArtifactsWithDiff(); } @@ -85,7 +98,7 @@ export const getManifestManagerMock = (opts?: { artifactClient: getArtifactClientMock(savedObjectsClient), cache, packageConfigService, - exceptionListClient: listMock.getExceptionListClient(), + exceptionListClient, logger: loggingSystemMock.create().get() as jest.Mocked, savedObjectsClient, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index d99d6a959d7aac..8e0d55104fb7c5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -9,7 +9,7 @@ import { savedObjectsClientMock } from 'src/core/server/mocks'; import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks'; import { ArtifactConstants, ManifestConstants, isCompleteArtifact } from '../../../lib/artifacts'; -import { getManifestManagerMock } from './manifest_manager.mock'; +import { getManifestManagerMock, ManifestManagerMockType } from './manifest_manager.mock'; import LRU from 'lru-cache'; describe('manifest_manager', () => { @@ -204,5 +204,14 @@ describe('manifest_manager', () => { oldArtifactId ); }); + + test('ManifestManager handles promise rejections when building artifacts', async () => { + // This test won't fail on an unhandled promise rejection, but it will cause + // an UnhandledPromiseRejectionWarning to be printed. + const manifestManager = getManifestManagerMock({ + mockType: ManifestManagerMockType.ListClientPromiseRejection, + }); + await expect(manifestManager.buildNewManifest()).rejects.toThrow(); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 217fd6de2ba689..7d700cdffc60d3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -82,18 +82,17 @@ export class ManifestManager { protected async buildExceptionListArtifacts( artifactSchemaVersion?: string ): Promise { - return ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.reduce< - Promise - >(async (acc, os) => { + const artifacts: InternalArtifactCompleteSchema[] = []; + for (const os of ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS) { const exceptionList = await getFullEndpointExceptionList( this.exceptionListClient, os, artifactSchemaVersion ?? 'v1' ); - const artifacts = await acc; const artifact = await buildArtifact(exceptionList, os, artifactSchemaVersion ?? 'v1'); - return Promise.resolve([...artifacts, artifact]); - }, Promise.resolve([])); + artifacts.push(artifact); + } + return artifacts; } /** 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 ab729bae6474d2..f4a18a40f7d4be 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 fce81e2f0dce09..58a13a7115b728 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 1e397a4e6bb6cb..ca0732816aa4d5 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/rules/patches/simplest_updated_name.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json index 56c9f151dc7129..bec88bcb0e30e7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/patches/simplest_updated_name.json @@ -1,4 +1,60 @@ { - "rule_id": "query-rule-id", - "name": "Changes only the name to this new value" + "author": [], + "actions": [], + "description": "endpoint list only", + "enabled": true, + "false_positives": [], + "filters": [], + "from": "now-360s", + "index": [ + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "interval": "5m", + "rule_id": "bf8ee47a-3f7f-4561-b2e6-92c9d618a0b2", + "language": "kuery", + "license": "", + "output_index": ".siem-signals-ytercero-default", + "max_signals": 100, + "risk_score": 50, + "risk_score_mapping": [], + "name": "endpoint list only", + "query": "host.name: * ", + "references": [], + "meta": { + "from": "1m", + "kibana_siem_app_url": "http://localhost:5601/app/security" + }, + "severity": "low", + "severity_mapping": [], + "tags": [], + "to": "now", + "type": "query", + "threat": [], + "throttle": "no_actions", + "exceptions_list": [ + { + "list_id": "endpoint_list", + "namespace_type": "agnostic", + "id": "endpoint_list", + "type": "endpoint" + }, + { + "list_id": "b27b7e13-4105-49cf-8142-cee0c61de321", + "namespace_type": "single", + "id": "8da260a0-d1bb-11ea-b248-4ba44bc54af7", + "type": "detection" + }, + { + "list_id": "b27b7e13-4105-49cf-8142-cee0c61de321", + "namespace_type": "single", + "id": "8da260a0-d1bb-11ea-b248-4ba44bc54af7", + "type": "detection" + } + ] } 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 c267b4d9f36d5e..3dd8e7f1097f4b 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 f58632c7cbbe31..335d1b8c86696c 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 b50195219f9935..6bc0ca64ae33fb 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/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index 9a7ad6fc2db743..6fadf956ccaf1e 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -6,7 +6,7 @@ import { LegacyAPICaller, CoreSetup } from '../../../../../src/core/server'; import { CollectorDependencies } from './types'; -import { DetectionsUsage, fetchDetectionsUsage } from './detections'; +import { DetectionsUsage, fetchDetectionsUsage, defaultDetectionsUsage } from './detections'; import { EndpointUsage, getEndpointTelemetryFromFleet } from './endpoints'; export type RegisterCollector = (deps: CollectorDependencies) => void; @@ -76,9 +76,14 @@ export const registerCollector: RegisterCollector = ({ isReady: () => kibanaIndex.length > 0, fetch: async (callCluster: LegacyAPICaller): Promise => { const savedObjectsClient = await getInternalSavedObjectsClient(core); + const [detections, endpoints] = await Promise.allSettled([ + fetchDetectionsUsage(kibanaIndex, callCluster, ml), + getEndpointTelemetryFromFleet(savedObjectsClient), + ]); + return { - detections: await fetchDetectionsUsage(kibanaIndex, callCluster, ml), - endpoints: await getEndpointTelemetryFromFleet(savedObjectsClient), + detections: detections.status === 'fulfilled' ? detections.value : defaultDetectionsUsage, + endpoints: endpoints.status === 'fulfilled' ? endpoints.value : {}, }; }, }); diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts index f9905c373291c4..80a9dba26df8ed 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts @@ -23,7 +23,10 @@ interface DetectionsMetric { const isElasticRule = (tags: string[]) => tags.includes(`${INTERNAL_IMMUTABLE_KEY}:true`); -const initialRulesUsage: DetectionRulesUsage = { +/** + * Default detection rule usage count + */ +export const initialRulesUsage: DetectionRulesUsage = { custom: { enabled: 0, disabled: 0, @@ -34,7 +37,10 @@ const initialRulesUsage: DetectionRulesUsage = { }, }; -const initialMlJobsUsage: MlJobsUsage = { +/** + * Default ml job usage count + */ +export const initialMlJobsUsage: MlJobsUsage = { custom: { enabled: 0, disabled: 0, diff --git a/x-pack/plugins/security_solution/server/usage/detections/index.ts b/x-pack/plugins/security_solution/server/usage/detections/index.ts index dd50e79e22cc90..a366c86299b91b 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/index.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/index.ts @@ -5,7 +5,12 @@ */ import { LegacyAPICaller } from '../../../../../../src/core/server'; -import { getMlJobsUsage, getRulesUsage } from './detections_helpers'; +import { + getMlJobsUsage, + getRulesUsage, + initialRulesUsage, + initialMlJobsUsage, +} from './detections_helpers'; import { MlPluginSetup } from '../../../../ml/server'; interface FeatureUsage { @@ -28,12 +33,23 @@ export interface DetectionsUsage { ml_jobs: MlJobsUsage; } +export const defaultDetectionsUsage = { + detection_rules: initialRulesUsage, + ml_jobs: initialMlJobsUsage, +}; + export const fetchDetectionsUsage = async ( kibanaIndex: string, callCluster: LegacyAPICaller, ml: MlPluginSetup | undefined ): Promise => { - const rulesUsage = await getRulesUsage(kibanaIndex, callCluster); - const mlJobsUsage = await getMlJobsUsage(ml); - return { detection_rules: rulesUsage, ml_jobs: mlJobsUsage }; + const [rulesUsage, mlJobsUsage] = await Promise.allSettled([ + getRulesUsage(kibanaIndex, callCluster), + getMlJobsUsage(ml), + ]); + + return { + detection_rules: rulesUsage.status === 'fulfilled' ? rulesUsage.value : initialRulesUsage, + ml_jobs: mlJobsUsage.status === 'fulfilled' ? mlJobsUsage.value : initialMlJobsUsage, + }; }; diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts index e3f0f7bde2fed5..d753eeee935949 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts @@ -15,6 +15,7 @@ import { FLEET_ENDPOINT_PACKAGE_CONSTANT } from './fleet_saved_objects'; const testAgentId = 'testAgentId'; const testConfigId = 'testConfigId'; const testHostId = 'randoHostId'; +const testHostName = 'testDesktop'; /** Mock OS Platform for endpoint telemetry */ export const MockOSPlatform = 'somePlatform'; @@ -56,8 +57,8 @@ export const mockFleetObjectsResponse = ( }, }, host: { - hostname: 'testDesktop', - name: 'testDesktop', + hostname: testHostName, + name: testHostName, id: testHostId, }, os: { @@ -93,8 +94,8 @@ export const mockFleetObjectsResponse = ( }, }, host: { - hostname: 'testDesktop', - name: 'testDesktop', + hostname: hasDuplicates ? testHostName : 'oldRandoHostName', + name: hasDuplicates ? testHostName : 'oldRandoHostName', id: hasDuplicates ? testHostId : 'oldRandoHostId', }, os: { diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts b/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts index 42c1ec0e2eed29..c46610ec9388ef 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts @@ -23,6 +23,8 @@ export const getFleetSavedObjectsMetadata = async (savedObjectsClient: ISavedObj 'last_checkin', 'local_metadata.agent.id', 'local_metadata.host.id', + 'local_metadata.host.name', + 'local_metadata.host.hostname', 'local_metadata.elastic.agent.id', 'local_metadata.os', ], diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/index.ts b/x-pack/plugins/security_solution/server/usage/endpoints/index.ts index 9e071f4adff25a..19beda4554d938 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/index.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/index.ts @@ -42,7 +42,9 @@ export interface AgentLocalMetadata extends AgentMetadata { }; }; host: { + hostname: string; id: string; + name: string; }; os: { name: string; @@ -78,17 +80,20 @@ export const updateEndpointOSTelemetry = ( os: AgentLocalMetadata['os'], osTracker: OSTracker ): OSTracker => { - const updatedOSTracker = cloneDeep(osTracker); - const { version: osVersion, platform: osPlatform, full: osFullName } = os; - if (osFullName && osVersion) { - if (updatedOSTracker[osFullName]) updatedOSTracker[osFullName].count += 1; - else { - updatedOSTracker[osFullName] = { - full_name: osFullName, - platform: osPlatform, - version: osVersion, - count: 1, - }; + let updatedOSTracker = osTracker; + if (os && typeof os === 'object') { + updatedOSTracker = cloneDeep(osTracker); + const { version: osVersion, platform: osPlatform, full: osFullName } = os; + if (osFullName && osVersion) { + if (updatedOSTracker[osFullName]) updatedOSTracker[osFullName].count += 1; + else { + updatedOSTracker[osFullName] = { + full_name: osFullName, + platform: osPlatform, + version: osVersion, + count: 1, + }; + } } } @@ -211,46 +216,53 @@ export const getEndpointTelemetryFromFleet = async ( if (!endpointAgents || endpointAgentsCount < 1) return endpointTelemetry; // Use unique hosts to prevent any potential duplicates - const uniqueHostIds: Set = new Set(); + const uniqueHosts: Set = new Set(); let osTracker: OSTracker = {}; let dailyActiveCount = 0; let policyTracker: PoliciesTelemetry = { malware: { active: 0, inactive: 0, failure: 0 } }; for (let i = 0; i < endpointAgentsCount; i += 1) { - const { attributes: metadataAttributes } = endpointAgents[i]; - const { last_checkin: lastCheckin, local_metadata: localMetadata } = metadataAttributes; - const { host, os, elastic } = localMetadata as AgentLocalMetadata; // AgentMetadata is just an empty blob, casting for our use case - - if (!uniqueHostIds.has(host.id)) { - uniqueHostIds.add(host.id); - const agentId = elastic?.agent?.id; - osTracker = updateEndpointOSTelemetry(os, osTracker); - - if (agentId) { - let agentEvents; - try { - const response = await getLatestFleetEndpointEvent(soClient, agentId); - agentEvents = response.saved_objects; - } catch (error) { - // If the request fails we do not obtain `active within last 24 hours for this agent` or policy specifics - } - - // AgentEvents will have a max length of 1 - if (agentEvents && agentEvents.length > 0) { - const latestEndpointEvent = agentEvents[0]; - dailyActiveCount = updateEndpointDailyActiveCount( - latestEndpointEvent, - lastCheckin, - dailyActiveCount + try { + const { attributes: metadataAttributes } = endpointAgents[i]; + const { last_checkin: lastCheckin, local_metadata: localMetadata } = metadataAttributes; + const { host, os, elastic } = localMetadata as AgentLocalMetadata; + + // Although not perfect, the goal is to dedupe hosts to get the most recent data for a host + // An agent re-installed on the same host will have the same id and hostname + // A cloned VM will have the same id, but "may" have the same hostname, but it's really up to the user. + const compoundUniqueId = `${host?.id}-${host?.hostname}`; + if (!uniqueHosts.has(compoundUniqueId)) { + uniqueHosts.add(compoundUniqueId); + const agentId = elastic?.agent?.id; + osTracker = updateEndpointOSTelemetry(os, osTracker); + + if (agentId) { + const { saved_objects: agentEvents } = await getLatestFleetEndpointEvent( + soClient, + agentId ); - policyTracker = updateEndpointPolicyTelemetry(latestEndpointEvent, policyTracker); + + // AgentEvents will have a max length of 1 + if (agentEvents && agentEvents.length > 0) { + const latestEndpointEvent = agentEvents[0]; + dailyActiveCount = updateEndpointDailyActiveCount( + latestEndpointEvent, + lastCheckin, + dailyActiveCount + ); + policyTracker = updateEndpointPolicyTelemetry(latestEndpointEvent, policyTracker); + } } } + } catch (error) { + // All errors thrown in the loop would be handled here + // Not logging any errors to avoid leaking any potential PII + // Depending on when the error is thrown in the loop some specifics may be missing, but it allows the loop to continue } } // All unique hosts with an endpoint installed, thus all unique endpoint installs - endpointTelemetry.total_installed = uniqueHostIds.size; + endpointTelemetry.total_installed = uniqueHosts.size; // Set the daily active count for the endpoints endpointTelemetry.active_within_last_24_hours = dailyActiveCount; // Get the objects to populate our OS Telemetry 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 b8e70125295544..688a1a409680a2 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/transform/public/app/sections/transform_management/components/action_clone/clone_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_button.tsx index 2b5ffa27e0f82a..7b371d35f8d4cb 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_button.tsx @@ -44,7 +44,7 @@ export const CloneButton: FC = ({ itemId }) => { iconType="copy" isDisabled={buttonDisabled} onClick={clickHandler} - size="s" + size="xs" > {buttonText} diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/__snapshots__/delete_button.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/__snapshots__/delete_button.test.tsx.snap index 7e98fc90cfad4e..8d4568c5ce20e3 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/__snapshots__/delete_button.test.tsx.snap +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/__snapshots__/delete_button.test.tsx.snap @@ -14,7 +14,7 @@ exports[`Transform: Transform List Actions Minimal initializati iconType="trash" isDisabled={true} onClick={[Function]} - size="s" + size="xs" > Delete diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.tsx index 2ca48ed734c7f4..dc6ddcfc45a117 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.tsx @@ -56,7 +56,7 @@ export const DeleteButton: FC = ({ items, forceDisable, onCli iconType="trash" isDisabled={buttonDisabled} onClick={() => onClick(items)} - size="s" + size="xs" > {buttonText} diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/edit_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/edit_button.tsx index 40c27cff1e398f..7bae8807425fcb 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/edit_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/edit_button.tsx @@ -36,7 +36,7 @@ export const EditButton: FC = ({ onClick }) => { iconType="pencil" isDisabled={buttonDisabled} onClick={onClick} - size="s" + size="xs" > {buttonText} diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/__snapshots__/start_button.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/__snapshots__/start_button.test.tsx.snap index d8184773e16b59..543f8f9dfcffe6 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/__snapshots__/start_button.test.tsx.snap +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/__snapshots__/start_button.test.tsx.snap @@ -14,7 +14,7 @@ exports[`Transform: Transform List Actions Minimal initializatio iconType="play" isDisabled={true} onClick={[Function]} - size="s" + size="xs" > Start diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.tsx index 60f899adc5fb2b..7f5595043b775e 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.tsx @@ -95,7 +95,7 @@ export const StartButton: FC = ({ items, forceDisable, onClick iconType="play" isDisabled={buttonDisabled} onClick={() => onClick(items)} - size="s" + size="xs" > {buttonText} diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/__snapshots__/stop_button.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/__snapshots__/stop_button.test.tsx.snap index 0052dc62547898..646162d370ac67 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/__snapshots__/stop_button.test.tsx.snap +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/__snapshots__/stop_button.test.tsx.snap @@ -14,7 +14,7 @@ exports[`Transform: Transform List Actions Minimal initialization iconType="stop" isDisabled={true} onClick={[Function]} - size="s" + size="xs" > Stop diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.tsx index 3c5e4323cc69a3..1c672193dd1eea 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.tsx @@ -68,7 +68,7 @@ export const StopButton: FC = ({ items, forceDisable }) => { iconType="stop" isDisabled={buttonDisabled} onClick={handleStop} - size="s" + size="xs" > {buttonText} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a87e92bb33d501..c81aade2b063ef 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": "または、スクリプトタグを使用してエージェントのセットアップと構成ができます。` を追加