diff --git a/.eslintrc.js b/.eslintrc.js index 3cac46e7d2605a..9657719f0f526f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -197,9 +197,12 @@ module.exports = { errorMessage: `Plugins may only import from src/core/server and src/core/public.`, }, { - target: ['(src|x-pack)/plugins/*/public/**/*'], - from: ['(src|x-pack)/plugins/*/server/**/*'], - errorMessage: `Public code can not import from server, use a common directory.`, + target: [ + '(src|x-pack)/plugins/*/server/**/*', + '!x-pack/plugins/apm/**/*', // https://github.com/elastic/kibana/issues/67210 + ], + from: ['(src|x-pack)/plugins/*/public/**/*'], + errorMessage: `Server code can not import from public, use a common directory.`, }, { target: ['(src|x-pack)/plugins/*/common/**/*'], @@ -589,8 +592,11 @@ module.exports = { * Security Solution overrides */ { - // front end typescript and javascript files only - files: ['x-pack/plugins/security_solution/public/**/*.{js,ts,tsx}'], + // front end and common typescript and javascript files only + files: [ + 'x-pack/plugins/security_solution/public/**/*.{js,ts,tsx}', + 'x-pack/plugins/security_solution/common/**/*.{js,ts,tsx}', + ], rules: { 'import/no-nodejs-modules': 'error', 'no-restricted-imports': [ @@ -766,6 +772,23 @@ module.exports = { /** * Lists overrides */ + { + // front end and common typescript and javascript files only + files: [ + 'x-pack/plugins/lists/public/**/*.{js,ts,tsx}', + 'x-pack/plugins/lists/common/**/*.{js,ts,tsx}', + ], + rules: { + 'import/no-nodejs-modules': 'error', + 'no-restricted-imports': [ + 'error', + { + // prevents UI code from importing server side code and then webpack including it when doing builds + patterns: ['**/server/*'], + }, + ], + }, + }, { // typescript and javascript for front and back end files: ['x-pack/plugins/lists/**/*.{js,ts,tsx}'], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 472d29ed29413a..07546fa54ce4f4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -222,8 +222,7 @@ /x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem /x-pack/test/api_integration/apis/endpoint/ @elastic/endpoint-app-team @elastic/siem /x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/functional_endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/functional_endpoint_ingest_failure/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/security_solution_endpoint/ @elastic/endpoint-app-team @elastic/siem /x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team @elastic/siem /x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team @elastic/siem /x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team @elastic/siem diff --git a/.gitignore b/.gitignore index b3911d0f8d0c2a..b8adcf4508db20 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /.es .DS_Store .node_binaries +.native_modules node_modules !/src/dev/npm/integration_tests/__fixtures__/fixture1/node_modules !/src/dev/notice/__fixtures__/node_modules diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc index 7411f37d3c692e..54159b642dd1a8 100644 --- a/docs/apm/api.asciidoc +++ b/docs/apm/api.asciidoc @@ -15,8 +15,39 @@ Some APM app features are provided via a REST API: [[apm-api-example]] === Using the APIs -Users interacting with APM APIs must have <>. -In addition, there are request headers to be aware of, like `kbn-xsrf: true`, and `Content-Type: applicaton/json`. +// The following content is reused throughout the API docs +// tag::using-the-APIs[] +Interact with APM APIs using cURL or another API tool. +All APM APIs are Kibana APIs, not Elasticsearch APIs; +because of this, the Kibana dev tools console cannot be used to interact with APM APIs. + +For all APM APIs, you must use a request header. +Supported headers are `Authorization`, `kbn-xsrf`, and `Content-Type`. + +`Authorization: ApiKey {credentials}`:: +Kibana supports token-based authentication with the Elasticsearch API key service. +The API key returned by the {ref}/security-api-create-api-key.html[Elasticsearch create API key API] +can be used by sending a request with an `Authorization` header that has a value of `ApiKey` followed by the `{credentials}`, +where `{credentials}` is the base64 encoding of `id` and `api_key` joined by a colon. ++ +Alternatively, you can create a user and use their username and password to authenticate API access: `-u $USER:$PASSWORD`. ++ +Whether using `Authorization: ApiKey {credentials}`, or `-u $USER:$PASSWORD`, +users interacting with APM APIs must have <>. + +`kbn-xsrf: true`:: + By default, you must use `kbn-xsrf` for all API calls, except in the following scenarios: + +* The API endpoint uses the `GET` or `HEAD` operations +* The path is whitelisted using the <> setting +* XSRF protections are disabled using the `server.xsrf.disableProtection` setting + +`Content-Type: application/json`:: + Applicable only when you send a payload in the API request. + {kib} API requests and responses use JSON. + Typically, if you include the `kbn-xsrf` header, you must also include the `Content-Type` header. +// end::using-the-APIs[] + Here's an example CURL request that adds an annotation to the APM app: [source,curl] @@ -38,9 +69,6 @@ curl -X POST \ }' ---- -The Kibana <> provides additional information on how to use Kibana APIs, -required request headers, and token-based authentication options. - //// ******************************************************* //// @@ -59,7 +87,15 @@ The following Agent configuration APIs are available: * <> to list all Agent configurations. * <> to search for an Agent configuration. -See <> for information on the privileges required to use this API endpoint. +[float] +[[use-agent-config-api]] +==== How to use APM APIs + +.Expand for required headers, privileges, and usage details +[%collapsible%closed] +====== +include::api.asciidoc[tag=using-the-APIs] +====== //// ******************************************************* @@ -100,7 +136,7 @@ See <> for information on the privileges required to [[apm-update-config-example]] ===== Example -[source,console] +[source,curl] -------------------------------------------------- PUT /api/apm/settings/agent-configuration { @@ -150,7 +186,7 @@ PUT /api/apm/settings/agent-configuration [[apm-delete-config-example]] ===== Example -[source,console] +[source,curl] -------------------------------------------------- DELETE /api/apm/settings/agent-configuration { @@ -228,7 +264,7 @@ DELETE /api/apm/settings/agent-configuration [[apm-list-config-example]] ===== Example -[source,console] +[source,curl] -------------------------------------------------- GET /api/apm/settings/agent-configuration -------------------------------------------------- @@ -293,7 +329,7 @@ GET /api/apm/settings/agent-configuration [[apm-search-config-example]] ===== Example -[source,console] +[source,curl] -------------------------------------------------- POST /api/apm/settings/agent-configuration/search { @@ -317,6 +353,9 @@ POST /api/apm/settings/agent-configuration/search The Annotation API allows you to annotate visualizations in the APM app with significant events, like deployments, allowing you to easily see how these events are impacting the performance of your existing applications. +By default, annotations are stored in a newly created `observability-annotations` index. +The name of this index can be changed in your `config.yml` by editing `xpack.observability.annotations.index`. + The following APIs are available: * <> to create an annotation for APM. @@ -324,10 +363,15 @@ The following APIs are available: // * <> GET /api/observability/annotation/:id // * <> DELETE /api/observability/annotation/:id -By default, annotations are stored in a newly created `observability-annotations` index. -The name of this index can be changed in your `config.yml` by editing `xpack.observability.annotations.index`. +[float] +[[use-annotation-api]] +==== How to use APM APIs -See <> for information on the privileges required to use this API endpoint. +.Expand for required headers, privileges, and usage details +[%collapsible%closed] +====== +include::api.asciidoc[tag=using-the-APIs] +====== //// ******************************************************* @@ -374,19 +418,20 @@ While you can add additional tags, you cannot remove the `apm` tag. The following example creates an annotation for a service named `opbeans-java`. -[source,console] +[source,curl] -------------------------------------------------- -POST /api/apm/services/opbeans-java/annotation -{ - "@timestamp": "2020-05-08T10:31:30.452Z", - "service": { - "version": "1.2" - }, - "message": "Deployment 1.2", - "tags": [ - "elastic.co", "customer" - ] -} +curl -X POST \ + http://localhost:5601/api/apm/services/opbeans-java/annotation \ +-H 'Content-Type: application/json' \ +-H 'kbn-xsrf: true' \ +-H 'Authorization: Basic YhUlubWZhM0FDbnlQeE6WRtaW49FQmSGZ4RUWXdX' \ +-d '{ + "@timestamp": "2020-05-08T10:31:30.452Z", + "service": { + "version": "1.2" + }, + "message": "Deployment 1.2" + }' -------------------------------------------------- [[apm-annotation-config-body]] diff --git a/docs/apm/deployment-annotations.asciidoc b/docs/apm/deployment-annotations.asciidoc index 142b0c0193d74c..53fd963a81f73c 100644 --- a/docs/apm/deployment-annotations.asciidoc +++ b/docs/apm/deployment-annotations.asciidoc @@ -18,7 +18,7 @@ Alternatively, you can explicitly create deployment annotations with our annotat The API can integrate into your CI/CD pipeline, so that each time you deploy, a POST request is sent to the annotation API endpoint: -[source,console] +[source,curl] ---- curl -X POST \ http://localhost:5601/api/apm/services/${SERVICE_NAME}/annotation \ <1> diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expandshorthand.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expandshorthand.md new file mode 100644 index 00000000000000..71835e6d28763c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.expandshorthand.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [expandShorthand](./kibana-plugin-plugins-data-public.expandshorthand.md) + +## expandShorthand variable + + +Signature: + +```typescript +expandShorthand: (sh: Record) => Record +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._deserialize.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._deserialize.md new file mode 100644 index 00000000000000..3e8b0abec529ce --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._deserialize.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) > [\_deserialize](./kibana-plugin-plugins-data-public.fieldmappingspec._deserialize.md) + +## FieldMappingSpec.\_deserialize property + +Signature: + +```typescript +_deserialize?: (mapping: string) => any | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._serialize.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._serialize.md new file mode 100644 index 00000000000000..d0aaf7ddd0c172 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec._serialize.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) > [\_serialize](./kibana-plugin-plugins-data-public.fieldmappingspec._serialize.md) + +## FieldMappingSpec.\_serialize property + +Signature: + +```typescript +_serialize?: (mapping: any) => string | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.md new file mode 100644 index 00000000000000..38ebe60df99a12 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) + +## FieldMappingSpec interface + + +Signature: + +```typescript +export interface FieldMappingSpec +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [\_deserialize](./kibana-plugin-plugins-data-public.fieldmappingspec._deserialize.md) | (mapping: string) => any | undefined | | +| [\_serialize](./kibana-plugin-plugins-data-public.fieldmappingspec._serialize.md) | (mapping: any) => string | undefined | | +| [type](./kibana-plugin-plugins-data-public.fieldmappingspec.type.md) | ES_FIELD_TYPES | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.type.md new file mode 100644 index 00000000000000..73cff623dc7f26 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldmappingspec.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) > [type](./kibana-plugin-plugins-data-public.fieldmappingspec.type.md) + +## FieldMappingSpec.type property + +Signature: + +```typescript +type: ES_FIELD_TYPES; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getindexpatternfieldlistcreator.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getindexpatternfieldlistcreator.md index 60302286cbd728..880acdc8956d49 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getindexpatternfieldlistcreator.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getindexpatternfieldlistcreator.md @@ -7,5 +7,5 @@ Signature: ```typescript -getIndexPatternFieldListCreator: ({ fieldFormats, toastNotifications, }: FieldListDependencies) => CreateIndexPatternFieldList +getIndexPatternFieldListCreator: ({ fieldFormats, onNotification, }: FieldListDependencies) => CreateIndexPatternFieldList ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md index 6256709e2ee368..c6359fc2688824 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `IndexPattern` class Signature: ```typescript -constructor(id: string | undefined, getConfig: any, savedObjectsClient: SavedObjectsClientContract, apiClient: IIndexPatternsApiClient, patternCache: PatternCache); +constructor(id: string | undefined, getConfig: any, savedObjectsClient: SavedObjectsClientContract, apiClient: IIndexPatternsApiClient, patternCache: PatternCache, fieldFormats: FieldFormatsStartCommon, onNotification: OnNotification, onError: OnError); ``` ## Parameters @@ -21,4 +21,7 @@ constructor(id: string | undefined, getConfig: any, savedObjectsClient: SavedObj | savedObjectsClient | SavedObjectsClientContract | | | apiClient | IIndexPatternsApiClient | | | patternCache | PatternCache | | +| fieldFormats | FieldFormatsStartCommon | | +| onNotification | OnNotification | | +| onError | OnError | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 60cbfd30e667d7..3e67b96cb80ce9 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -14,7 +14,7 @@ export declare class IndexPattern implements IIndexPattern | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(id, getConfig, savedObjectsClient, apiClient, patternCache)](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | +| [(constructor)(id, getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError)](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | ## Properties diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md index 8ee9acc684fb1c..e1e0d58ce38c10 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md @@ -9,15 +9,15 @@ Constructs a new instance of the `Field` class Signature: ```typescript -constructor(indexPattern: IndexPattern, spec: FieldSpec | Field, shortDotsEnable: boolean, { fieldFormats, toastNotifications }: FieldDependencies); +constructor(indexPattern: IIndexPattern, spec: FieldSpec | Field, shortDotsEnable: boolean, { fieldFormats, onNotification }: FieldDependencies); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| indexPattern | IndexPattern | | +| indexPattern | IIndexPattern | | | spec | FieldSpec | Field | | | shortDotsEnable | boolean | | -| { fieldFormats, toastNotifications } | FieldDependencies | | +| { fieldFormats, onNotification } | FieldDependencies | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md index d1a1ee0905c6e3..4acaaa8c0dc2cc 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md @@ -7,5 +7,5 @@ Signature: ```typescript -indexPattern?: IndexPattern; +indexPattern?: IIndexPattern; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index 1d3cfa9305c188..8fa1ee0d72e54d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -14,7 +14,7 @@ export declare class Field implements IFieldType | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(indexPattern, spec, shortDotsEnable, { fieldFormats, toastNotifications })](./kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md) | | Constructs a new instance of the Field class | +| [(constructor)(indexPattern, spec, shortDotsEnable, { fieldFormats, onNotification })](./kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md) | | Constructs a new instance of the Field class | ## Properties @@ -28,7 +28,7 @@ export declare class Field implements IFieldType | [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | | | [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) | | boolean | | | [format](./kibana-plugin-plugins-data-public.indexpatternfield.format.md) | | any | | -| [indexPattern](./kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md) | | IndexPattern | | +| [indexPattern](./kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md) | | IIndexPattern | | | [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) | | string | | | [name](./kibana-plugin-plugins-data-public.indexpatternfield.name.md) | | string | | | [readFromDocValues](./kibana-plugin-plugins-data-public.indexpatternfield.readfromdocvalues.md) | | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.mappingobject.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.mappingobject.md new file mode 100644 index 00000000000000..b1f33c8e8546d4 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.mappingobject.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [MappingObject](./kibana-plugin-plugins-data-public.mappingobject.md) + +## MappingObject type + + +Signature: + +```typescript +export declare type MappingObject = Record; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index bc1eb9100e85cf..f62479f02926e5 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -54,6 +54,7 @@ | [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) | | | [FetchOptions](./kibana-plugin-plugins-data-public.fetchoptions.md) | | | [FieldFormatConfig](./kibana-plugin-plugins-data-public.fieldformatconfig.md) | | +| [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) | | | [Filter](./kibana-plugin-plugins-data-public.filter.md) | | | [IDataPluginServices](./kibana-plugin-plugins-data-public.idatapluginservices.md) | | | [IEsSearchRequest](./kibana-plugin-plugins-data-public.iessearchrequest.md) | | @@ -101,6 +102,7 @@ | [esFilters](./kibana-plugin-plugins-data-public.esfilters.md) | | | [esKuery](./kibana-plugin-plugins-data-public.eskuery.md) | | | [esQuery](./kibana-plugin-plugins-data-public.esquery.md) | | +| [expandShorthand](./kibana-plugin-plugins-data-public.expandshorthand.md) | | | [extractSearchSourceReferences](./kibana-plugin-plugins-data-public.extractsearchsourcereferences.md) | | | [fieldFormats](./kibana-plugin-plugins-data-public.fieldformats.md) | | | [FilterBar](./kibana-plugin-plugins-data-public.filterbar.md) | | @@ -141,6 +143,7 @@ | [ISearch](./kibana-plugin-plugins-data-public.isearch.md) | | | [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | | [ISearchSource](./kibana-plugin-plugins-data-public.isearchsource.md) | \* | +| [MappingObject](./kibana-plugin-plugins-data-public.mappingobject.md) | | | [MatchAllFilter](./kibana-plugin-plugins-data-public.matchallfilter.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-public.parsedinterval.md) | | | [PhraseFilter](./kibana-plugin-plugins-data-public.phrasefilter.md) | | diff --git a/docs/discover/context.asciidoc b/docs/discover/context.asciidoc index c402a734a16fae..17ed78a1635713 100644 --- a/docs/discover/context.asciidoc +++ b/docs/discover/context.asciidoc @@ -1,5 +1,5 @@ [[document-context]] -== Viewing a document in context +== View a document in context Once you've narrowed your search to a specific event, you might want to inspect the documents that occurred diff --git a/docs/discover/document-data.asciidoc b/docs/discover/document-data.asciidoc index 477c2ec90e95cf..ee130e84054832 100644 --- a/docs/discover/document-data.asciidoc +++ b/docs/discover/document-data.asciidoc @@ -1,5 +1,5 @@ [[document-data]] -== Viewing document data +== View document data When you submit a search query in *Discover*, the most recent documents that match the query are listed in the documents table. diff --git a/docs/discover/field-filter.asciidoc b/docs/discover/field-filter.asciidoc index 49bb6078cdc588..949cab2c2f976d 100644 --- a/docs/discover/field-filter.asciidoc +++ b/docs/discover/field-filter.asciidoc @@ -1,5 +1,5 @@ [[field-filter]] -== Filtering by field +== Filter by field *Discover* offers various types of filters, so you can restrict your documents to the exact data you want. diff --git a/docs/discover/kuery.asciidoc b/docs/discover/kuery.asciidoc index 48a7c65bdbf153..1a481c46b38166 100644 --- a/docs/discover/kuery.asciidoc +++ b/docs/discover/kuery.asciidoc @@ -5,7 +5,7 @@ In Kibana 6.3, we introduced a number of exciting experimental query language en features are now available by default in 7.0. Out of the box, Kibana's query language now includes scripted field support and a simplified, easier to use syntax. If you have a Basic license or above, autocomplete functionality will also be enabled. -==== Language Syntax +==== Language syntax If you're familiar with Kibana's old Lucene query syntax, you should feel right at home with the new syntax. The basics stay the same, we've simply refined things to make the query language easier to use. @@ -72,7 +72,7 @@ set these terms will be matched against all fields. For example, a query for `re in the response field, but a query for just `200` will search for 200 across all fields in your index. ============ -==== Nested Field Support +==== Nested field support KQL supports querying on {ref}/nested.html[nested fields] through a special syntax. You can query nested fields in subtly different ways, depending on the results you want, so crafting nested queries requires extra thought. diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index 9fe35f03027604..c6e1daee9b0721 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -1,5 +1,5 @@ [[search]] -== Searching your data +== Search data Many Kibana apps embed a query bar for real-time search, including *Discover*, *Visualize*, and *Dashboard*. @@ -83,14 +83,14 @@ query language you can also submit queries using the {ref}/query-dsl.html[Elasti [[save-open-search]] -=== Saving searches +=== Save a search A saved search persists your current view of Discover for later retrieval and reuse. You can reload a saved search into Discover, add it to a dashboard, and use it as the basis for a <>. A saved search includes the query text, filters, and optionally, the time filter. A saved search also includes the selected columns in the document table, the sort order, and the current index pattern. [role="xpack"] [[discover-read-only-access]] -==== Read only access +==== Read-only access When you have insufficient privileges to save searches, the following indicator in Kibana will be displayed and the *Save* button won't be visible. For more information on granting access to Kibana see <>. @@ -117,7 +117,7 @@ selected, opening the saved search changes the selected index pattern. The query used for the saved search will also be automatically selected. [[save-load-delete-query]] -=== Saving queries +=== Save a query A saved query is a portable collection of query text and filters that you can reuse in <>, <>, and <>. Save a query when you want to: * Retrieve results from the same query at a later time without having to reenter the query text, add the filters or set the time filter @@ -127,7 +127,7 @@ A saved query is a portable collection of query text and filters that you can re Saved queries don't include information specific to Discover, such as the currently selected columns in the document table, the sort order, and the index pattern. If you want to save your current view of Discover for later retrieval and reuse, create a <> instead. [role="xpack"] -==== Read only access +==== Read-only access If you have insufficient privileges to save queries, the *Save current query* button isn't visible in the saved query management popover. For more information, see <> ==== Save a query diff --git a/docs/discover/set-time-filter.asciidoc b/docs/discover/set-time-filter.asciidoc index c53850b38a2b00..a5b81b0fa461cd 100644 --- a/docs/discover/set-time-filter.asciidoc +++ b/docs/discover/set-time-filter.asciidoc @@ -1,39 +1,39 @@ [[set-time-filter]] -== Setting the time filter -If your index contains time-based events, and a time-field is configured for the +== Set the time filter +If your index contains time-based events, and a time-field is configured for the selected <>, set a time filter that displays only the data within the specified time range. -You can use the time filter to change the time range, or select a specific time +You can use the time filter to change the time range, or select a specific time range in the histogram. [float] [[use-time-filter]] === Use the time filter -Use the time filter to change the time range. By default, the time filter is set +Use the time filter to change the time range. By default, the time filter is set to the last 15 minutes. -. Click image:images/time-filter-calendar.png[]. +. Click image:images/time-filter-calendar.png[]. . Choose one of the following: -* *Quick select* to use a recent time range, then use the back and forward +* *Quick select* to use a recent time range, then use the back and forward arrows to move through the time ranges. - -* *Commonly used* to use a time range from options such as *Last 15 minutes*, + +* *Commonly used* to use a time range from options such as *Last 15 minutes*, *Today*, and *Week to date*. - -* *Recently used date ranges* to use a previously selected data range that + +* *Recently used date ranges* to use a previously selected data range that you recently used. - + * *Refresh every* to specify an automatic refresh rate. + [role="screenshot"] image::images/Timepicker-View.png[Time filter menu] -. To set the start and end times, click the bar next to the time filter. -In the popup, select *Absolute*, *Relative* or *Now*, then specify the required +. To set the start and end times, click the bar next to the time filter. +In the popup, select *Absolute*, *Relative* or *Now*, then specify the required options. + [role="screenshot"] @@ -54,4 +54,3 @@ when you hover over a valid start point. [role="screenshot"] image::images/Histogram-Time.png[Time range selector in Histogram] - diff --git a/docs/discover/viewing-field-stats.asciidoc b/docs/discover/viewing-field-stats.asciidoc index 3631aba73fb205..5ada5839fd3449 100644 --- a/docs/discover/viewing-field-stats.asciidoc +++ b/docs/discover/viewing-field-stats.asciidoc @@ -1,5 +1,5 @@ [[viewing-field-stats]] -== Viewing Field Data Statistics +== View field data statistics From the fields list, you can see how many of the documents in the documents table contain a particular field, what the top 5 values are, and what diff --git a/docs/drilldowns/drilldowns.asciidoc b/docs/drilldowns/drilldowns.asciidoc index 2687441c99340d..e2dfaa5af39ce9 100644 --- a/docs/drilldowns/drilldowns.asciidoc +++ b/docs/drilldowns/drilldowns.asciidoc @@ -15,8 +15,8 @@ that shows a single data center or server. [[how-drilldowns-work]] === How drilldowns work -Drilldowns are {kib} actions that you configure and store -in the dashboard saved object. Drilldowns are specific to the dashboard panel +Drilldowns are user-configurable {kib} actions that are stored with the +dashboard metadata. Drilldowns are specific to the dashboard panel for which you create them—they are not shared across panels. A panel can have multiple drilldowns. diff --git a/docs/infrastructure/infra-ui.asciidoc b/docs/infrastructure/infra-ui.asciidoc index 96550b4ed57585..9e7459da743a47 100644 --- a/docs/infrastructure/infra-ui.asciidoc +++ b/docs/infrastructure/infra-ui.asciidoc @@ -103,7 +103,7 @@ You can: * Select *View Metrics* to <>. -* Select *View Logs* to <> in the *Logs* app. +* Select *View Logs* to {logs-guide}/inspect-log-events.html[view the logs] in the *Logs* app. Depending on the features you have installed and configured, you may also be able to: diff --git a/docs/ingest_manager/images/ingest-manager-start.png b/docs/ingest_manager/images/ingest-manager-start.png new file mode 100644 index 00000000000000..89174686a9768e Binary files /dev/null and b/docs/ingest_manager/images/ingest-manager-start.png differ diff --git a/docs/ingest_manager/index-templates.asciidoc b/docs/ingest_manager/index-templates.asciidoc deleted file mode 100644 index e19af63c3116f1..00000000000000 --- a/docs/ingest_manager/index-templates.asciidoc +++ /dev/null @@ -1,7 +0,0 @@ -# Elasticsearch Index Templates - -## Generation - -* Index templates are generated from `YAML` files contained in the package. -* There is one index template per dataset. -* For the generation of an index template, all `yml` files contained in the package subdirectory `dataset/DATASET_NAME/fields/` are used. diff --git a/docs/ingest_manager/index.asciidoc b/docs/ingest_manager/index.asciidoc index 1728309f3dfd90..f719c774c7739e 100644 --- a/docs/ingest_manager/index.asciidoc +++ b/docs/ingest_manager/index.asciidoc @@ -1,208 +1,14 @@ +[chapter] [role="xpack"] -[[epm]] -== Ingest Manager +[[xpack-ingest-manager]] += Ingest Manager -These are the docs for the Ingest Manager. +The {ingest-manager} app in Kibana enables you to add and manage integrations for popular services and platforms, as well as manage {elastic-agent} installations in standalone or {fleet} mode. +[role="screenshot"] +image::ingest_manager/images/ingest-manager-start.png[Ingest Manager App in Kibana] -=== Configuration +[float] +=== Get started -The Elastic Package Manager by default access `epr.elastic.co` to retrieve the package. The url can be configured with: - -``` -xpack.epm.registryUrl: 'http://localhost:8080' -``` - -=== API - -The Package Manager offers an API. Here an example on how they can be used. - -List installed packages: - -``` -curl localhost:5601/api/ingest_manager/epm/packages -``` - -Install a package: - -``` -curl -X POST localhost:5601/api/ingest_manager/epm/packages/iptables-1.0.4 -``` - -Delete a package: - -``` -curl -X DELETE localhost:5601/api/ingest_manager/epm/packages/iptables-1.0.4 -``` - -=== Definitions - -This section is to define terms used across ingest management. - -==== Data Source - -A data source is a definition on how to collect data from a service, for example `nginx`. A data source contains -definitions for one or multiple inputs and each input can contain one or multiple streams. - -With the example of the nginx Data Source, it contains to inputs: `logs` and `nginx/metrics`. Logs and metrics are collected -differently. The `logs` input contains two streams, `access` and `error`, the `nginx/metrics` input contains the stubstatus stream. - - -==== Data Stream - -Data Streams are a [new concept](https://github.com/elastic/elasticsearch/issues/53100) in Elasticsearch which simplify -ingesting data and the setup of Elasticsearch. - -==== Elastic Agent - -A single, unified agent that users can deploy to hosts or containers. It controls which data is collected from the host or containers and where the data is sent. It will run Beats, Endpoint or other monitoring programs as needed. It can operate standalone or pull a configuration policy from Fleet. - - -==== Elastic Package Registry - -The Elastic Package Registry (EPR) is a service which runs under [https://epr.elastic.co]. It serves the packages through its API. -More details about the registry can be found [here](https://github.com/elastic/package-registry). - -==== Fleet - -Fleet is the part of the Ingest Manager UI in Kibana that handles the part of enrolling Elastic Agents, -managing agents and sending configurations to the Elastic Agent. - -==== Indexing Strategy - -Ingest Management + Elastic Agent follow a strict new indexing strategy: `{type}-{dataset}-{namespace}`. An example -for this is `logs-nginx.access-default`. More details about it can be found in the Index Strategy below. All data of -the index strategy is sent to Data Streams. - -==== Input - -An input is the configuration unit in an Agent Config that defines the options on how to collect data from -an endpoint. This could be username / password which are need to authenticate with a service or a host url -as an example. - -An input is part of a Data Source and contains streams. - -==== Integration - -An integration is a package with the type integration. An integration package has at least 1 data source -and usually collects data from / about a service. - - -==== Namespace - -A user-specified string that will be used to part of the index name in Elasticsearch. It helps users identify logs coming from a specific environment (like prod or test), an application, or other identifiers. - - -==== Package - -A package contains all the assets for the Elastic Stack. A more detailed definition of a -package can be found under https://github.com/elastic/package-registry. - -Besides the assets, a package contains the data source definitions with its inputs and streams. - -==== Stream - -A stream is a configuration unit in the Elastic Agent config. A stream is part of an input and defines how the data -fetched by this input should be processed and which Data Stream to send it to. - -== Indexing Strategy - -Ingest Management enforces an indexing strategy to allow the system to automatically detect indices and run queries on it. In short the indexing strategy looks as following: - -``` -{dataset.type}-{dataset.name}-{dataset.namespace} -``` - -The `{dataset.type}` can be `logs` or `metrics`. The `{dataset.namespace}` is the part where the user can use free form. The only two requirement are that it has only characters allowed in an Elasticsearch index name and does NOT contain a `-`. The `dataset` is defined by the data that is indexed. The same requirements as for the namespace apply. It is expected that the fields for type, namespace and dataset are part of each event and are constant keywords. If there is a dataset or a namespace with a `-` inside, it is recommended to replace it either by a `.` or a `_`. - -Note: More `{dataset.type}`s might be added in the future like `traces`. - -This indexing strategy has a few advantages: - -* Each index contains only the fields which are relevant for the dataset. This leads to more dense indices and better field completion. -* ILM policies can be applied per namespace per dataset. -* Rollups can be specified per namespace per dataset. -* Having the namespace user configurable makes setting security permissions possible. -* Having a global metrics and logs template, allows to create new indices on demand which still follow the convention. This is common in the case of k8s as an example. -* Constant keywords allow to narrow down the indices we need to access for querying very efficiently. This is especially relevant in environments which a large number of indices or with indices on slower nodes. - -Overall it creates smaller indices in size, makes querying more efficient and allows users to define their own naming parts in namespace and still benefiting from all features that can be built on top of the indexing startegy. - -=== Ingest Pipeline - -The ingest pipelines for a specific dataset will have the following naming scheme: - -``` -{dataset.type}-{dataset.name}-{package.version} -``` - -As an example, the ingest pipeline for the Nginx access logs is called `logs-nginx.access-3.4.1`. The same ingest pipeline is used for all namespaces. It is possible that a dataset has multiple ingest pipelines in which case a suffix is added to the name. - -The version is included in each pipeline to allow upgrades. The pipeline itself is listed in the index template and is automatically applied at ingest time. - -=== Templates & ILM Policies - -To make the above strategy possible, alias templates are required. For each type there is a basic alias template with a default ILM policy. These default templates apply to all indices which follow the indexing strategy and do not have a more specific dataset alias template. - -The `metrics` and `logs` alias template contain all the basic fields from ECS. - -Each type template contains an ILM policy. Modifying this default ILM policy will affect all data covered by the default templates. - -The templates for a dataset are called as following: - -``` -{dataset.type}-{dataset.name} -``` - -The pattern used inside the index template is `{type}-{dataset}-*` to match all namespaces. - -=== Defaults - -If the Elastic Agent is used to ingest data and only the type is specified, `default` for the namespace is used and `generic` for the dataset. - -=== Data filtering - -Filtering for data in queries for example in visualizations or dashboards should always be done on the constant keyword fields. Visualizations needing data for the nginx.access dataset should query on `type:logs AND dataset:nginx.access`. As these are constant keywords the prefiltering is very efficient. - -=== Security permissions - -Security permissions can be set on different levels. To set special permissions for the access on the prod namespace, use the following index pattern: - -``` -/(logs|metrics)-[^-]+-prod-$/ -``` - -To set specific permissions on the logs index, the following can be used: - -``` -/^(logs|metrics)-.*/ -``` - -Todo: The above queries need to be tested. - - - -== Package Manager - -=== Package Upgrades - -When upgrading a package between a bugfix or a minor version, no breaking changes should happen. Upgrading a package has the following effect: - -* Removal of existing dashboards -* Installation of new dashboards -* Write new ingest pipelines with the version -* Write new Elasticsearch alias templates -* Trigger a rollover for all the affected indices - -The new ingest pipeline is expected to still work with the data coming from older configurations. In most cases this means some of the fields can be missing. For this to work, each event must contain the version of config / package it is coming from to make such a decision. - -In case of a breaking change in the data structure, the new ingest pipeline is also expected to deal with this change. In case there are breaking changes which cannot be dealt with in an ingest pipeline, a new package has to be created. - -Each package lists its minimal required agent version. In case there are agents enrolled with an older version, the user is notified to upgrade these agents as otherwise the new configs cannot be rolled out. - -=== Generated assets - -When a package is installed or upgraded, certain Kibana and Elasticsearch assets are generated from . These follow the naming conventions explained above (see "indexing strategy") and contain configuration for the elastic stack that makes ingesting and displaying data work with as little user interaction as possible. - -* link:index-templates.asciidoc[Elasticsearch Index Templates] -* Kibana Index Patterns +To get started with Ingest Management, refer to the LINK_TO_INGEST_MANAGEMENT_GUIDE[Ingest Management Guide]. \ No newline at end of file diff --git a/docs/logs/configuring.asciidoc b/docs/logs/configuring.asciidoc deleted file mode 100644 index 6b54721f92e899..00000000000000 --- a/docs/logs/configuring.asciidoc +++ /dev/null @@ -1,46 +0,0 @@ -[role="xpack"] -[[xpack-logs-configuring]] - -:ecs-base-link: {ecs-ref}/ecs-base.html[base] - -== Configuring the Logs data - -The default source configuration for logs is specified in the {kibana-ref}/logs-ui-settings-kb.html[Logs app settings] in the {kibana-ref}/settings.html[Kibana configuration file]. -The default configuration uses the `filebeat-*` index pattern to query the data. -The default configuration also defines field settings for things like timestamps and container names, and the default columns to show in the logs stream. - -If your logs have custom index patterns, use non-default field settings, or contain parsed fields which you want to expose as individual columns, you can override the default configuration settings. - -To change the configuration settings, click the *Settings* tab. - -NOTE: These settings are shared with metrics. Changes you make here may also affect the settings used by the *Metrics* app. - -In the *Settings* tab, you can change the values in these sections: - -* *Name*: the name of the source configuration -* *Indices*: the index pattern or patterns in the Elasticsearch indices to read metrics data and log data from -* *Fields*: the names of specific fields in the indices that are used to query and interpret the data correctly -* *Log columns*: the columns that are shown in the logs stream - -By default the logs stream shows following columns: - -* *Timestamp*: The timestamp of the log entry from the `timestamp` field. -* *Message*: The message extracted from the document. -The content of this field depends on the type of log message. -If no special log message type is detected, the Elastic Common Schema (ECS) {ecs-base-link} field, `message`, is used. - -To add a new column to the logs stream, in the *Settings* tab, click *Add column*. -In the list of available fields, select the field you want to add. -You can start typing a field name in the search box to filter the field list by that name. - -To remove an existing column, click the *Remove this column* icon -image:logs/images/logs-configure-source-dialog-remove-column-button.png[Remove column]. - -When you have completed your changes, click *Apply*. - -If the fields are greyed out and cannot be edited, you may not have sufficient privileges to change the source configuration. -For more information see <>. - -TIP: If <> are enabled in your Kibana instance, any configuration changes you make here are specific to the current space. -You can make different subsets of data available by creating multiple spaces with different data source configurations. - diff --git a/docs/logs/getting-started.asciidoc b/docs/logs/getting-started.asciidoc deleted file mode 100644 index ca09bb34c0e56a..00000000000000 --- a/docs/logs/getting-started.asciidoc +++ /dev/null @@ -1,11 +0,0 @@ -[role="xpack"] -[[xpack-logs-getting-started]] -== Getting started with logs monitoring - -To get started with the Logs app in Kibana, you need to start collecting logs data for your infrastructure. - -Kibana provides step-by-step instructions to help you add logs data. -The {logs-guide}[Logs Monitoring Guide] is a good source for more detailed information and instructions. - -[role="screenshot"] -image::logs/images/logs-add-data.png[Screenshot showing Add logging data in Kibana] diff --git a/docs/logs/images/alert-actions-menu.png b/docs/logs/images/alert-actions-menu.png deleted file mode 100644 index 3f96a700a0ac12..00000000000000 Binary files a/docs/logs/images/alert-actions-menu.png and /dev/null differ diff --git a/docs/logs/images/alert-flyout.png b/docs/logs/images/alert-flyout.png deleted file mode 100644 index 30c8857758a8b4..00000000000000 Binary files a/docs/logs/images/alert-flyout.png and /dev/null differ diff --git a/docs/logs/images/analysis-tab-create-ml-job.png b/docs/logs/images/analysis-tab-create-ml-job.png deleted file mode 100644 index 0f4115bb93f4c7..00000000000000 Binary files a/docs/logs/images/analysis-tab-create-ml-job.png and /dev/null differ diff --git a/docs/logs/images/log-rate-anomalies.png b/docs/logs/images/log-rate-anomalies.png deleted file mode 100644 index 74ce8d682e1cc8..00000000000000 Binary files a/docs/logs/images/log-rate-anomalies.png and /dev/null differ diff --git a/docs/logs/images/log-rate-entries.png b/docs/logs/images/log-rate-entries.png deleted file mode 100644 index efa693a2ac5292..00000000000000 Binary files a/docs/logs/images/log-rate-entries.png and /dev/null differ diff --git a/docs/logs/images/log-time-filter.png b/docs/logs/images/log-time-filter.png deleted file mode 100644 index ffba6f972aeb77..00000000000000 Binary files a/docs/logs/images/log-time-filter.png and /dev/null differ diff --git a/docs/logs/images/logs-action-menu.png b/docs/logs/images/logs-action-menu.png deleted file mode 100644 index f1c79b6fa88d14..00000000000000 Binary files a/docs/logs/images/logs-action-menu.png and /dev/null differ diff --git a/docs/logs/images/logs-add-data.png b/docs/logs/images/logs-add-data.png deleted file mode 100644 index 176c71466aa385..00000000000000 Binary files a/docs/logs/images/logs-add-data.png and /dev/null differ diff --git a/docs/logs/images/logs-configure-source-dialog-remove-column-button.png b/docs/logs/images/logs-configure-source-dialog-remove-column-button.png deleted file mode 100644 index 995b7ac1f538d1..00000000000000 Binary files a/docs/logs/images/logs-configure-source-dialog-remove-column-button.png and /dev/null differ diff --git a/docs/logs/images/logs-time-selector.png b/docs/logs/images/logs-time-selector.png deleted file mode 100644 index 5e6a9b7222c541..00000000000000 Binary files a/docs/logs/images/logs-time-selector.png and /dev/null differ diff --git a/docs/logs/images/logs-view-event-with-filter.png b/docs/logs/images/logs-view-event-with-filter.png deleted file mode 100644 index 4e378af39ab057..00000000000000 Binary files a/docs/logs/images/logs-view-event-with-filter.png and /dev/null differ diff --git a/docs/logs/images/logs-view-event.png b/docs/logs/images/logs-view-event.png deleted file mode 100644 index 29dff68e3fdba9..00000000000000 Binary files a/docs/logs/images/logs-view-event.png and /dev/null differ diff --git a/docs/logs/images/logs-view-in-context.png b/docs/logs/images/logs-view-in-context.png deleted file mode 100644 index 09a9e89fc30429..00000000000000 Binary files a/docs/logs/images/logs-view-in-context.png and /dev/null differ diff --git a/docs/logs/index.asciidoc b/docs/logs/index.asciidoc index 0d225e5e89c17e..45d4321f40556c 100644 --- a/docs/logs/index.asciidoc +++ b/docs/logs/index.asciidoc @@ -1,9 +1,8 @@ +[chapter] [role="xpack"] [[xpack-logs]] = Logs -[partintro] --- The Logs app in Kibana enables you to explore logs for common servers, containers, and services. The Logs app has a compact, console-like display that you can customize. @@ -13,23 +12,10 @@ You can open the Logs app from the *Logs* tab in Kibana. You can also open the Logs app directly from a component in the Metrics app. In this case, you will only see the logs for the selected component. -* <> -* <> -* <> -* <> -* <> - [role="screenshot"] -image::logs/images/logs-console.png[Log Console in Kibana] - --- - -include::getting-started.asciidoc[] - -include::using.asciidoc[] - -include::configuring.asciidoc[] +image::logs/images/logs-console.png[Logs Console in Kibana] -include::log-rate.asciidoc[] +[float] +=== Get started -include::logs-alerting.asciidoc[] +To get started with Elastic Logs, refer to {logs-guide}/install-logs-monitoring.html[Install Logs]. diff --git a/docs/logs/log-rate.asciidoc b/docs/logs/log-rate.asciidoc deleted file mode 100644 index 56284a1c76219c..00000000000000 --- a/docs/logs/log-rate.asciidoc +++ /dev/null @@ -1,94 +0,0 @@ -[role="xpack"] -[[xpack-logs-analysis]] -== Detecting and inspecting log anomalies - -beta::[] - -When the {ml} {anomaly-detect} features are enabled, -you can use the **Log rate** page in the Logs app. -**Log rate** helps you to detect and inspect log anomalies and the log partitions where the log anomalies occur. -This means you can easily spot anomalous behavior without significant human intervention -- -no more manually sampling log data, calculating rates, and determining if rates are normal. - -*Log rate* automatically highlights periods of time where the log rate is outside expected bounds, -and therefore may be anomalous. -You can use this information as a basis for further investigations. -For example: - -* A significant drop in the log rate might suggest that a piece of infrastructure stopped responding, -and thus we're serving less requests. -* A spike in the log rate could denote a DDoS attack. -This may lead to an investigation of IP addresses from incoming requests. - -You can also view log anomalies directly in the <>. - -[float] -[[logs-analysis-create-ml-job]] -=== Enable log rate analysis and anomaly detection - -Create a machine learning job to enable log rate analysis and anomaly detection. - -[role="screenshot"] -image::logs/images/analysis-tab-create-ml-job.png[Create machine learning job] - -1. To enable log rate analysis and anomaly detection, -you must first create your own {kibana-ref}/xpack-spaces.html[space]. -2. Within a space, navigate to the Logs app and select *Log rate*. -Here, you'll be prompted to create a machine learning job which will carry out the log rate analysis. -3. Choose a time range for the machine learning analysis. -4. Add the Indices that contain the logs you want to analyze. -5. Click *Create ML job*. -6. You're now ready to analyze your log partitions. - -Even though the machine learning job's time range is fixed, -you can still use the time filter to adjust the results that are shown in your analysis. - -[role="screenshot"] -image::logs/images/log-time-filter.png[Log rate time filter] - -[float] -[[logs-analysis-entries-chart]] -=== Log entries chart - -The log entries chart shows an overall, color-coded visualization of the log entry rate, -partitioned according to the value of the Elastic Common Schema (ECS) -{ecs-ref}/ecs-event.html[`event.dataset`] field. -This chart helps you quickly spot increases or decreases in each partition's log rate. - -[role="screenshot"] -image::logs/images/log-rate-entries.png[Log rate entries chart] - -If you have a lot of log partitions, use the following to filter your data: - -* Hover over a time range to see the log rate for each partition. -* Click or hover on a partition name to show, hide, or highlight the partition values. - -[float] -[[logs-analysis-anomalies-chart]] -=== Anomalies charts - -The Anomalies chart shows the time range where anomalies were detected. -The typical rate values are shown in grey, while the anomalous regions are color-coded and superimposed on top. - -[role="screenshot"] -image::logs/images/log-rate-anomalies.png[Log rate entries chart] - -When a time range is flagged as anomalous, -the machine learning algorithms have detected unusual log rate activity. -This might be because: - -* The log rate is significantly higher than usual. -* The log rate is significantly lower than usual. -* Other anomalous behavior has been detected. -For example, the log rate is within bounds, but not fluctuating when it is expected to. - -The level of anomaly detected in a time period is color-coded, from red, orange, yellow, to blue. -Red indicates a critical anomaly level, while blue is a warning level. - -To help you further drill down into a potential anomaly, -you can view an anomaly chart for each individual partition: - -Anomaly scores range from 0 (no anomalies) to 100 (critical). - -To analyze the anomalies in more detail, click *Analyze in ML*, which opens the -{kibana-ref}/xpack-ml.html[Anomaly Explorer in Machine Learning]. diff --git a/docs/logs/logs-alerting.asciidoc b/docs/logs/logs-alerting.asciidoc deleted file mode 100644 index f08a09187a0c81..00000000000000 --- a/docs/logs/logs-alerting.asciidoc +++ /dev/null @@ -1,27 +0,0 @@ -[role="xpack"] -[[xpack-logs-alerting]] -== Logs alerting - -[float] -=== Overview - -To use the alerting functionality you need to {kibana-ref}/alerting-getting-started.html#alerting-setup-prerequisites[set up alerting]. - -You can then select the *Create alert* option, from the *Alerts* actions dropdown. - -[role="screenshot"] -image::logs/images/alert-actions-menu.png[Screenshot showing alerts menu] - -Within the alert flyout you can configure your logs alert: - -[role="screenshot"] -image::logs/images/alert-flyout.png[Screenshot showing alerts flyout] - -[float] -=== Fields and comparators - -The comparators available for conditions depend on the chosen field. The combinations available are: - -- Numeric fields: *more than*, *more than or equals*, *less than*, *less than or equals*, *equals*, and *does not equal*. -- Aggregatable fields: *is* and *is not*. -- Non-aggregatable fields: *matches*, *does not match*, *matches phrase*, *does not match phrase*. diff --git a/docs/logs/using.asciidoc b/docs/logs/using.asciidoc deleted file mode 100644 index eb3025f88ce1b4..00000000000000 --- a/docs/logs/using.asciidoc +++ /dev/null @@ -1,100 +0,0 @@ -[role="xpack"] -[[xpack-logs-using]] -== Using the Logs app -Use the Logs app in {kib} to explore and filter your logs in real time. - -You can customize the output to focus on the data you want to see and to control how you see it. -You can also view related application traces or uptime information where available. - -[role="screenshot"] -image::logs/images/logs-console.png[Logs Console in Kibana] - -[float] -[[logs-search]] -=== Use the power of search - -Use the search bar to perform ad hoc searches for specific text. -You can also create structured queries using {kibana-ref}/kuery-query.html[Kibana Query Language]. -For example, enter `host.hostname : "host1"` to see only the information for `host1`. -// ++ this isn't quite the same as the corresponding metrics description now. - -[float] -[[logs-configure-source]] -=== Configure the data to use for your logs -Are you using a custom index pattern to store the log entries? -Do you want to limit the entries shown or change the fields displayed in the columns? -If so, <> to change the index pattern and other settings. - -[float] -[[logs-time]] -=== Specify the time and date - -Click image:images/time-filter-calendar.png[time filter calendar], then choose the time range for the logs. - -Log entries for the specified time appear in the middle of the page. To quickly jump to a nearby point in time, click the minimap timeline to the right. -// ++ what's this thing called? It's minimap in the UI. Would timeline be better? - -[float] -[[logs-customize]] -=== Customize your view -Click *Customize* to customize the view. -Here, you can choose whether to wrap long lines, and choose your preferred text size. - -[float] -=== Configuring the data to use for your logs - -If your logs have custom index patterns, use non-default field settings, or contain parsed fields which you want to expose as individual columns, you can <>. - -[float] -[[logs-stream]] -=== Stream or pause logs -Click *Stream live* to start streaming live log data, or click *Stop streaming* to focus on historical data. - -When you are viewing historical data, you can scroll back through the entries as far as there is data available. - -When you are streaming live data, the most recent log appears at the bottom of the page. -In live streaming mode, you are not able to choose a different time in the time selector or use the minimap timeline. -To do either of these things, you need to stop live streaming first. -// ++ Not sure whether this is correct or not. And what about just scrolling through the display? -// ++ There may be a bug here, (I managed to get future logs) see https://github.com/elastic/kibana/issues/43361 - -[float] -[[logs-highlight]] -=== Highlight a phrase in the logs stream -To highlight a word or phrase in the logs stream, click *Highlights* and enter your search phrase. -// ++ Is search case sensitive? -// ++ Can you search for multiple phrases together, if so, what's the separator? -// ++ What about special characters? For example, I notice that when searching for "Mozilla/4.0" which appears as written in my logs, "Mozilla" is highlighted, as is "4.0" but "/" isn't. The string "-" (which appears in the logs as written, quotes and all, isn't found at all. Any significance? - -[float] -[[logs-event-inspector]] -=== Inspect a log event -To inspect a log event, hover over it, then click the *View actions for line* icon image:logs/images/logs-action-menu.png[View actions for line icon]. On the menu that opens, select *View details*. This opens the *Log event document details* fly-out that shows the fields associated with the log event. - -To quickly filter the logs stream by one of the field values, in the log event details, click the *View event with filter* icon image:logs/images/logs-view-event-with-filter.png[View event icon] beside the field. -This automatically adds a search filter to the logs stream to filter the entries by this field and value. - -[float] -[[log-view-in-context]] -=== View log line in context -To view a certain line in its context (for example, with other log lines from the same file, or the same cloud container), hover over it, then click the *View actions for line* image:logs/images/logs-action-menu.png[View actions for line icon]. On the menu that opens, select *View in context*. This opens the *View log in context* modal, that shows the log line in its context. - -[role="screenshot"] -image::logs/images/logs-view-in-context.png[View a log line in context] - -[float] -[[view-log-anomalies]] -=== View log anomalies - -When the machine learning anomaly detection features are enabled, click *Log rate*, which allows you to -<> in your log data. - -[float] -[[logs-integrations]] -=== Logs app integrations - -To see other actions related to the event, click *Actions* in the log event details. -Depending on the event and the features you have configured, you may also be able to: - -* Select *View status in Uptime* to {uptime-guide}/uptime-app-overview.html[view related uptime information] in the *Uptime* app. -* Select *View in APM* to <> in the *APM* app. diff --git a/docs/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc index bb16faab7fe5a1..37980e2b15c6b0 100644 --- a/docs/management/index-patterns.asciidoc +++ b/docs/management/index-patterns.asciidoc @@ -1,5 +1,5 @@ [[index-patterns]] -== Creating an index pattern +== Create an index pattern To explore and visualize data in {kib}, you must create an index pattern. An index pattern tells {kib} which {es} indices contain the data that diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 85d580de9475f9..04959b2627a785 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -33,7 +33,7 @@ This page has moved. Please see <>. [role="exclude",id="logs-read-only-access"] == Configure source read-only access -This page has moved. Please see <>. +This page has moved. Please see {logs-guide}/configure-logs-source.html[logs configuration]. [role="exclude",id="extend"] == Extend your use case @@ -80,3 +80,28 @@ This page was deleted. See <>. == Developing Visualizations This page was deleted. See <>. + +[role="exclude",id="xpack-logs-getting-started"] +== Getting started with logs monitoring + +This page has moved. Please see the new section in the {logs-guide}/install-logs-monitoring.html[Logs Monitoring Guide]. + +[role="exclude",id="xpack-logs-using"] +== Using the Logs app + +This page has moved. Please see the new section in the {logs-guide}/logs-app-overview.html[Logs Monitoring Guide]. + +[role="exclude",id="xpack-logs-configuring"] +== Configuring the Logs data + +This page has moved. Please see the new section in the {logs-guide}/configure-logs-source.html[Logs Monitoring Guide]. + +[role="exclude",id="xpack-logs-analysis"] +== Detecting and inspecting log anomalies + +This page has moved. Please see the new section in the {logs-guide}/detect-log-anomalies.html[Logs Monitoring Guide] + +[role="exclude",id="xpack-logs-alerting"] +== Logs alerting + +This page has moved. Please see the new section in the {logs-guide}/create-log-alert.html[Logs Monitoring Guide] diff --git a/docs/settings/ingest-manager-settings.asciidoc b/docs/settings/ingest-manager-settings.asciidoc new file mode 100644 index 00000000000000..0c3427b034ae2f --- /dev/null +++ b/docs/settings/ingest-manager-settings.asciidoc @@ -0,0 +1,52 @@ +[role="xpack"] +[[ingest-manager-settings-kb]] +=== {ingest-manager} settings in {kib} +++++ +{ingest-manager} settings +++++ + +experimental[] + +You can configure `xpack.ingestManager` settings in your `kibana.yml`. +By default, {ingest-manager} is not enabled. You need to enable it. To use +{fleet}, you also need to configure {kib} and {es} hosts. + +[[general-ingest-manager-settings-kb]] +==== General {ingest-manager} settings + +[cols="2*<"] +|=== +| `xpack.ingestManager.enabled` {ess-icon} + | Set to `true` to enable {ingest-manager}. +| `xpack.ingestManager.epm.enabled` {ess-icon} + | Set to `true` (default) to enable {package-manager}. +| `xpack.ingestManager.fleet.enabled` {ess-icon} + | Set to `true` (default) to enable {fleet}. +|=== + +[[ingest-manager-data-visualizer-settings]] + +==== {package-manager} settings + +[cols="2*<"] +|=== +| `xpack.ingestManager.epm.registryUrl` + | The address to use to reach {package-manager} registry. +|=== + +==== {fleet} settings + +[cols="2*<"] +|=== +| `xpack.ingestManager.fleet.kibana.host` + | The hostname used by {agent} for accessing {kib}. +| `xpack.ingestManager.fleet.elasticsearch.host` + | The hostname used by {agent} for accessing {es}. +| `xpack.ingestManager.fleet.tlsCheckDisabled` + | Set to `true` to allow {fleet} to run on a {kib} instance without TLS enabled. +|=== + +[NOTE] +==== +In {ecloud}, {fleet} flags are already configured. +==== diff --git a/docs/settings/settings-xkb.asciidoc b/docs/settings/settings-xkb.asciidoc index 4d594b4113e69f..04fed0d6204b7b 100644 --- a/docs/settings/settings-xkb.asciidoc +++ b/docs/settings/settings-xkb.asciidoc @@ -20,3 +20,4 @@ include::ml-settings.asciidoc[] include::reporting-settings.asciidoc[] include::spaces-settings.asciidoc[] include::i18n-settings.asciidoc[] +include::ingest-manager-settings.asciidoc[] diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index fb4cbbada9a333..1a0b13edf80860 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -1,5 +1,9 @@ [[docker]] -=== Install Kibana with Docker +=== Install {kib} with Docker +++++ +Install with Docker +++++ + Docker images for Kibana are available from the Elastic Docker registry. The base image is https://hub.docker.com/_/centos/[centos:7]. diff --git a/docs/setup/install/brew.asciidoc b/docs/setup/install/brew.asciidoc index 3fe104bd047941..07d740361d5056 100644 --- a/docs/setup/install/brew.asciidoc +++ b/docs/setup/install/brew.asciidoc @@ -1,5 +1,8 @@ [[brew]] === Install {kib} on macOS with Homebrew +++++ +Install on macOS with Homebrew +++++ Elastic publishes Homebrew formulae so you can install {kib} with the https://brew.sh/[Homebrew] package manager. @@ -44,7 +47,7 @@ and data directory are stored in the following locations. | conf | Configuration files including `kibana.yml` | /usr/local/etc/kibana - d| + d| | data | The location of the data files of each index / shard allocated diff --git a/docs/setup/install/deb.asciidoc b/docs/setup/install/deb.asciidoc index 8193a088c8b7e7..dfa1e3a37fd050 100644 --- a/docs/setup/install/deb.asciidoc +++ b/docs/setup/install/deb.asciidoc @@ -1,5 +1,8 @@ [[deb]] === Install {kib} with Debian package +++++ +Install with Debian package +++++ The Debian package for Kibana can be <> or from our <>. It can be used to install diff --git a/docs/setup/install/rpm.asciidoc b/docs/setup/install/rpm.asciidoc index c3922ffba1efae..ccc38c26961581 100644 --- a/docs/setup/install/rpm.asciidoc +++ b/docs/setup/install/rpm.asciidoc @@ -1,5 +1,9 @@ [[rpm]] === Install {kib} with RPM +++++ +Install with RPM +++++ + The RPM for Kibana can be <> or from our <>. It can be used to install diff --git a/docs/setup/install/targz.asciidoc b/docs/setup/install/targz.asciidoc index a53360900657e0..c8bff5d58889dc 100644 --- a/docs/setup/install/targz.asciidoc +++ b/docs/setup/install/targz.asciidoc @@ -1,5 +1,8 @@ [[targz]] === Install {kib} from archive on Linux or macOS +++++ +Install from archive on Linux or macOS +++++ Kibana is provided for Linux and Darwin as a `.tar.gz` package. These packages are the easiest formats to use when trying out Kibana. diff --git a/docs/setup/install/windows.asciidoc b/docs/setup/install/windows.asciidoc index 1cfbaaa0eda2fb..24bf74f607fef5 100644 --- a/docs/setup/install/windows.asciidoc +++ b/docs/setup/install/windows.asciidoc @@ -1,5 +1,8 @@ [[windows]] === Install {kib} on Windows +++++ +Install on Windows +++++ Kibana can be installed on Windows using the `.zip` package. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index c4f5439e25489d..01a9e96484965a 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -633,9 +633,10 @@ include::{kib-repo-dir}/settings/alert-action-settings.asciidoc[] include::{kib-repo-dir}/settings/apm-settings.asciidoc[] include::{kib-repo-dir}/settings/dev-settings.asciidoc[] include::{kib-repo-dir}/settings/graph-settings.asciidoc[] -include::{kib-repo-dir}/settings/infrastructure-ui-settings.asciidoc[] +include::{kib-repo-dir}/settings/ingest-manager-settings.asciidoc[] include::{kib-repo-dir}/settings/i18n-settings.asciidoc[] include::{kib-repo-dir}/settings/logs-ui-settings.asciidoc[] +include::{kib-repo-dir}/settings/infrastructure-ui-settings.asciidoc[] include::{kib-repo-dir}/settings/ml-settings.asciidoc[] include::{kib-repo-dir}/settings/monitoring-settings.asciidoc[] include::{kib-repo-dir}/settings/reporting-settings.asciidoc[] diff --git a/docs/user/graph/configuring-graph.asciidoc b/docs/user/graph/configuring-graph.asciidoc index 5427bdee79ecbb..4eb8939b004ba5 100644 --- a/docs/user/graph/configuring-graph.asciidoc +++ b/docs/user/graph/configuring-graph.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[graph-configuration]] -== Configuring Graph +== Configure Graph When a user saves a graph workspace in Kibana, it is stored in the `.kibana` index along with other saved objects like visualizations and dashboards. @@ -48,7 +48,7 @@ Only the configuration is saved unless the user explicitly selects the include data option. [float] -=== Using Security to grant access +=== Use Security to grant access You can also use security to grant read only or all access to different roles. When security is used to grant read only access, the following indicator in Kibana is displayed. For more information on granting access to Kibana, see @@ -59,7 +59,7 @@ image::user/graph/images/graph-read-only-badge.png[Example of Graph's read only [discrete] [[disable-drill-down]] -=== Disabling drilldown configuration +=== Disable drilldown configuration By default, users can configure _drilldown_ URLs to display additional information about a selected vertex in a new browser window. For example, diff --git a/docs/user/graph/getting-started.asciidoc b/docs/user/graph/getting-started.asciidoc index a155017f1bb223..4f61b62da5cce6 100644 --- a/docs/user/graph/getting-started.asciidoc +++ b/docs/user/graph/getting-started.asciidoc @@ -1,13 +1,13 @@ [role="xpack"] [[graph-getting-started]] -== Using Graph +== Create a graph You must index data into {es} before you can create a graph. <> or get started with a <>. [float] [[exploring-connections]] -=== Graph connections in your data +=== Graph a data connection . From the side navigation, open *Graph*. + diff --git a/docs/user/graph/index.asciidoc b/docs/user/graph/index.asciidoc index f9094f5b594b10..40c75c868e237a 100644 --- a/docs/user/graph/index.asciidoc +++ b/docs/user/graph/index.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[xpack-graph]] -= Graph data connections += Graph [partintro] -- diff --git a/docs/user/graph/troubleshooting.asciidoc b/docs/user/graph/troubleshooting.asciidoc index 4ce287396f8091..3819d99036f986 100644 --- a/docs/user/graph/troubleshooting.asciidoc +++ b/docs/user/graph/troubleshooting.asciidoc @@ -2,7 +2,7 @@ [[graph-troubleshooting]] == Graph troubleshooting ++++ -Troubleshooting +Troubleshoot ++++ [discrete] diff --git a/package.json b/package.json index 419edcf2683569..d5f738fad0400a 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "@elastic/apm-rum": "^5.1.1", "@elastic/charts": "19.2.0", "@elastic/datemath": "5.0.3", - "@elastic/ems-client": "7.8.0", + "@elastic/ems-client": "7.9.3", "@elastic/eui": "24.1.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", @@ -236,6 +236,7 @@ "pug": "^2.0.4", "query-string": "5.1.1", "raw-loader": "3.1.0", + "re2": "1.14.0", "react": "^16.12.0", "react-color": "^2.13.8", "react-dom": "^16.12.0", diff --git a/src/dev/build/build_distributables.js b/src/dev/build/build_distributables.js index 66f0c0355c2d92..2ea71fa2c1d335 100644 --- a/src/dev/build/build_distributables.js +++ b/src/dev/build/build_distributables.js @@ -47,6 +47,7 @@ import { InstallDependenciesTask, BuildKibanaPlatformPluginsTask, OptimizeBuildTask, + PatchNativeModulesTask, RemovePackageJsonDepsTask, RemoveWorkspacesTask, TranspileBabelTask, @@ -136,6 +137,7 @@ export async function buildDistributables(options) { * directories and perform platform-specific steps */ await run(CreateArchivesSourcesTask); + await run(PatchNativeModulesTask); await run(CleanExtraBinScriptsTask); await run(CleanExtraBrowsersTask); await run(CleanNodeBuildsTask); diff --git a/src/dev/build/tasks/nodejs/__tests__/download.js b/src/dev/build/lib/__tests__/download.js similarity index 100% rename from src/dev/build/tasks/nodejs/__tests__/download.js rename to src/dev/build/lib/__tests__/download.js diff --git a/src/dev/build/lib/__tests__/fixtures/foo.txt.gz b/src/dev/build/lib/__tests__/fixtures/foo.txt.gz new file mode 100644 index 00000000000000..46fef5a6af78cb Binary files /dev/null and b/src/dev/build/lib/__tests__/fixtures/foo.txt.gz differ diff --git a/src/dev/build/lib/__tests__/fs.js b/src/dev/build/lib/__tests__/fs.js index 0b2db4c538fb82..bf7596b012f791 100644 --- a/src/dev/build/lib/__tests__/fs.js +++ b/src/dev/build/lib/__tests__/fs.js @@ -23,11 +23,12 @@ import { chmodSync, statSync } from 'fs'; import del from 'del'; import expect from '@kbn/expect'; -import { mkdirp, write, read, getChildPaths, copyAll, getFileHash, untar } from '../fs'; +import { mkdirp, write, read, getChildPaths, copyAll, getFileHash, untar, gunzip } from '../fs'; const TMP = resolve(__dirname, '__tmp__'); const FIXTURES = resolve(__dirname, 'fixtures'); const FOO_TAR_PATH = resolve(FIXTURES, 'foo_dir.tar.gz'); +const FOO_GZIP_PATH = resolve(FIXTURES, 'foo.txt.gz'); const BAR_TXT_PATH = resolve(FIXTURES, 'foo_dir/bar.txt'); const WORLD_EXECUTABLE = resolve(FIXTURES, 'bin/world_executable'); @@ -323,4 +324,39 @@ describe('dev/build/lib/fs', () => { expect(await read(resolve(destination, 'foo/foo.txt'))).to.be('foo\n'); }); }); + + describe('gunzip()', () => { + it('rejects if source path is not absolute', async () => { + try { + await gunzip('foo/bar', '**/*', __dirname); + throw new Error('Expected gunzip() to reject'); + } catch (error) { + assertNonAbsoluteError(error); + } + }); + + it('rejects if destination path is not absolute', async () => { + try { + await gunzip(__dirname, '**/*', 'foo/bar'); + throw new Error('Expected gunzip() to reject'); + } catch (error) { + assertNonAbsoluteError(error); + } + }); + + it('rejects if neither path is not absolute', async () => { + try { + await gunzip('foo/bar', '**/*', 'foo/bar'); + throw new Error('Expected gunzip() to reject'); + } catch (error) { + assertNonAbsoluteError(error); + } + }); + + it('extracts gzip from source into destination, creating destination if necessary', async () => { + const destination = resolve(TMP, 'z/y/x/v/u/t/foo.txt'); + await gunzip(FOO_GZIP_PATH, destination); + expect(await read(resolve(destination))).to.be('foo\n'); + }); + }); }); diff --git a/src/dev/build/tasks/nodejs/download.js b/src/dev/build/lib/download.js similarity index 98% rename from src/dev/build/tasks/nodejs/download.js rename to src/dev/build/lib/download.js index fb3294e2d1221a..fbd2d47ff7b067 100644 --- a/src/dev/build/tasks/nodejs/download.js +++ b/src/dev/build/lib/download.js @@ -24,7 +24,7 @@ import chalk from 'chalk'; import { createHash } from 'crypto'; import Axios from 'axios'; -import { mkdirp } from '../../lib'; +import { mkdirp } from './fs'; function tryUnlink(path) { try { diff --git a/src/dev/build/lib/fs.js b/src/dev/build/lib/fs.js index 864a07e837c3f2..b905f40d0de1e5 100644 --- a/src/dev/build/lib/fs.js +++ b/src/dev/build/lib/fs.js @@ -195,6 +195,19 @@ export async function untar(source, destination, extractOptions = {}) { ]); } +export async function gunzip(source, destination) { + assertAbsolute(source); + assertAbsolute(destination); + + await mkdirAsync(dirname(destination), { recursive: true }); + + await createPromiseFromStreams([ + fs.createReadStream(source), + createGunzip(), + fs.createWriteStream(destination), + ]); +} + export async function compress(type, options = {}, source, destination) { const output = fs.createWriteStream(destination); const archive = archiver(type, options.archiverOptions); diff --git a/src/dev/build/lib/index.js b/src/dev/build/lib/index.js index afebd090d797da..6540db6f37a724 100644 --- a/src/dev/build/lib/index.js +++ b/src/dev/build/lib/index.js @@ -28,10 +28,12 @@ export { copyAll, getFileHash, untar, + gunzip, deleteAll, deleteEmptyFolders, compress, isFileAccessible, } from './fs'; +export { download } from './download'; export { scanDelete } from './scan_delete'; export { scanCopy } from './scan_copy'; diff --git a/src/dev/build/tasks/index.js b/src/dev/build/tasks/index.js index bafb5a2fe115e4..be675b4aa6ca4d 100644 --- a/src/dev/build/tasks/index.js +++ b/src/dev/build/tasks/index.js @@ -33,6 +33,7 @@ export * from './nodejs_modules'; export * from './notice_file_task'; export * from './optimize_task'; export * from './os_packages'; +export * from './patch_native_modules_task'; export * from './transpile_babel_task'; export * from './transpile_scss_task'; export * from './verify_env_task'; diff --git a/src/dev/build/tasks/nodejs/__tests__/download_node_builds_task.js b/src/dev/build/tasks/nodejs/__tests__/download_node_builds_task.js index 9357735e3f5a30..c1764d06b43b33 100644 --- a/src/dev/build/tasks/nodejs/__tests__/download_node_builds_task.js +++ b/src/dev/build/tasks/nodejs/__tests__/download_node_builds_task.js @@ -22,7 +22,7 @@ import expect from '@kbn/expect'; import * as NodeShasumsNS from '../node_shasums'; import * as NodeDownloadInfoNS from '../node_download_info'; -import * as DownloadNS from '../download'; +import * as DownloadNS from '../../../lib/download'; // sinon can't stub '../../../lib' properly import { DownloadNodeBuildsTask } from '../download_node_builds_task'; describe('src/dev/build/tasks/nodejs/download_node_builds_task', () => { diff --git a/src/dev/build/tasks/nodejs/download_node_builds_task.js b/src/dev/build/tasks/nodejs/download_node_builds_task.js index 86ddb0506f9724..c0907e6c42a976 100644 --- a/src/dev/build/tasks/nodejs/download_node_builds_task.js +++ b/src/dev/build/tasks/nodejs/download_node_builds_task.js @@ -17,7 +17,7 @@ * under the License. */ -import { download } from './download'; +import { download } from '../../lib'; import { getNodeShasums } from './node_shasums'; import { getNodeDownloadInfo } from './node_download_info'; diff --git a/src/dev/build/tasks/patch_native_modules_task.js b/src/dev/build/tasks/patch_native_modules_task.js new file mode 100644 index 00000000000000..fba33442fad103 --- /dev/null +++ b/src/dev/build/tasks/patch_native_modules_task.js @@ -0,0 +1,103 @@ +/* + * 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 fs from 'fs'; +import path from 'path'; +import util from 'util'; +import { deleteAll, download, gunzip, untar } from '../lib'; + +const DOWNLOAD_DIRECTORY = '.native_modules'; + +const packages = [ + { + name: 're2', + version: '1.14.0', + destinationPath: 'node_modules/re2/build/Release/re2.node', + extractMethod: 'gunzip', + archives: { + darwin: { + url: 'https://github.com/uhop/node-re2/releases/download/1.14.0/darwin-x64-64.gz', + sha256: '54c8386cb7cd53895cf379522114bfe82378e300e127e58d392ddd40a77e396f', + }, + linux: { + url: 'https://github.com/uhop/node-re2/releases/download/1.14.0/linux-x64-64.gz', + sha256: 'f54f059035e71a7ccb3fa201080e260c41d228d13a8247974b4bb157691b6757', + }, + windows: { + url: 'https://github.com/uhop/node-re2/releases/download/1.14.0/win32-x64-64.gz', + sha256: 'de708446a8b802f4634c2cfef097c2625a2811fdcd8133dfd7b7c485f966caa9', + }, + }, + }, +]; + +async function getInstalledVersion(config, packageName) { + const packageJSONPath = config.resolveFromRepo( + path.join('node_modules', packageName, 'package.json') + ); + const buffer = await util.promisify(fs.readFile)(packageJSONPath); + const packageJSON = JSON.parse(buffer); + return packageJSON.version; +} + +async function patchModule(config, log, build, platform, pkg) { + const installedVersion = await getInstalledVersion(config, pkg.name); + if (installedVersion !== pkg.version) { + throw new Error( + `Can't patch ${pkg.name}'s native module, we were expecting version ${pkg.version} and found ${installedVersion}` + ); + } + const platformName = platform.getName(); + const archive = pkg.archives[platformName]; + const archiveName = path.basename(archive.url); + const downloadPath = config.resolveFromRepo(DOWNLOAD_DIRECTORY, pkg.name, archiveName); + const extractPath = build.resolvePathForPlatform(platform, pkg.destinationPath); + log.debug(`Patching ${pkg.name} binaries from ${archive.url} to ${extractPath}`); + + await deleteAll([extractPath], log); + await download({ + log, + url: archive.url, + destination: downloadPath, + sha256: archive.sha256, + retries: 3, + }); + switch (pkg.extractMethod) { + case 'gunzip': + await gunzip(downloadPath, extractPath); + break; + case 'untar': + await untar(downloadPath, extractPath); + break; + default: + throw new Error(`Extract method of ${pkg.extractMethod} is not supported`); + } +} + +export const PatchNativeModulesTask = { + description: 'Patching platform-specific native modules', + async run(config, log, build) { + for (const pkg of packages) { + await Promise.all( + config.getTargetPlatforms().map(async (platform) => { + await patchModule(config, log, build, platform, pkg); + }) + ); + } + }, +}; diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 64db131f5219af..04b07748c40525 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -79,7 +79,7 @@ export default { ], coverageDirectory: '/target/kibana-coverage/jest', coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html', 'text'], - moduleFileExtensions: ['js', 'json', 'ts', 'tsx'], + moduleFileExtensions: ['js', 'json', 'ts', 'tsx', 'node'], modulePathIgnorePatterns: ['__fixtures__/', 'target/'], testMatch: ['**/*.test.{js,ts,tsx}'], testPathIgnorePatterns: [ diff --git a/src/plugins/console/public/lib/autocomplete/autocomplete.ts b/src/plugins/console/public/lib/autocomplete/autocomplete.ts index b05c7ddbb020d0..3959f2ed8cf46f 100644 --- a/src/plugins/console/public/lib/autocomplete/autocomplete.ts +++ b/src/plugins/console/public/lib/autocomplete/autocomplete.ts @@ -308,7 +308,7 @@ export function getCurrentMethodAndTokenPaths( } // eslint-disable-next-line -export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor; parser: any }) { +export default function ({ coreEditor: editor, parser }: { coreEditor: CoreEditor; parser: any }) { function isUrlPathToken(token: Token | null) { switch ((token || ({} as any)).type) { case 'url.slash': diff --git a/src/plugins/data/common/field_formats/index.ts b/src/plugins/data/common/field_formats/index.ts index b64e115fd55ff2..5c67073c07dd54 100644 --- a/src/plugins/data/common/field_formats/index.ts +++ b/src/plugins/data/common/field_formats/index.ts @@ -54,4 +54,5 @@ export { // Used in data plugin only FieldFormatInstanceType, IFieldFormat, + FieldFormatsStartCommon, } from './types'; diff --git a/src/plugins/data/common/field_formats/types.ts b/src/plugins/data/common/field_formats/types.ts index 5f11c7fe094bcd..6f773378de08d2 100644 --- a/src/plugins/data/common/field_formats/types.ts +++ b/src/plugins/data/common/field_formats/types.ts @@ -17,6 +17,7 @@ * under the License. */ import { FieldFormat } from './field_format'; +import { FieldFormatsRegistry } from './field_formats_registry'; /** @public **/ export type FieldFormatsContentType = 'html' | 'text'; @@ -99,3 +100,5 @@ export interface IFieldFormatMetaParams { basePath?: string; }; } + +export type FieldFormatsStartCommon = Omit; diff --git a/src/plugins/kibana_utils/public/field_mapping/index.ts b/src/plugins/data/common/field_mapping/index.ts similarity index 100% rename from src/plugins/kibana_utils/public/field_mapping/index.ts rename to src/plugins/data/common/field_mapping/index.ts diff --git a/src/plugins/kibana_utils/public/field_mapping/mapping_setup.test.ts b/src/plugins/data/common/field_mapping/mapping_setup.test.ts similarity index 97% rename from src/plugins/kibana_utils/public/field_mapping/mapping_setup.test.ts rename to src/plugins/data/common/field_mapping/mapping_setup.test.ts index ca40685db0ebf1..e57699e879a878 100644 --- a/src/plugins/kibana_utils/public/field_mapping/mapping_setup.test.ts +++ b/src/plugins/data/common/field_mapping/mapping_setup.test.ts @@ -18,7 +18,7 @@ */ import { expandShorthand } from './mapping_setup'; -import { ES_FIELD_TYPES } from '../../../data/public'; +import { ES_FIELD_TYPES } from '../../../data/common'; describe('mapping_setup', () => { it('allows shortcuts for field types by just setting the value to the type name', () => { diff --git a/src/plugins/kibana_utils/public/field_mapping/mapping_setup.ts b/src/plugins/data/common/field_mapping/mapping_setup.ts similarity index 100% rename from src/plugins/kibana_utils/public/field_mapping/mapping_setup.ts rename to src/plugins/data/common/field_mapping/mapping_setup.ts diff --git a/src/plugins/kibana_utils/public/field_mapping/types.ts b/src/plugins/data/common/field_mapping/types.ts similarity index 95% rename from src/plugins/kibana_utils/public/field_mapping/types.ts rename to src/plugins/data/common/field_mapping/types.ts index f3fb9b000e45a5..973a58d3baec4e 100644 --- a/src/plugins/kibana_utils/public/field_mapping/types.ts +++ b/src/plugins/data/common/field_mapping/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ES_FIELD_TYPES } from '../../../data/public'; +import { ES_FIELD_TYPES } from '../../../data/common'; /** @public */ export interface FieldMappingSpec { diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index e4a663a1599f12..adbd93d518fc7d 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -27,3 +27,4 @@ export * from './search'; export * from './search/aggs'; export * from './types'; export * from './utils'; +export * from './field_mapping'; diff --git a/src/plugins/data/public/index_patterns/fields/__snapshots__/field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/field.test.ts.snap similarity index 100% rename from src/plugins/data/public/index_patterns/fields/__snapshots__/field.test.ts.snap rename to src/plugins/data/common/index_patterns/fields/__snapshots__/field.test.ts.snap diff --git a/src/plugins/data/public/index_patterns/fields/field.test.ts b/src/plugins/data/common/index_patterns/fields/field.test.ts similarity index 94% rename from src/plugins/data/public/index_patterns/fields/field.test.ts rename to src/plugins/data/common/index_patterns/fields/field.test.ts index 18252b159d98d6..711c176fed9ccb 100644 --- a/src/plugins/data/public/index_patterns/fields/field.test.ts +++ b/src/plugins/data/common/index_patterns/fields/field.test.ts @@ -18,9 +18,8 @@ */ import { Field } from './field'; -import { IndexPattern } from '..'; -import { notificationServiceMock } from '../../../../../core/public/mocks'; -import { FieldFormatsStart } from '../../field_formats'; +import { IndexPattern } from '../index_patterns'; +import { FieldFormatsStartCommon } from '../..'; import { KBN_FIELD_TYPES } from '../../../common'; describe('Field', function () { @@ -34,8 +33,8 @@ describe('Field', function () { { ...fieldValues, ...values }, false, { - fieldFormats: {} as FieldFormatsStart, - toastNotifications: notificationServiceMock.createStartContract().toasts, + fieldFormats: {} as FieldFormatsStartCommon, + onNotification: () => {}, } ); } @@ -215,8 +214,8 @@ describe('Field', function () { it('exports the property to JSON', () => { const field = new Field({ fieldFormatMap: { name: {} } } as IndexPattern, fieldValues, false, { - fieldFormats: {} as FieldFormatsStart, - toastNotifications: notificationServiceMock.createStartContract().toasts, + fieldFormats: {} as FieldFormatsStartCommon, + onNotification: () => {}, }); expect(flatten(field)).toMatchSnapshot(); }); diff --git a/src/plugins/data/public/index_patterns/fields/field.ts b/src/plugins/data/common/index_patterns/fields/field.ts similarity index 89% rename from src/plugins/data/public/index_patterns/fields/field.ts rename to src/plugins/data/common/index_patterns/fields/field.ts index 625df17d62e0dc..c53e3f2b1f621f 100644 --- a/src/plugins/data/public/index_patterns/fields/field.ts +++ b/src/plugins/data/common/index_patterns/fields/field.ts @@ -18,10 +18,9 @@ */ import { i18n } from '@kbn/i18n'; -import { ToastsStart } from 'kibana/public'; // @ts-ignore import { ObjDefine } from './obj_define'; -import { IndexPattern } from '../index_patterns'; +import { IIndexPattern } from '../../types'; import { IFieldType, getKbnFieldType, @@ -29,13 +28,14 @@ import { FieldFormat, shortenDottedString, } from '../../../common'; -import { FieldFormatsStart } from '../../field_formats'; +import { OnNotification } from '../types'; +import { FieldFormatsStartCommon } from '../../field_formats'; export type FieldSpec = Record; interface FieldDependencies { - fieldFormats: FieldFormatsStart; - toastNotifications: ToastsStart; + fieldFormats: FieldFormatsStartCommon; + onNotification: OnNotification; } export class Field implements IFieldType { @@ -55,17 +55,17 @@ export class Field implements IFieldType { scripted?: boolean; subType?: IFieldSubType; displayName?: string; - indexPattern?: IndexPattern; + indexPattern?: IIndexPattern; readFromDocValues?: boolean; format: any; $$spec: FieldSpec; conflictDescriptions?: Record; constructor( - indexPattern: IndexPattern, + indexPattern: IIndexPattern, spec: FieldSpec | Field, shortDotsEnable: boolean, - { fieldFormats, toastNotifications }: FieldDependencies + { fieldFormats, onNotification }: FieldDependencies ) { // unwrap old instances of Field if (spec instanceof Field) spec = spec.$$spec; @@ -90,11 +90,7 @@ export class Field implements IFieldType { values: { name: spec.name, title: indexPattern.title }, defaultMessage: 'Field {name} in indexPattern {title} is using an unknown field type.', }); - - toastNotifications.addDanger({ - title, - text, - }); + onNotification({ title, text, color: 'danger', iconType: 'alert' }); } if (!type) type = getKbnFieldType('unknown'); @@ -103,7 +99,7 @@ export class Field implements IFieldType { if (!FieldFormat.isInstanceOfFieldFormat(format)) { format = - indexPattern.fieldFormatMap[spec.name] || + (indexPattern.fieldFormatMap && indexPattern.fieldFormatMap[spec.name]) || fieldFormats.getDefaultInstance(spec.type, spec.esTypes); } diff --git a/src/plugins/data/public/index_patterns/fields/field_list.ts b/src/plugins/data/common/index_patterns/fields/field_list.ts similarity index 87% rename from src/plugins/data/public/index_patterns/fields/field_list.ts rename to src/plugins/data/common/index_patterns/fields/field_list.ts index 1aef0b1ccadaa0..173a629863a716 100644 --- a/src/plugins/data/public/index_patterns/fields/field_list.ts +++ b/src/plugins/data/common/index_patterns/fields/field_list.ts @@ -18,17 +18,17 @@ */ import { findIndex } from 'lodash'; -import { ToastsStart } from 'kibana/public'; -import { IndexPattern } from '../index_patterns'; +import { IIndexPattern } from '../../types'; import { IFieldType } from '../../../common'; import { Field, FieldSpec } from './field'; -import { FieldFormatsStart } from '../../field_formats'; +import { OnNotification } from '../types'; +import { FieldFormatsStartCommon } from '../../field_formats'; type FieldMap = Map; interface FieldListDependencies { - fieldFormats: FieldFormatsStart; - toastNotifications: ToastsStart; + fieldFormats: FieldFormatsStartCommon; + onNotification: OnNotification; } export interface IIndexPatternFieldList extends Array { @@ -40,19 +40,19 @@ export interface IIndexPatternFieldList extends Array { } export type CreateIndexPatternFieldList = ( - indexPattern: IndexPattern, + indexPattern: IIndexPattern, specs?: FieldSpec[], shortDotsEnable?: boolean ) => IIndexPatternFieldList; export const getIndexPatternFieldListCreator = ({ fieldFormats, - toastNotifications, + onNotification, }: FieldListDependencies): CreateIndexPatternFieldList => (...fieldListParams) => { class FieldList extends Array implements IIndexPatternFieldList { private byName: FieldMap = new Map(); private groups: Map = new Map(); - private indexPattern: IndexPattern; + private indexPattern: IIndexPattern; private shortDotsEnable: boolean; private setByName = (field: Field) => this.byName.set(field.name, field); private setByGroup = (field: Field) => { @@ -63,7 +63,7 @@ export const getIndexPatternFieldListCreator = ({ }; private removeByGroup = (field: IFieldType) => this.groups.get(field.type)!.delete(field.name); - constructor(indexPattern: IndexPattern, specs: FieldSpec[] = [], shortDotsEnable = false) { + constructor(indexPattern: IIndexPattern, specs: FieldSpec[] = [], shortDotsEnable = false) { super(); this.indexPattern = indexPattern; this.shortDotsEnable = shortDotsEnable; @@ -76,7 +76,7 @@ export const getIndexPatternFieldListCreator = ({ add = (field: FieldSpec) => { const newField = new Field(this.indexPattern, field, this.shortDotsEnable, { fieldFormats, - toastNotifications, + onNotification, }); this.push(newField); this.setByName(newField); @@ -94,7 +94,7 @@ export const getIndexPatternFieldListCreator = ({ update = (field: FieldSpec) => { const newField = new Field(this.indexPattern, field, this.shortDotsEnable, { fieldFormats, - toastNotifications, + onNotification, }); const index = this.findIndex((f) => f.name === newField.name); this.splice(index, 1, newField); diff --git a/src/plugins/data/common/index_patterns/fields/index.ts b/src/plugins/data/common/index_patterns/fields/index.ts index 5b6fef3e51fa9b..1b7c87d556f592 100644 --- a/src/plugins/data/common/index_patterns/fields/index.ts +++ b/src/plugins/data/common/index_patterns/fields/index.ts @@ -19,3 +19,5 @@ export * from './types'; export { isFilterable, isNestedField } from './utils'; +export * from './field_list'; +export * from './field'; diff --git a/src/plugins/data/public/index_patterns/fields/obj_define.js b/src/plugins/data/common/index_patterns/fields/obj_define.js similarity index 100% rename from src/plugins/data/public/index_patterns/fields/obj_define.js rename to src/plugins/data/common/index_patterns/fields/obj_define.js diff --git a/src/plugins/data/public/index_patterns/fields/obj_define.test.js b/src/plugins/data/common/index_patterns/fields/obj_define.test.js similarity index 100% rename from src/plugins/data/public/index_patterns/fields/obj_define.test.js rename to src/plugins/data/common/index_patterns/fields/obj_define.test.js diff --git a/src/plugins/data/public/index_patterns/index_patterns/_fields_fetcher.ts b/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts similarity index 91% rename from src/plugins/data/public/index_patterns/index_patterns/_fields_fetcher.ts rename to src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts index d9850086339c04..727c4d445688dd 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/_fields_fetcher.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts @@ -17,8 +17,7 @@ * under the License. */ -import { IndexPattern } from './index_pattern'; -import { GetFieldsOptions, IIndexPatternsApiClient } from './index_patterns_api_client'; +import { GetFieldsOptions, IIndexPatternsApiClient, IndexPattern } from '.'; /** @internal */ export const createFieldsFetcher = ( diff --git a/src/plugins/data/public/index_patterns/index_patterns/_pattern_cache.ts b/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts similarity index 100% rename from src/plugins/data/public/index_patterns/index_patterns/_pattern_cache.ts rename to src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts diff --git a/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts new file mode 100644 index 00000000000000..2737627bf1977b --- /dev/null +++ b/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { contains } from 'lodash'; +import { CoreStart } from 'kibana/public'; +import { IndexPatternsContract } from './index_patterns'; + +export type EnsureDefaultIndexPattern = () => Promise | undefined; + +export const createEnsureDefaultIndexPattern = ( + uiSettings: CoreStart['uiSettings'], + onRedirectNoIndexPattern: () => Promise | void +) => { + /** + * Checks whether a default index pattern is set and exists and defines + * one otherwise. + */ + return async function ensureDefaultIndexPattern(this: IndexPatternsContract) { + const patterns = await this.getIds(); + let defaultId = uiSettings.get('defaultIndex'); + let defined = !!defaultId; + const exists = contains(patterns, defaultId); + + if (defined && !exists) { + uiSettings.remove('defaultIndex'); + defaultId = defined = false; + } + + if (defined) { + return; + } + + // If there is any index pattern created, set the first as default + if (patterns.length >= 1) { + defaultId = patterns[0]; + uiSettings.set('defaultIndex', defaultId); + } else { + return onRedirectNoIndexPattern(); + } + }; +}; diff --git a/src/plugins/data/public/index_patterns/index_patterns/flatten_hit.ts b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts similarity index 100% rename from src/plugins/data/public/index_patterns/index_patterns/flatten_hit.ts rename to src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts diff --git a/src/plugins/data/public/index_patterns/index_patterns/format_hit.ts b/src/plugins/data/common/index_patterns/index_patterns/format_hit.ts similarity index 100% rename from src/plugins/data/public/index_patterns/index_patterns/format_hit.ts rename to src/plugins/data/common/index_patterns/index_patterns/format_hit.ts diff --git a/src/plugins/data/common/index_patterns/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index_patterns/index.ts new file mode 100644 index 00000000000000..5fae08f3bb7755 --- /dev/null +++ b/src/plugins/data/common/index_patterns/index_patterns/index.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './index_patterns_api_client'; +export * from './types'; +export * from './_pattern_cache'; +export * from './flatten_hit'; +export * from './format_hit'; +export * from './index_pattern'; +export * from './index_patterns'; diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts similarity index 93% rename from src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts rename to src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index 84135bb5d1e2bd..8ec3072bf916bc 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -19,24 +19,19 @@ import { defaults, pluck, last, get } from 'lodash'; -jest.mock('../../../../kibana_utils/public/history'); import { IndexPattern } from './index_pattern'; -import { DuplicateField } from '../../../../kibana_utils/public'; +import { DuplicateField } from '../../../../kibana_utils/common'; // @ts-ignore import mockLogStashFields from '../../../../../fixtures/logstash_fields'; // @ts-ignore import { stubbedSavedObjectIndexPattern } from '../../../../../fixtures/stubbed_saved_object_index_pattern'; import { Field } from '../fields'; -import { setNotifications, setFieldFormats } from '../../services'; -// Temporary disable eslint, will be removed after moving to new platform folder -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { notificationServiceMock } from '../../../../../core/public/notifications/notifications_service.mock'; -import { FieldFormatsStart } from '../../field_formats'; +import { FieldFormatsStartCommon } from '../../field_formats'; -jest.mock('../../../../kibana_utils/public', () => { - const originalModule = jest.requireActual('../../../../kibana_utils/public'); +jest.mock('../../field_mapping', () => { + const originalModule = jest.requireActual('../../field_mapping'); return { ...originalModule, @@ -107,7 +102,10 @@ function create(id: string, payload?: any): Promise { (cfg: any) => config.get(cfg), savedObjectsClient as any, apiClient, - patternCache + patternCache, + ({ getDefaultInstance: () => {}, getType: () => {} } as unknown) as FieldFormatsStartCommon, + () => {}, + () => {} ); setDocsourcePayload(id, payload); @@ -121,18 +119,11 @@ function setDocsourcePayload(id: string | null, providedPayload: any) { describe('IndexPattern', () => { const indexPatternId = 'test-pattern'; - const notifications = notificationServiceMock.createStartContract(); let indexPattern: IndexPattern; // create an indexPattern instance for each test beforeEach(() => { - setNotifications(notifications); - setFieldFormats(({ - getDefaultInstance: jest.fn(), - deserialize: jest.fn() as any, - } as unknown) as FieldFormatsStart); - return create(indexPatternId).then((pattern: IndexPattern) => { indexPattern = pattern; }); @@ -377,7 +368,10 @@ describe('IndexPattern', () => { (cfg: any) => config.get(cfg), savedObjectsClient as any, apiClient, - patternCache + patternCache, + ({ getDefaultInstance: () => {}, getType: () => {} } as unknown) as FieldFormatsStartCommon, + () => {}, + () => {} ); await pattern.init(); @@ -389,7 +383,10 @@ describe('IndexPattern', () => { (cfg: any) => config.get(cfg), savedObjectsClient as any, apiClient, - patternCache + patternCache, + ({ getDefaultInstance: () => {}, getType: () => {} } as unknown) as FieldFormatsStartCommon, + () => {}, + () => {} ); await samePattern.init(); diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts similarity index 91% rename from src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts rename to src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 84ea12a1f684fb..aefa8734b01c73 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -20,13 +20,7 @@ import _, { each, reject } from 'lodash'; import { i18n } from '@kbn/i18n'; import { SavedObjectsClientContract } from 'src/core/public'; -import { - DuplicateField, - SavedObjectNotFound, - expandShorthand, - FieldMappingSpec, - MappingObject, -} from '../../../../kibana_utils/public'; +import { DuplicateField, SavedObjectNotFound } from '../../../../kibana_utils/common'; import { ES_FIELD_TYPES, @@ -41,10 +35,12 @@ import { Field, IIndexPatternFieldList, getIndexPatternFieldListCreator } from ' import { createFieldsFetcher } from './_fields_fetcher'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; -import { IIndexPatternsApiClient } from './index_patterns_api_client'; -import { getNotifications, getFieldFormats } from '../../services'; -import { TypeMeta } from './types'; +import { IIndexPatternsApiClient } from '.'; +import { TypeMeta } from '.'; +import { OnNotification, OnError } from '../types'; +import { FieldFormatsStartCommon } from '../../field_formats'; import { PatternCache } from './_pattern_cache'; +import { expandShorthand, FieldMappingSpec, MappingObject } from '../../field_mapping'; const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; const type = 'index-pattern'; @@ -72,6 +68,9 @@ export class IndexPattern implements IIndexPattern { private originalBody: { [key: string]: any } = {}; public fieldsFetcher: any; // probably want to factor out any direct usage and change to private private shortDotsEnable: boolean = false; + private fieldFormats: FieldFormatsStartCommon; + private onNotification: OnNotification; + private onError: OnError; private apiClient: IIndexPatternsApiClient; private mapping: MappingObject = expandShorthand({ @@ -101,7 +100,10 @@ export class IndexPattern implements IIndexPattern { getConfig: any, savedObjectsClient: SavedObjectsClientContract, apiClient: IIndexPatternsApiClient, - patternCache: PatternCache + patternCache: PatternCache, + fieldFormats: FieldFormatsStartCommon, + onNotification: OnNotification, + onError: OnError ) { this.id = id; this.savedObjectsClient = savedObjectsClient; @@ -109,13 +111,16 @@ export class IndexPattern implements IIndexPattern { // instead of storing config we rather store the getter only as np uiSettingsClient has circular references // which cause problems when being consumed from angular this.getConfig = getConfig; + this.fieldFormats = fieldFormats; + this.onNotification = onNotification; + this.onError = onError; this.shortDotsEnable = this.getConfig(UI_SETTINGS.SHORT_DOTS_ENABLE); this.metaFields = this.getConfig(UI_SETTINGS.META_FIELDS); this.createFieldList = getIndexPatternFieldListCreator({ - fieldFormats: getFieldFormats(), - toastNotifications: getNotifications().toasts, + fieldFormats, + onNotification, }); this.fields = this.createFieldList(this, [], this.shortDotsEnable); @@ -128,7 +133,7 @@ export class IndexPattern implements IIndexPattern { this.flattenHit = flattenHitWrapper(this, this.getConfig(UI_SETTINGS.META_FIELDS)); this.formatHit = formatHitProvider( this, - getFieldFormats().getDefaultInstance(KBN_FIELD_TYPES.STRING) + fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING) ); this.formatField = this.formatHit.formatField; } @@ -140,7 +145,7 @@ export class IndexPattern implements IIndexPattern { } private deserializeFieldFormatMap(mapping: any) { - const FieldFormat = getFieldFormats().getType(mapping.id); + const FieldFormat = this.fieldFormats.getType(mapping.id); return FieldFormat && new FieldFormat(mapping.params, this.getConfig); } @@ -296,8 +301,8 @@ export class IndexPattern implements IIndexPattern { }, false, { - fieldFormats: getFieldFormats(), - toastNotifications: getNotifications().toasts, + fieldFormats: this.fieldFormats, + onNotification: this.onNotification, } ) ); @@ -400,8 +405,12 @@ export class IndexPattern implements IIndexPattern { this.getConfig, this.savedObjectsClient, this.apiClient, - this.patternCache + this.patternCache, + this.fieldFormats, + this.onNotification, + this.onError ); + await duplicatePattern.destroy(); } @@ -449,7 +458,10 @@ export class IndexPattern implements IIndexPattern { this.getConfig, this.savedObjectsClient, this.apiClient, - this.patternCache + this.patternCache, + this.fieldFormats, + this.onNotification, + this.onError ); return samePattern.init().then(() => { // What keys changed from now and what the server returned @@ -474,14 +486,12 @@ export class IndexPattern implements IIndexPattern { } if (unresolvedCollision) { - const message = i18n.translate('data.indexPatterns.unableWriteLabel', { + const title = i18n.translate('data.indexPatterns.unableWriteLabel', { defaultMessage: 'Unable to write index pattern! Refresh the page to get the most up to date changes for this index pattern.', }); - const { toasts } = getNotifications(); - - toasts.addDanger(message); + this.onNotification({ title, color: 'danger' }); throw err; } @@ -519,15 +529,13 @@ export class IndexPattern implements IIndexPattern { // we still want to notify the user that there is a problem // but we do not want to potentially make any pages unusable // so do not rethrow the error here - const { toasts } = getNotifications(); if (err instanceof IndexPatternMissingIndices) { - toasts.addDanger((err as any).message); - + this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); return []; } - toasts.addError(err, { + this.onError(err, { title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', { defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})', values: { diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts similarity index 91% rename from src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts rename to src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index fc0be270e9c506..5ff19a3c54cb18 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -21,11 +21,11 @@ import { IndexPatternsService } from './index_patterns'; import { SavedObjectsClientContract, SavedObjectsFindResponsePublic } from 'kibana/public'; import { coreMock, httpServiceMock } from '../../../../../core/public/mocks'; -import { fieldFormatsServiceMock } from '../../field_formats/mocks'; +import { fieldFormatsMock } from '../../field_formats/mocks'; const core = coreMock.createStart(); const http = httpServiceMock.createStartContract(); -const fieldFormats = fieldFormatsServiceMock.createStartContract(); +const fieldFormats = fieldFormatsMock; jest.mock('./index_pattern', () => { class IndexPattern { @@ -62,7 +62,15 @@ describe('IndexPatterns', () => { }) as Promise> ); - indexPatterns = new IndexPatternsService(core, savedObjectsClient, http, fieldFormats); + indexPatterns = new IndexPatternsService( + core.uiSettings, + savedObjectsClient, + http, + fieldFormats, + () => {}, + () => {}, + () => {} + ); }); test('does cache gets for the same id', async () => { diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts similarity index 85% rename from src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts rename to src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 32b31d4f2758d5..db2a68956f1063 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -25,9 +25,9 @@ import { CoreStart, } from 'src/core/public'; -import { createIndexPatternCache } from './_pattern_cache'; +import { createIndexPatternCache } from '.'; import { IndexPattern } from './index_pattern'; -import { IndexPatternsApiClient, GetFieldsOptions } from './index_patterns_api_client'; +import { IndexPatternsApiClient, GetFieldsOptions } from '.'; import { createEnsureDefaultIndexPattern, EnsureDefaultIndexPattern, @@ -38,7 +38,8 @@ import { Field, FieldSpec, } from '../fields'; -import { FieldFormatsStart } from '../../field_formats'; +import { OnNotification, OnError } from '../types'; +import { FieldFormatsStartCommon } from '../../field_formats'; const indexPatternCache = createIndexPatternCache(); @@ -53,6 +54,9 @@ export class IndexPatternsService { private savedObjectsClient: SavedObjectsClientContract; private savedObjectsCache?: Array> | null; private apiClient: IndexPatternsApiClient; + private fieldFormats: FieldFormatsStartCommon; + private onNotification: OnNotification; + private onError: OnError; ensureDefaultIndexPattern: EnsureDefaultIndexPattern; createFieldList: CreateIndexPatternFieldList; createField: ( @@ -62,23 +66,32 @@ export class IndexPatternsService { ) => Field; constructor( - core: CoreStart, + uiSettings: CoreStart['uiSettings'], savedObjectsClient: SavedObjectsClientContract, http: HttpStart, - fieldFormats: FieldFormatsStart + fieldFormats: FieldFormatsStartCommon, + onNotification: OnNotification, + onError: OnError, + onRedirectNoIndexPattern: () => void ) { this.apiClient = new IndexPatternsApiClient(http); - this.config = core.uiSettings; + this.config = uiSettings; this.savedObjectsClient = savedObjectsClient; - this.ensureDefaultIndexPattern = createEnsureDefaultIndexPattern(core); + this.fieldFormats = fieldFormats; + this.onNotification = onNotification; + this.onError = onError; + this.ensureDefaultIndexPattern = createEnsureDefaultIndexPattern( + uiSettings, + onRedirectNoIndexPattern + ); this.createFieldList = getIndexPatternFieldListCreator({ fieldFormats, - toastNotifications: core.notifications.toasts, + onNotification, }); this.createField = (indexPattern, spec, shortDotsEnable) => { return new Field(indexPattern, spec, shortDotsEnable, { fieldFormats, - toastNotifications: core.notifications.toasts, + onNotification, }); }; } @@ -178,7 +191,10 @@ export class IndexPatternsService { (cfg: any) => this.config.get(cfg), this.savedObjectsClient, this.apiClient, - indexPatternCache + indexPatternCache, + this.fieldFormats, + this.onNotification, + this.onError ); return indexPattern.init(); diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts similarity index 100% rename from src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts rename to src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.test.ts similarity index 100% rename from src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts rename to src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.test.ts diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.ts similarity index 100% rename from src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts rename to src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.ts diff --git a/src/plugins/data/public/index_patterns/index_patterns/types.ts b/src/plugins/data/common/index_patterns/index_patterns/types.ts similarity index 100% rename from src/plugins/data/public/index_patterns/index_patterns/types.ts rename to src/plugins/data/common/index_patterns/index_patterns/types.ts diff --git a/src/plugins/data/public/index_patterns/lib/errors.ts b/src/plugins/data/common/index_patterns/lib/errors.ts similarity index 100% rename from src/plugins/data/public/index_patterns/lib/errors.ts rename to src/plugins/data/common/index_patterns/lib/errors.ts diff --git a/src/plugins/data/public/index_patterns/lib/get_from_saved_object.ts b/src/plugins/data/common/index_patterns/lib/get_from_saved_object.ts similarity index 100% rename from src/plugins/data/public/index_patterns/lib/get_from_saved_object.ts rename to src/plugins/data/common/index_patterns/lib/get_from_saved_object.ts diff --git a/src/plugins/data/public/index_patterns/lib/get_title.ts b/src/plugins/data/common/index_patterns/lib/get_title.ts similarity index 100% rename from src/plugins/data/public/index_patterns/lib/get_title.ts rename to src/plugins/data/common/index_patterns/lib/get_title.ts diff --git a/src/plugins/data/public/index_patterns/lib/index.ts b/src/plugins/data/common/index_patterns/lib/index.ts similarity index 99% rename from src/plugins/data/public/index_patterns/lib/index.ts rename to src/plugins/data/common/index_patterns/lib/index.ts index 2893096c4af9d4..d9eccb6685ded7 100644 --- a/src/plugins/data/public/index_patterns/lib/index.ts +++ b/src/plugins/data/common/index_patterns/lib/index.ts @@ -17,9 +17,10 @@ * under the License. */ -export { getTitle } from './get_title'; -export * from './types'; -export { validateIndexPattern } from './validate_index_pattern'; export { IndexPatternMissingIndices } from './errors'; +export { getTitle } from './get_title'; export { getFromSavedObject } from './get_from_saved_object'; export { isDefault } from './is_default'; + +export * from './types'; +export { validateIndexPattern } from './validate_index_pattern'; diff --git a/src/plugins/data/public/index_patterns/lib/is_default.ts b/src/plugins/data/common/index_patterns/lib/is_default.ts similarity index 100% rename from src/plugins/data/public/index_patterns/lib/is_default.ts rename to src/plugins/data/common/index_patterns/lib/is_default.ts diff --git a/src/plugins/data/public/index_patterns/lib/types.ts b/src/plugins/data/common/index_patterns/lib/types.ts similarity index 100% rename from src/plugins/data/public/index_patterns/lib/types.ts rename to src/plugins/data/common/index_patterns/lib/types.ts diff --git a/src/plugins/data/public/index_patterns/lib/validate_index_pattern.test.ts b/src/plugins/data/common/index_patterns/lib/validate_index_pattern.test.ts similarity index 100% rename from src/plugins/data/public/index_patterns/lib/validate_index_pattern.test.ts rename to src/plugins/data/common/index_patterns/lib/validate_index_pattern.test.ts diff --git a/src/plugins/data/public/index_patterns/lib/validate_index_pattern.ts b/src/plugins/data/common/index_patterns/lib/validate_index_pattern.ts similarity index 100% rename from src/plugins/data/public/index_patterns/lib/validate_index_pattern.ts rename to src/plugins/data/common/index_patterns/lib/validate_index_pattern.ts diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index e21d27a70e02ad..7399bbbc10a7e1 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -17,6 +17,7 @@ * under the License. */ +import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notifications'; import { IFieldType } from './fields'; export interface IIndexPattern { @@ -47,3 +48,6 @@ export interface IndexPatternAttributes { typeMeta: string; timeFieldName?: string; } + +export type OnNotification = (toastInputFields: ToastInputFields) => void; +export type OnError = (error: Error, toastInputFields: ErrorToastOptions) => void; diff --git a/src/plugins/data/public/index_patterns/utils.ts b/src/plugins/data/common/index_patterns/utils.ts similarity index 100% rename from src/plugins/data/public/index_patterns/utils.ts rename to src/plugins/data/common/index_patterns/utils.ts diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 301ff8d3f67d8a..1554ac71f8c552 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -443,6 +443,8 @@ export { getKbnTypeNames, } from '../common'; +export * from '../common/field_mapping'; + /* * Plugin setup */ diff --git a/src/plugins/data/public/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index.ts index 58c2cae1de0f38..0a8397467807c6 100644 --- a/src/plugins/data/public/index_patterns/index.ts +++ b/src/plugins/data/public/index_patterns/index.ts @@ -25,10 +25,14 @@ export { validateIndexPattern, getFromSavedObject, isDefault, -} from './lib'; -export { flattenHitWrapper, formatHitProvider } from './index_patterns'; +} from '../../common/index_patterns/lib'; +export { flattenHitWrapper, formatHitProvider, onRedirectNoIndexPattern } from './index_patterns'; -export { getIndexPatternFieldListCreator, Field, IIndexPatternFieldList } from './fields'; +export { + getIndexPatternFieldListCreator, + Field, + IIndexPatternFieldList, +} from '../../common/index_patterns'; // TODO: figure out how to replace IndexPatterns in get_inner_angular. export { diff --git a/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx b/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx deleted file mode 100644 index 2088bd8c925df9..00000000000000 --- a/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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 { contains } from 'lodash'; -import React from 'react'; -import { History } from 'history'; -import { i18n } from '@kbn/i18n'; -import { EuiCallOut } from '@elastic/eui'; -import { CoreStart } from 'kibana/public'; -import { toMountPoint } from '../../../../kibana_react/public'; -import { IndexPatternsContract } from './index_patterns'; - -export type EnsureDefaultIndexPattern = (history: History) => Promise | undefined; - -export const createEnsureDefaultIndexPattern = (core: CoreStart) => { - let bannerId: string; - let timeoutId: NodeJS.Timeout | undefined; - - /** - * Checks whether a default index pattern is set and exists and defines - * one otherwise. - * - * If there are no index patterns, redirect to management page and show - * banner. In this case the promise returned from this function will never - * resolve to wait for the URL change to happen. - */ - return async function ensureDefaultIndexPattern(this: IndexPatternsContract, history: History) { - const patterns = await this.getIds(); - let defaultId = core.uiSettings.get('defaultIndex'); - let defined = !!defaultId; - const exists = contains(patterns, defaultId); - - if (defined && !exists) { - core.uiSettings.remove('defaultIndex'); - defaultId = defined = false; - } - - if (defined) { - return; - } - - // If there is any index pattern created, set the first as default - if (patterns.length >= 1) { - defaultId = patterns[0]; - core.uiSettings.set('defaultIndex', defaultId); - } else { - const canManageIndexPatterns = core.application.capabilities.management.kibana.index_patterns; - const redirectTarget = canManageIndexPatterns ? '/management/kibana/indexPatterns' : '/home'; - - if (timeoutId) { - clearTimeout(timeoutId); - } - - const bannerMessage = i18n.translate( - 'data.indexPatterns.ensureDefaultIndexPattern.bannerLabel', - { - defaultMessage: - "In order to visualize and explore data in Kibana, you'll need to create an index pattern to retrieve data from Elasticsearch.", - } - ); - - // Avoid being hostile to new users who don't have an index pattern setup yet - // give them a friendly info message instead of a terse error message - bannerId = core.overlays.banners.replace( - bannerId, - toMountPoint() - ); - - // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around - timeoutId = setTimeout(() => { - core.overlays.banners.remove(bannerId); - timeoutId = undefined; - }, 15000); - - if (redirectTarget === '/home') { - core.application.navigateToApp('home'); - } else { - core.application.navigateToApp('management', { - path: `/kibana/indexPatterns?bannerMessage=${bannerMessage}`, - }); - } - - // return never-resolving promise to stop resolving and wait for the url change - return new Promise(() => {}); - } - }; -}; diff --git a/src/plugins/data/public/index_patterns/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index_patterns/index.ts index fca82025cdc66d..0db1c8c68b4ac6 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index.ts @@ -17,9 +17,5 @@ * under the License. */ -export * from './flatten_hit'; -export * from './format_hit'; -export * from './index_pattern'; -export * from './index_patterns'; -export * from './index_patterns_api_client'; -export * from './types'; +export * from '../../../common/index_patterns/index_patterns'; +export * from './redirect_no_index_pattern'; diff --git a/src/plugins/data/public/index_patterns/index_patterns/redirect_no_index_pattern.tsx b/src/plugins/data/public/index_patterns/index_patterns/redirect_no_index_pattern.tsx new file mode 100644 index 00000000000000..e32a8e023cf402 --- /dev/null +++ b/src/plugins/data/public/index_patterns/index_patterns/redirect_no_index_pattern.tsx @@ -0,0 +1,69 @@ +/* + * 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 { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { CoreStart } from 'kibana/public'; +import { toMountPoint } from '../../../../kibana_react/public'; + +let bannerId: string; + +export const onRedirectNoIndexPattern = ( + capabilities: CoreStart['application']['capabilities'], + navigateToApp: CoreStart['application']['navigateToApp'], + overlays: CoreStart['overlays'] +) => () => { + const canManageIndexPatterns = capabilities.management.kibana.index_patterns; + const redirectTarget = canManageIndexPatterns ? '/management/kibana/indexPatterns' : '/home'; + let timeoutId: NodeJS.Timeout | undefined; + + if (timeoutId) { + clearTimeout(timeoutId); + } + + const bannerMessage = i18n.translate('data.indexPatterns.ensureDefaultIndexPattern.bannerLabel', { + defaultMessage: + "In order to visualize and explore data in Kibana, you'll need to create an index pattern to retrieve data from Elasticsearch.", + }); + + // Avoid being hostile to new users who don't have an index pattern setup yet + // give them a friendly info message instead of a terse error message + bannerId = overlays.banners.replace( + bannerId, + toMountPoint() + ); + + // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around + timeoutId = setTimeout(() => { + overlays.banners.remove(bannerId); + timeoutId = undefined; + }, 15000); + + if (redirectTarget === '/home') { + navigateToApp('home'); + } else { + navigateToApp('management', { + path: `/kibana/indexPatterns?bannerMessage=${bannerMessage}`, + }); + } + + // return never-resolving promise to stop resolving and wait for the url change + return new Promise(() => {}); +}; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 06b5cbdfdfdfb5..baac2cf0d7a77c 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -40,7 +40,7 @@ import { SearchService } from './search/search_service'; import { FieldFormatsService } from './field_formats'; import { QueryService } from './query'; import { createIndexPatternSelect } from './ui/index_pattern_select'; -import { IndexPatternsService } from './index_patterns'; +import { IndexPatternsService, onRedirectNoIndexPattern } from './index_patterns'; import { setFieldFormats, setHttp, @@ -154,7 +154,7 @@ export class DataPublicPlugin implements Plugin { + notifications.toasts.add(toastInputFields); + }, + notifications.toasts.addError, + onRedirectNoIndexPattern(application.capabilities, application.navigateToApp, overlays) + ); setIndexPatterns(indexPatterns); const query = this.queryService.start(savedObjects); diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index bd3ec0d3f22946..df4c60dd44fe0d 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -15,6 +15,7 @@ import { CoreSetup } from 'src/core/public'; import { CoreStart } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'src/core/public'; import { Ensure } from '@kbn/utility-types'; +import { ErrorToastOptions } from 'src/core/public/notifications'; import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiComboBoxProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; @@ -53,6 +54,7 @@ import { SearchResponse as SearchResponse_2 } from 'elasticsearch'; import { SimpleSavedObject } from 'src/core/public'; import { Subscription } from 'rxjs'; import { Toast } from 'kibana/public'; +import { ToastInputFields } from 'src/core/public/notifications'; import { ToastsStart } from 'kibana/public'; import { TypeOf } from '@kbn/config-schema'; import { UiActionsSetup } from 'src/plugins/ui_actions/public'; @@ -421,6 +423,11 @@ export type ExistsFilter = Filter & { exists?: FilterExistsProperty; }; +// Warning: (ae-forgotten-export) The symbol "ShorthandFieldMapObject" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export const expandShorthand: (sh: Record) => Record; + // Warning: (ae-forgotten-export) The symbol "SavedObjectReference" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "extractReferences" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -547,6 +554,16 @@ export type FieldFormatsContentType = 'html' | 'text'; // @public (undocumented) export type FieldFormatsGetConfigFn = (key: string, defaultOverride?: T) => T; +// @public (undocumented) +export interface FieldMappingSpec { + // (undocumented) + _deserialize?: (mapping: string) => any | undefined; + // (undocumented) + _serialize?: (mapping: any) => string | undefined; + // (undocumented) + type: ES_FIELD_TYPES; +} + // Warning: (ae-missing-release-tag) "Filter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -624,7 +641,7 @@ export function getEsPreference(uiSettings: IUiSettingsClient_2, sessionId?: str // Warning: (ae-missing-release-tag) "getIndexPatternFieldListCreator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const getIndexPatternFieldListCreator: ({ fieldFormats, toastNotifications, }: FieldListDependencies) => CreateIndexPatternFieldList; +export const getIndexPatternFieldListCreator: ({ fieldFormats, onNotification, }: FieldListDependencies) => CreateIndexPatternFieldList; // Warning: (ae-missing-release-tag) "getKbnTypeNames" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -861,7 +878,10 @@ export type IMetricAggType = MetricAggType; export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IIndexPatternsApiClient" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "PatternCache" needs to be exported by the entry point index.d.ts - constructor(id: string | undefined, getConfig: any, savedObjectsClient: SavedObjectsClientContract, apiClient: IIndexPatternsApiClient, patternCache: PatternCache); + // Warning: (ae-forgotten-export) The symbol "FieldFormatsStartCommon" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "OnNotification" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "OnError" needs to be exported by the entry point index.d.ts + constructor(id: string | undefined, getConfig: any, savedObjectsClient: SavedObjectsClientContract, apiClient: IIndexPatternsApiClient, patternCache: PatternCache, fieldFormats: FieldFormatsStartCommon, onNotification: OnNotification, onError: OnError); // (undocumented) [key: string]: any; // (undocumented) @@ -989,7 +1009,7 @@ export class IndexPatternField implements IFieldType { // (undocumented) $$spec: FieldSpec; // Warning: (ae-forgotten-export) The symbol "FieldDependencies" needs to be exported by the entry point index.d.ts - constructor(indexPattern: IndexPattern, spec: FieldSpec | IndexPatternField, shortDotsEnable: boolean, { fieldFormats, toastNotifications }: FieldDependencies); + constructor(indexPattern: IIndexPattern, spec: FieldSpec | IndexPatternField, shortDotsEnable: boolean, { fieldFormats, onNotification }: FieldDependencies); // (undocumented) aggregatable?: boolean; // (undocumented) @@ -1005,7 +1025,7 @@ export class IndexPatternField implements IFieldType { // (undocumented) format: any; // (undocumented) - indexPattern?: IndexPattern; + indexPattern?: IIndexPattern; // (undocumented) lang?: string; // (undocumented) @@ -1214,6 +1234,9 @@ export interface KueryNode { type: keyof NodeTypes; } +// @public (undocumented) +export type MappingObject = Record; + // Warning: (ae-missing-release-tag) "MatchAllFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index 6eb4f82a940b1e..20e3fdae5ce5f9 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -24,7 +24,7 @@ import { Required } from '@kbn/utility-types'; import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui'; import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; -import { getTitle } from '../../index_patterns/lib'; +import { getTitle } from '../../../common/index_patterns/lib'; export type IndexPatternSelectProps = Required< // Omit, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions' | 'append' | 'prepend' | 'sortMatchesBy'>, diff --git a/src/plugins/data/public/index_patterns/fields/index.ts b/src/plugins/es_ui_shared/common/index.ts similarity index 89% rename from src/plugins/data/public/index_patterns/fields/index.ts rename to src/plugins/es_ui_shared/common/index.ts index 1644e23a163a63..c13dff5cf1fa19 100644 --- a/src/plugins/data/public/index_patterns/fields/index.ts +++ b/src/plugins/es_ui_shared/common/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export * from './field_list'; -export * from './field'; +export { Privileges, MissingPrivileges } from '../__packages_do_not_import__/authorization'; diff --git a/src/plugins/es_ui_shared/public/authorization/index.ts b/src/plugins/es_ui_shared/public/authorization/index.ts index 3a02c0d2694f3d..e18ab32ffdfa86 100644 --- a/src/plugins/es_ui_shared/public/authorization/index.ts +++ b/src/plugins/es_ui_shared/public/authorization/index.ts @@ -17,4 +17,14 @@ * under the License. */ -export * from '../../__packages_do_not_import__/authorization'; +export { + AuthorizationContext, + AuthorizationProvider, + Error, + MissingPrivileges, + NotAuthorizedSection, + Privileges, + SectionError, + useAuthorizationContext, + WithPrivileges, +} from '../../__packages_do_not_import__/authorization'; diff --git a/src/plugins/kibana_utils/public/errors/errors.test.ts b/src/plugins/kibana_utils/common/errors/errors.test.ts similarity index 100% rename from src/plugins/kibana_utils/public/errors/errors.test.ts rename to src/plugins/kibana_utils/common/errors/errors.test.ts diff --git a/src/plugins/kibana_utils/public/errors/errors.ts b/src/plugins/kibana_utils/common/errors/errors.ts similarity index 100% rename from src/plugins/kibana_utils/public/errors/errors.ts rename to src/plugins/kibana_utils/common/errors/errors.ts diff --git a/src/plugins/kibana_utils/public/errors/index.ts b/src/plugins/kibana_utils/common/errors/index.ts similarity index 100% rename from src/plugins/kibana_utils/public/errors/index.ts rename to src/plugins/kibana_utils/common/errors/index.ts diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index 87b625ef9a64f5..99daed98dbe640 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -22,6 +22,7 @@ export * from './of'; export * from './ui'; export * from './state_containers'; export * from './typed_json'; +export * from './errors'; export { createGetterSetter, Get, Set } from './create_getter_setter'; export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; export { url } from './url'; diff --git a/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx b/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx index cffb7ff4549a84..d8121b656deeb5 100644 --- a/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx +++ b/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx @@ -24,7 +24,7 @@ import ReactDOM from 'react-dom'; import ReactMarkdown from 'react-markdown'; import { ApplicationStart, HttpStart, ToastsSetup } from 'kibana/public'; -import { SavedObjectNotFound } from '../errors'; +import { SavedObjectNotFound } from '..'; interface Mapping { [key: string]: string | { app: string; path: string }; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 3d8a4414de70c8..6f61e2c228970d 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -34,8 +34,7 @@ export { defaultFeedbackMessage, } from '../common'; export * from './core'; -export * from './errors'; -export * from './field_mapping'; +export * from '../common/errors'; export * from './field_wildcard'; export * from './parse'; export * from './render_complete'; diff --git a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts index 9d0e25132271c3..47390c7dc9104a 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts @@ -18,11 +18,12 @@ */ import _ from 'lodash'; import { EsResponse, SavedObject, SavedObjectConfig, SavedObjectKibanaServices } from '../../types'; -import { expandShorthand, SavedObjectNotFound } from '../../../../kibana_utils/public'; +import { SavedObjectNotFound } from '../../../../kibana_utils/public'; import { IndexPattern, injectSearchSourceReferences, parseSearchSourceJSON, + expandShorthand, } from '../../../../data/public'; /** diff --git a/src/plugins/saved_objects/public/saved_object/helpers/check_for_duplicate_title.ts b/src/plugins/saved_objects/public/saved_object/helpers/check_for_duplicate_title.ts index 8895336aa9ffa1..0313b7978c5ab4 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/check_for_duplicate_title.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/check_for_duplicate_title.ts @@ -31,10 +31,13 @@ import { displayDuplicateTitleConfirmModal } from './display_duplicate_title_con * @param services */ export async function checkForDuplicateTitle( - savedObject: SavedObject, + savedObject: Pick< + SavedObject, + 'id' | 'title' | 'getDisplayName' | 'lastSavedTitle' | 'copyOnSave' | 'getEsType' + >, isTitleDuplicateConfirmed: boolean, onTitleDuplicate: (() => void) | undefined, - services: SavedObjectKibanaServices + services: Pick ): Promise { const { savedObjectsClient, overlays } = services; // Don't check for duplicates if user has already confirmed save with duplicate title diff --git a/src/plugins/saved_objects/public/saved_object/helpers/display_duplicate_title_confirm_modal.ts b/src/plugins/saved_objects/public/saved_object/helpers/display_duplicate_title_confirm_modal.ts index 0b02977830fda1..1b9e6fb6e996fd 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/display_duplicate_title_confirm_modal.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/display_duplicate_title_confirm_modal.ts @@ -23,7 +23,7 @@ import { confirmModalPromise } from './confirm_modal_promise'; import { SavedObject } from '../../types'; export function displayDuplicateTitleConfirmModal( - savedObject: SavedObject, + savedObject: Pick, overlays: OverlayStart ): Promise { const confirmMessage = i18n.translate( diff --git a/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts b/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts index acb371b8af9c2c..24e467ad18ac4b 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts @@ -18,8 +18,7 @@ */ import _ from 'lodash'; import { SavedObject, SavedObjectConfig } from '../../types'; -import { expandShorthand } from '../../../../kibana_utils/public'; -import { extractSearchSourceReferences } from '../../../../data/public'; +import { extractSearchSourceReferences, expandShorthand } from '../../../../data/public'; export function serializeSavedObject(savedObject: SavedObject, config: SavedObjectConfig) { // mapping definition for the fields that this object will expose diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index 707e65d60870c3..f7d5cc760f1bbf 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -197,138 +197,6 @@ exports[`SavedObjectsTable export should allow the user to choose when exporting `; -exports[`SavedObjectsTable import should show the flyout 1`] = ` - -`; - -exports[`SavedObjectsTable relationships should show the flyout 1`] = ` - -`; - exports[`SavedObjectsTable should render normally 1`] = ` { component.instance().showImportFlyout(); component.update(); - expect(component.find(Flyout)).toMatchSnapshot(); + expect(component.find(Flyout).length).toBe(1); }); it('should hide the flyout', async () => { @@ -450,7 +450,7 @@ describe('SavedObjectsTable', () => { } as SavedObjectWithMetadata); component.update(); - expect(component.find(Relationships)).toMatchSnapshot(); + expect(component.find(Relationships).length).toBe(1); expect(component.state('relationshipObject')).toEqual({ id: '2', type: 'search', diff --git a/src/plugins/usage_collection/server/report/store_report.test.ts b/src/plugins/usage_collection/server/report/store_report.test.ts index 29b6d79cc139a8..d8327eb834e12c 100644 --- a/src/plugins/usage_collection/server/report/store_report.test.ts +++ b/src/plugins/usage_collection/server/report/store_report.test.ts @@ -20,7 +20,7 @@ import { savedObjectsRepositoryMock } from '../../../../core/server/mocks'; import { storeReport } from './store_report'; import { ReportSchemaType } from './schema'; -import { METRIC_TYPE } from '../../public'; +import { METRIC_TYPE } from '@kbn/analytics'; describe('store_report', () => { test('stores report for all types of data', async () => { diff --git a/src/plugins/vis_type_timelion/server/series_functions/label.js b/src/plugins/vis_type_timelion/server/series_functions/label.js index 4d9a26c46aaaf2..b5282967a62e0e 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/label.js +++ b/src/plugins/vis_type_timelion/server/series_functions/label.js @@ -51,7 +51,10 @@ export default new Chainable('label', { const config = args.byName; return alter(args, function (eachSeries) { if (config.regex) { - eachSeries.label = eachSeries.label.replace(new RegExp(config.regex), config.label); + // not using a standard `import` so that if there's an issue with the re2 native module + // that it doesn't prevent Kibana from starting up and we only have an issue using Timelion labels + const RE2 = require('re2'); + eachSeries.label = eachSeries.label.replace(new RE2(config.regex), config.label); } else { eachSeries.label = config.label; } diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts index 6f18cbc5026ecf..73e3360004e5a7 100644 --- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts +++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts @@ -25,6 +25,7 @@ export interface VisualizationListItem { stage: 'experimental' | 'beta' | 'production'; savedObjectType: string; title: string; + description?: string; typeTitle: string; } diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 74553bbde0cd6e..4bf03828bb461b 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -9,7 +9,7 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector return { rootDir, roots: ['/plugins', '/legacy/plugins', '/legacy/server'], - moduleFileExtensions: ['js', 'json', 'ts', 'tsx'], + moduleFileExtensions: ['js', 'json', 'ts', 'tsx', 'node'], moduleNameMapper: { '@elastic/eui$': `${kibanaDirectory}/node_modules/@elastic/eui/test-env`, '@elastic/eui/lib/(.*)?': `${kibanaDirectory}/node_modules/@elastic/eui/test-env/$1`, diff --git a/x-pack/package.json b/x-pack/package.json index fb708ab09d8416..680f7001907266 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -36,6 +36,7 @@ "@kbn/es": "1.0.0", "@kbn/expect": "1.0.0", "@kbn/plugin-helpers": "9.0.2", + "@kbn/storybook": "1.0.0", "@kbn/test": "1.0.0", "@kbn/utility-types": "1.0.0", "@storybook/addon-actions": "^5.2.6", @@ -189,7 +190,7 @@ "@babel/runtime": "^7.9.2", "@elastic/apm-rum-react": "^1.1.1", "@elastic/datemath": "5.0.3", - "@elastic/ems-client": "7.8.0", + "@elastic/ems-client": "7.9.3", "@elastic/eui": "24.1.0", "@elastic/filesaver": "1.1.2", "@elastic/maki": "6.3.0", @@ -199,7 +200,6 @@ "@kbn/config-schema": "1.0.0", "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", - "@kbn/storybook": "1.0.0", "@kbn/ui-framework": "1.0.0", "@mapbox/geojson-rewind": "^0.4.1", "@mapbox/mapbox-gl-draw": "^1.1.2", diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 50dafba00a7e48..a2d64c94ce007c 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -11,7 +11,7 @@ import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { KibanaRequest } from '../../../../src/core/server'; import { loggingServiceMock, savedObjectsClientMock } from '../../../../src/core/server/mocks'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; -import { AuthenticatedUser } from '../../security/public'; +import { AuthenticatedUser } from '../../../plugins/security/common/model'; import { securityMock } from '../../security/server/mocks'; import { actionsMock } from '../../actions/server/mocks'; diff --git a/x-pack/plugins/apm/common/ml_job_constants.ts b/x-pack/plugins/apm/common/ml_job_constants.ts index 6df0d324981a16..0f0add7c4226b4 100644 --- a/x-pack/plugins/apm/common/ml_job_constants.ts +++ b/x-pack/plugins/apm/common/ml_job_constants.ts @@ -11,6 +11,8 @@ export enum severity { warning = 'warning', } +export const APM_ML_JOB_GROUP_NAME = 'apm'; + export function getMlPrefix(serviceName: string, transactionType?: string) { const maybeTransactionType = transactionType ? `${transactionType}-` : ''; return encodeForMlApi(`${serviceName}-${maybeTransactionType}`); diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 43f3585d0ebb2e..7d7a7811eeba2c 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -34,6 +34,16 @@ export interface Connection { destination: ConnectionNode; } +export interface ServiceAnomaly { + anomaly_score: number; + anomaly_severity: string; + actual_value: number; + typical_value: number; + ml_job_id: string; +} + +export type ServiceNode = ConnectionNode & Partial; + export interface ServiceNodeMetrics { avgMemoryUsage: number | null; avgCpuUsage: number | null; diff --git a/x-pack/plugins/apm/common/utils/left_join.ts b/x-pack/plugins/apm/common/utils/left_join.ts new file mode 100644 index 00000000000000..f3c4e48df755b0 --- /dev/null +++ b/x-pack/plugins/apm/common/utils/left_join.ts @@ -0,0 +1,21 @@ +/* + * 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 { Assign, Omit } from 'utility-types'; + +export function leftJoin< + TL extends object, + K extends keyof TL, + TR extends Pick +>(leftRecords: TL[], matchKey: K, rightRecords: TR[]) { + const rightLookup = new Map( + rightRecords.map((record) => [record[matchKey], record]) + ); + return leftRecords.map((record) => { + const matchProp = (record[matchKey] as unknown) as TR[K]; + const matchingRightRecord = rightLookup.get(matchProp); + return { ...record, ...matchingRightRecord }; + }) as Array>>>; +} diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 2de3c9c97065d2..1b8e7c4dc54318 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -17,7 +17,8 @@ "actions", "alerts", "observability", - "security" + "security", + "ml" ], "server": true, "ui": true, diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index d9254b487d037d..ff68288916af47 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -9,24 +9,15 @@ import { EuiFlexItem, EuiHorizontalRule, EuiTitle, - EuiIconTip, - EuiHealth, } from '@elastic/eui'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { i18n } from '@kbn/i18n'; import cytoscape from 'cytoscape'; import React, { MouseEvent } from 'react'; -import styled from 'styled-components'; -import { fontSize, px } from '../../../../style/variables'; import { Buttons } from './Buttons'; import { Info } from './Info'; import { ServiceMetricFetcher } from './ServiceMetricFetcher'; -import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; -import { getSeverityColor } from '../cytoscapeOptions'; -import { asInteger } from '../../../../utils/formatters'; -import { getMetricChangeDescription } from '../../../../../../ml/public'; - -const popoverMinWidth = 280; +import { AnomalyDetection } from './anomaly_detection'; +import { ServiceNode } from '../../../../../common/service_map'; +import { popoverMinWidth } from '../cytoscapeOptions'; interface ContentsProps { isService: boolean; @@ -36,31 +27,6 @@ interface ContentsProps { selectedNodeServiceName: string; } -const HealthStatusTitle = styled(EuiTitle)` - display: inline; - text-transform: uppercase; -`; - -const VerticallyCentered = styled.div` - display: flex; - align-items: center; -`; - -const SubduedText = styled.span` - color: ${theme.euiTextSubduedColor}; -`; - -const EnableText = styled.section` - color: ${theme.euiTextSubduedColor}; - line-height: 1.4; - font-size: ${fontSize}; - width: ${px(popoverMinWidth)}; -`; - -export const ContentLine = styled.section` - line-height: 2; -`; - // IE 11 does not handle flex properties as expected. With browser detection, // we can use regular div elements to render contents that are almost identical. // @@ -85,37 +51,6 @@ const FlexColumnGroup = (props: { const FlexColumnItem = (props: { children: React.ReactNode }) => isIE11 ?
: ; -const ANOMALY_DETECTION_TITLE = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverTitle', - { defaultMessage: 'Anomaly Detection' } -); - -const ANOMALY_DETECTION_TOOLTIP = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverTooltip', - { - defaultMessage: - 'Service health indicators are powered by the anomaly detection feature in machine learning', - } -); - -const ANOMALY_DETECTION_SCORE_METRIC = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverScoreMetric', - { defaultMessage: 'Score (max.)' } -); - -const ANOMALY_DETECTION_LINK = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverLink', - { defaultMessage: 'View anomalies' } -); - -const ANOMALY_DETECTION_DISABLED_TEXT = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverDisabled', - { - defaultMessage: - 'Display service health indicators by enabling anomaly detection from the Integrations menu in the Service details view.', - } -); - export function Contents({ selectedNodeData, isService, @@ -123,23 +58,6 @@ export function Contents({ onFocusClick, selectedNodeServiceName, }: ContentsProps) { - // Anomaly Detection - const severity = selectedNodeData.severity; - const maxScore = selectedNodeData.max_score; - const actualValue = selectedNodeData.actual_value; - const typicalValue = selectedNodeData.typical_value; - const jobId = selectedNodeData.job_id; - const hasAnomalyDetection = [ - severity, - maxScore, - actualValue, - typicalValue, - jobId, - ].every((value) => value !== undefined); - const anomalyDescription = hasAnomalyDetection - ? getMetricChangeDescription(actualValue, typicalValue).message - : null; - return ( {isService && ( - {hasAnomalyDetection ? ( - <> -
- -

{ANOMALY_DETECTION_TITLE}

-
-   - -
- - - - - - - {ANOMALY_DETECTION_SCORE_METRIC} - - - - -
- {asInteger(maxScore)} -  ({anomalyDescription}) -
-
-
-
- - - {ANOMALY_DETECTION_LINK} - - - - ) : ( - <> - -

{ANOMALY_DETECTION_TITLE}

-
- {ANOMALY_DETECTION_DISABLED_TEXT} - - )} +
)} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/anomaly_detection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/anomaly_detection.tsx new file mode 100644 index 00000000000000..ad4dc2ced2bfbb --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/anomaly_detection.tsx @@ -0,0 +1,156 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React from 'react'; +import styled from 'styled-components'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiIconTip, + EuiHealth, +} from '@elastic/eui'; +import { fontSize, px } from '../../../../style/variables'; +import { asInteger } from '../../../../utils/formatters'; +import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; +import { getSeverityColor, popoverMinWidth } from '../cytoscapeOptions'; +import { getMetricChangeDescription } from '../../../../../../ml/public'; +import { ServiceNode } from '../../../../../common/service_map'; + +const HealthStatusTitle = styled(EuiTitle)` + display: inline; + text-transform: uppercase; +`; + +const VerticallyCentered = styled.div` + display: flex; + align-items: center; +`; + +const SubduedText = styled.span` + color: ${theme.euiTextSubduedColor}; +`; + +const EnableText = styled.section` + color: ${theme.euiTextSubduedColor}; + line-height: 1.4; + font-size: ${fontSize}; + width: ${px(popoverMinWidth)}; +`; + +export const ContentLine = styled.section` + line-height: 2; +`; + +interface AnomalyDetectionProps { + serviceNodeData: cytoscape.NodeDataDefinition & ServiceNode; +} + +export function AnomalyDetection({ serviceNodeData }: AnomalyDetectionProps) { + const anomalySeverity = serviceNodeData.anomaly_severity; + const anomalyScore = serviceNodeData.anomaly_score; + const actualValue = serviceNodeData.actual_value; + const typicalValue = serviceNodeData.typical_value; + const mlJobId = serviceNodeData.ml_job_id; + const hasAnomalyDetectionScore = + anomalySeverity !== undefined && anomalyScore !== undefined; + const anomalyDescription = + hasAnomalyDetectionScore && + actualValue !== undefined && + typicalValue !== undefined + ? getMetricChangeDescription(actualValue, typicalValue).message + : null; + + return ( + <> +
+ +

{ANOMALY_DETECTION_TITLE}

+
+   + + {!mlJobId && {ANOMALY_DETECTION_DISABLED_TEXT}} +
+ {hasAnomalyDetectionScore && ( + + + + + + {ANOMALY_DETECTION_SCORE_METRIC} + + + +
+ {getDisplayedAnomalyScore(anomalyScore as number)} + {anomalyDescription && ( +  ({anomalyDescription}) + )} +
+
+
+
+ )} + {mlJobId && !hasAnomalyDetectionScore && ( + {ANOMALY_DETECTION_NO_DATA_TEXT} + )} + {mlJobId && ( + + + {ANOMALY_DETECTION_LINK} + + + )} + + ); +} + +function getDisplayedAnomalyScore(score: number) { + if (score > 0 && score < 1) { + return '< 1'; + } + return asInteger(score); +} + +const ANOMALY_DETECTION_TITLE = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverTitle', + { defaultMessage: 'Anomaly Detection' } +); + +const ANOMALY_DETECTION_TOOLTIP = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverTooltip', + { + defaultMessage: + 'Service health indicators are powered by the anomaly detection feature in machine learning', + } +); + +const ANOMALY_DETECTION_SCORE_METRIC = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverScoreMetric', + { defaultMessage: 'Score (max.)' } +); + +const ANOMALY_DETECTION_LINK = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverLink', + { defaultMessage: 'View anomalies' } +); + +const ANOMALY_DETECTION_DISABLED_TEXT = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverDisabled', + { + defaultMessage: + 'Display service health indicators by enabling anomaly detection from the Integrations menu in the Service details view.', + } +); + +const ANOMALY_DETECTION_NO_DATA_TEXT = i18n.translate( + 'xpack.apm.serviceMap.anomalyDetectionPopoverNoData', + { + defaultMessage: `We couldn't find an anomaly score within the selected time range. See details in the anomaly explorer.`, + } +); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index 0e4666b7bff170..9b35b0b33a70d2 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -13,7 +13,9 @@ import { import { severity } from '../../../../common/ml_job_constants'; import { defaultIcon, iconForNode } from './icons'; -export const getSeverityColor = (nodeSeverity: string) => { +export const popoverMinWidth = 280; + +export const getSeverityColor = (nodeSeverity?: string) => { switch (nodeSeverity) { case severity.warning: return theme.euiColorVis0; @@ -27,24 +29,26 @@ export const getSeverityColor = (nodeSeverity: string) => { } }; -const getBorderColor = (el: cytoscape.NodeSingular) => { - const nodeSeverity = el.data('severity'); - const severityColor = getSeverityColor(nodeSeverity); - if (severityColor) { - return severityColor; +const getBorderColor: cytoscape.Css.MapperFunction< + cytoscape.NodeSingular, + string +> = (el: cytoscape.NodeSingular) => { + const hasAnomalyDetectionJob = el.data('ml_job_id') !== undefined; + const nodeSeverity = el.data('anomaly_severity'); + if (hasAnomalyDetectionJob) { + return getSeverityColor(nodeSeverity) || theme.euiColorMediumShade; } if (el.hasClass('primary') || el.selected()) { return theme.euiColorPrimary; - } else { - return theme.euiColorMediumShade; } + return theme.euiColorMediumShade; }; const getBorderStyle: cytoscape.Css.MapperFunction< cytoscape.NodeSingular, cytoscape.Css.LineStyle > = (el: cytoscape.NodeSingular) => { - const nodeSeverity = el.data('severity'); + const nodeSeverity = el.data('anomaly_severity'); if (nodeSeverity === severity.critical) { return 'double'; } else { @@ -53,7 +57,7 @@ const getBorderStyle: cytoscape.Css.MapperFunction< }; const getBorderWidth = (el: cytoscape.NodeSingular) => { - const nodeSeverity = el.data('severity'); + const nodeSeverity = el.data('anomaly_severity'); if (nodeSeverity === severity.minor || nodeSeverity === severity.major) { return 4; @@ -183,6 +187,7 @@ const style: cytoscape.Stylesheet[] = [ // actually "hidden" { selector: 'edge[isInverseEdge]', + // @ts-ignore style: { visibility: 'hidden' }, }, { diff --git a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx index fd107a087cacef..b457fb1092bc65 100644 --- a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx @@ -116,3 +116,8 @@ export function ErrorRateAlertTrigger(props: Props) { /> ); } + +// Default export is required for React.lazy loading +// +// eslint-disable-next-line import/no-default-export +export default ErrorRateAlertTrigger; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx index 5cea4696d60a70..473fd432c166af 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx @@ -175,3 +175,8 @@ export function TransactionDurationAlertTrigger(props: Props) { /> ); } + +// Default export is required for React.lazy loading +// +// eslint-disable-next-line import/no-default-export +export default TransactionDurationAlertTrigger; diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/CustomPlot.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/CustomPlot.stories.tsx new file mode 100644 index 00000000000000..48e83763cb9df6 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/CustomPlot.stories.tsx @@ -0,0 +1,37 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import React from 'react'; +// @ts-ignore +import CustomPlot from './'; + +storiesOf('shared/charts/CustomPlot', module).add( + 'with annotations but no data', + () => { + const annotations = [ + { + type: 'version', + id: '2020-06-10 04:36:31', + '@timestamp': 1591763925012, + text: '2020-06-10 04:36:31', + }, + { + type: 'version', + id: '2020-06-10 15:23:01', + '@timestamp': 1591802689233, + text: '2020-06-10 15:23:01', + }, + ]; + return ; + }, + { + info: { + source: false, + text: + "When a chart has no data but does have annotations, the annotations shouldn't show up at all.", + }, + } +); diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js index 050cb0639ee88a..e1ffec3a8d97f5 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js @@ -168,7 +168,7 @@ export class InnerCustomPlot extends PureComponent { tickFormatX={this.props.tickFormatX} /> - {this.state.showAnnotations && !isEmpty(annotations) && ( + {this.state.showAnnotations && !isEmpty(annotations) && !noHits && ( { this.setState(({ showAnnotations }) => ({ diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js index d906e7f5093c23..ad1d73f2b766b0 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js @@ -255,12 +255,28 @@ describe('when response has no data', () => { const onHover = jest.fn(); const onMouseLeave = jest.fn(); const onSelectionEnd = jest.fn(); + const annotations = [ + { + type: 'version', + id: '2020-06-10 04:36:31', + '@timestamp': 1591763925012, + text: '2020-06-10 04:36:31', + }, + { + type: 'version', + id: '2020-06-10 15:23:01', + '@timestamp': 1591802689233, + text: '2020-06-10 15:23:01', + }, + ]; + let wrapper; beforeEach(() => { const series = getEmptySeries(1451606400000, 1451610000000); wrapper = mount( { expect(wrapper.find('Tooltip').length).toEqual(0); }); + it('should not show annotations', () => { + expect(wrapper.find('AnnotationsPlot')).toHaveLength(0); + }); + it('should have correct markup', () => { expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 0939c51b166050..2f46e2090351b8 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -5,40 +5,38 @@ */ import { i18n } from '@kbn/i18n'; +import { lazy } from 'react'; +import { ConfigSchema } from '.'; import { AppMountParameters, CoreSetup, CoreStart, + DEFAULT_APP_CATEGORIES, Plugin, PluginInitializerContext, } from '../../../../src/core/public'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; - -import { - PluginSetupContract as AlertingPluginPublicSetup, - PluginStartContract as AlertingPluginPublicStart, -} from '../../alerts/public'; -import { FeaturesPluginSetup } from '../../features/public'; import { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../src/plugins/data/public'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { + PluginSetupContract as AlertingPluginPublicSetup, + PluginStartContract as AlertingPluginPublicStart, +} from '../../alerts/public'; +import { FeaturesPluginSetup } from '../../features/public'; import { LicensingPluginSetup } from '../../licensing/public'; import { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '../../triggers_actions_ui/public'; -import { ConfigSchema } from '.'; -import { createCallApmApi } from './services/rest/createCallApmApi'; -import { featureCatalogueEntry } from './featureCatalogueEntry'; import { AlertType } from '../common/alert_types'; -import { ErrorRateAlertTrigger } from './components/shared/ErrorRateAlertTrigger'; -import { TransactionDurationAlertTrigger } from './components/shared/TransactionDurationAlertTrigger'; +import { featureCatalogueEntry } from './featureCatalogueEntry'; +import { createCallApmApi } from './services/rest/createCallApmApi'; +import { createStaticIndexPattern } from './services/rest/index_pattern'; import { setHelpExtension } from './setHelpExtension'; import { toggleAppLinkInNav } from './toggleAppLinkInNav'; import { setReadonlyBadge } from './updateBadge'; -import { createStaticIndexPattern } from './services/rest/index_pattern'; export type ApmPluginSetup = void; export type ApmPluginStart = void; @@ -112,7 +110,9 @@ export class ApmPlugin implements Plugin { defaultMessage: 'Error rate', }), iconClass: 'bell', - alertParamsExpression: ErrorRateAlertTrigger, + alertParamsExpression: lazy(() => + import('./components/shared/ErrorRateAlertTrigger') + ), validate: () => ({ errors: [], }), @@ -125,7 +125,9 @@ export class ApmPlugin implements Plugin { defaultMessage: 'Transaction duration', }), iconClass: 'bell', - alertParamsExpression: TransactionDurationAlertTrigger, + alertParamsExpression: lazy(() => + import('./components/shared/TransactionDurationAlertTrigger') + ), validate: () => ({ errors: [], }), diff --git a/x-pack/plugins/apm/public/services/rest/ml.ts b/x-pack/plugins/apm/public/services/rest/ml.ts index 99c162bde02da4..47032501d9fbe1 100644 --- a/x-pack/plugins/apm/public/services/rest/ml.ts +++ b/x-pack/plugins/apm/public/services/rest/ml.ts @@ -11,6 +11,7 @@ import { TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { + APM_ML_JOB_GROUP_NAME, getMlJobId, getMlPrefix, encodeForMlApi, @@ -55,7 +56,7 @@ export async function startMLJob({ }) { const transactionIndices = await getTransactionIndices(); const groups = [ - 'apm', + APM_ML_JOB_GROUP_NAME, encodeForMlApi(serviceName), encodeForMlApi(transactionType), ]; diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index e529f4d4ab1ed1..5a4bc62b874863 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -59,6 +59,9 @@ function getMockRequest() { }, }, }, + plugins: { + ml: undefined, + }, } as unknown) as APMRequestHandlerContext & { core: { elasticsearch: { diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 34f3fd9b40bb02..c41dff79a916ab 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -36,6 +36,7 @@ function decodeUiFilters( export interface Setup { client: ESClient; internalClient: ESClient; + ml?: ReturnType; config: APMConfig; indices: ApmIndicesConfig; dynamicIndexPattern?: IIndexPattern; @@ -93,6 +94,7 @@ export async function setupRequest( internalClient: getESClient(context, request, { clientAsInternalUser: true, }), + ml: getMlSetup(context, request), config, dynamicIndexPattern, }; @@ -104,3 +106,16 @@ export async function setupRequest( ...coreSetupRequest, } as InferSetup; } + +function getMlSetup(context: APMRequestHandlerContext, request: KibanaRequest) { + if (!context.plugins.ml) { + return; + } + const ml = context.plugins.ml; + const mlClient = ml.mlClient.asScoped(request).callAsCurrentUser; + return { + mlSystem: ml.mlSystemProvider(mlClient, request), + anomalyDetectors: ml.anomalyDetectorsProvider(mlClient), + mlClient, + }; +} diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts index d788ae81a7db83..ea7cc9b1456965 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server'; -import { apmIndexPattern } from '../../../../../../src/plugins/apm_oss/server'; -import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../src/plugins/apm_oss/server'; +import { + apmIndexPattern, + APM_STATIC_INDEX_PATTERN_ID, +} from '../../../../../../src/plugins/apm_oss/server'; import { hasHistoricalAgentData } from '../services/get_services/has_historical_agent_data'; import { Setup } from '../helpers/setup_request'; import { APMRequestHandlerContext } from '../../routes/typings'; 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 new file mode 100644 index 00000000000000..7b26078d5ffbfc --- /dev/null +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { intersection } from 'lodash'; +import { leftJoin } from '../../../common/utils/left_join'; +import { Job as AnomalyDetectionJob } from '../../../../ml/server'; +import { PromiseReturnType } from '../../../typings/common'; +import { IEnvOptions } from './get_service_map'; +import { APM_ML_JOB_GROUP_NAME } from '../../../common/ml_job_constants'; + +type ApmMlJobCategory = NonNullable>; +const getApmMlJobCategory = ( + mlJob: AnomalyDetectionJob, + serviceNames: string[] +) => { + const apmJobGroups = mlJob.groups.filter( + (groupName) => groupName !== APM_ML_JOB_GROUP_NAME + ); + if (apmJobGroups.length === mlJob.groups.length) { + // ML job missing "apm" group name + return; + } + const [serviceName] = intersection(apmJobGroups, serviceNames); + if (!serviceName) { + // APM ML job service was not found + return; + } + const [transactionType] = apmJobGroups.filter( + (groupName) => groupName !== serviceName + ); + if (!transactionType) { + // APM ML job transaction type was not found. + return; + } + return { jobId: mlJob.job_id, serviceName, transactionType }; +}; + +export type ServiceAnomalies = PromiseReturnType; + +export async function getServiceAnomalies( + options: IEnvOptions, + serviceNames: string[] +) { + const { start, end, ml } = options.setup; + + if (!ml || serviceNames.length === 0) { + return []; + } + + const { jobs: apmMlJobs } = await ml.anomalyDetectors.jobs('apm'); + const apmMlJobCategories = apmMlJobs + .map((job) => getApmMlJobCategory(job, serviceNames)) + .filter( + (apmJobCategory) => apmJobCategory !== undefined + ) as ApmMlJobCategory[]; + const apmJobIds = apmMlJobs.map((job) => job.job_id); + const params = { + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { result_type: 'record' } }, + { + terms: { + job_id: apmJobIds, + }, + }, + { + range: { + timestamp: { gte: start, lte: end, format: 'epoch_millis' }, + }, + }, + ], + }, + }, + aggs: { + jobs: { + terms: { field: 'job_id', size: apmJobIds.length }, + aggs: { + top_score_hits: { + top_hits: { + sort: [{ record_score: { order: 'desc' as const } }], + _source: ['record_score', 'timestamp', 'typical', 'actual'], + size: 1, + }, + }, + }, + }, + }, + }, + }; + + const response = (await ml.mlSystem.mlAnomalySearch(params)) as { + aggregations: { + jobs: { + buckets: Array<{ + key: string; + top_score_hits: { + hits: { + hits: Array<{ + _source: { + record_score: number; + timestamp: number; + typical: number[]; + actual: number[]; + }; + }>; + }; + }; + }>; + }; + }; + }; + const anomalyScores = response.aggregations.jobs.buckets.map((jobBucket) => { + const jobId = jobBucket.key; + const bucketSource = jobBucket.top_score_hits.hits.hits?.[0]?._source; + return { + jobId, + anomalyScore: bucketSource.record_score, + timestamp: bucketSource.timestamp, + typical: bucketSource.typical[0], + actual: bucketSource.actual[0], + }; + }); + return leftJoin(apmMlJobCategories, 'jobId', anomalyScores); +} 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 47ba9ecc78ffca..9f3ded82d7cbd8 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 @@ -9,15 +9,18 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { getMlIndex } from '../../../common/ml_job_constants'; import { getServicesProjection } from '../../../common/projections/services'; import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { PromiseReturnType } from '../../../typings/common'; -import { rangeFilter } from '../helpers/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { transformServiceMapResponses } from './transform_service_map_responses'; +import { + transformServiceMapResponses, + getAllNodes, + getServiceNodes, +} from './transform_service_map_responses'; import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; import { getTraceSampleIds } from './get_trace_sample_ids'; +import { getServiceAnomalies, ServiceAnomalies } from './get_service_anomalies'; export interface IEnvOptions { setup: Setup & SetupTimeRange; @@ -129,58 +132,30 @@ async function getServicesData(options: IEnvOptions) { ); } -function getAnomaliesData(options: IEnvOptions) { - const { start, end, client } = options.setup; - const rangeQuery = { range: rangeFilter(start, end, 'timestamp') }; - - const params = { - index: getMlIndex('*'), - body: { - size: 0, - query: { - bool: { filter: [{ term: { result_type: 'record' } }, rangeQuery] }, - }, - aggs: { - jobs: { - terms: { field: 'job_id', size: 10 }, - aggs: { - top_score_hits: { - top_hits: { - sort: [{ record_score: { order: 'desc' as const } }], - _source: ['job_id', 'record_score', 'typical', 'actual'], - size: 1, - }, - }, - }, - }, - }, - }, - }; - - return client.search(params); -} - -export type AnomaliesResponse = PromiseReturnType; +export { ServiceAnomalies }; export type ConnectionsResponse = PromiseReturnType; export type ServicesResponse = PromiseReturnType; export type ServiceMapAPIResponse = PromiseReturnType; export async function getServiceMap(options: IEnvOptions) { - const [connectionData, servicesData, anomaliesData]: [ - // explicit types to avoid TS "excessively deep" error - ConnectionsResponse, - ServicesResponse, - AnomaliesResponse - // @ts-ignore - ] = await Promise.all([ + const [connectionData, servicesData] = await Promise.all([ getConnectionData(options), getServicesData(options), - getAnomaliesData(options), ]); + // Derive all related service names from connection and service data + const allNodes = getAllNodes(servicesData, connectionData.connections); + const serviceNodes = getServiceNodes(allNodes); + const serviceNames = serviceNodes.map( + (serviceData) => serviceData[SERVICE_NAME] + ); + + // Get related service anomalies + const serviceAnomalies = await getServiceAnomalies(options, serviceNames); + return transformServiceMapResponses({ ...connectionData, - anomalies: anomaliesData, + anomalies: serviceAnomalies, services: servicesData, }); } diff --git a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts index 6d2bd783e9cde1..f07b575cc0a35a 100644 --- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AnomaliesResponse } from './get_service_map'; +import { ServiceAnomalies } from './get_service_map'; import { addAnomaliesDataToNodes } from './ml_helpers'; describe('addAnomaliesDataToNodes', () => { @@ -22,76 +22,54 @@ describe('addAnomaliesDataToNodes', () => { }, ]; - const anomaliesResponse = { - aggregations: { - jobs: { - buckets: [ - { - key: 'opbeans-ruby-request-high_mean_response_time', - top_score_hits: { - hits: { - hits: [ - { - _source: { - record_score: 50, - actual: [2000], - typical: [1000], - job_id: 'opbeans-ruby-request-high_mean_response_time', - }, - }, - ], - }, - }, - }, - { - key: 'opbeans-java-request-high_mean_response_time', - top_score_hits: { - hits: { - hits: [ - { - _source: { - record_score: 100, - actual: [9000], - typical: [3000], - job_id: 'opbeans-java-request-high_mean_response_time', - }, - }, - ], - }, - }, - }, - ], - }, + const serviceAnomalies: ServiceAnomalies = [ + { + jobId: 'opbeans-ruby-request-high_mean_response_time', + serviceName: 'opbeans-ruby', + transactionType: 'request', + anomalyScore: 50, + timestamp: 1591351200000, + actual: 2000, + typical: 1000, + }, + { + jobId: 'opbeans-java-request-high_mean_response_time', + serviceName: 'opbeans-java', + transactionType: 'request', + anomalyScore: 100, + timestamp: 1591351200000, + actual: 9000, + typical: 3000, }, - }; + ]; const result = [ { 'service.name': 'opbeans-ruby', 'agent.name': 'ruby', 'service.environment': null, - max_score: 50, - severity: 'major', + anomaly_score: 50, + anomaly_severity: 'major', actual_value: 2000, typical_value: 1000, - job_id: 'opbeans-ruby-request-high_mean_response_time', + ml_job_id: 'opbeans-ruby-request-high_mean_response_time', }, { 'service.name': 'opbeans-java', 'agent.name': 'java', 'service.environment': null, - max_score: 100, - severity: 'critical', + anomaly_score: 100, + anomaly_severity: 'critical', actual_value: 9000, typical_value: 3000, - job_id: 'opbeans-java-request-high_mean_response_time', + ml_job_id: 'opbeans-java-request-high_mean_response_time', }, ]; expect( addAnomaliesDataToNodes( nodes, - (anomaliesResponse as unknown) as AnomaliesResponse + (serviceAnomalies as unknown) as ServiceAnomalies ) ).toEqual(result); }); diff --git a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts index 3289958733b2bd..8162417616b6cc 100644 --- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts +++ b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts @@ -5,65 +5,59 @@ */ import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; -import { - getMlJobServiceName, - getSeverity, -} from '../../../common/ml_job_constants'; -import { ConnectionNode } from '../../../common/service_map'; -import { AnomaliesResponse } from './get_service_map'; +import { getSeverity } from '../../../common/ml_job_constants'; +import { ConnectionNode, ServiceNode } from '../../../common/service_map'; +import { ServiceAnomalies } from './get_service_map'; export function addAnomaliesDataToNodes( nodes: ConnectionNode[], - anomaliesResponse: AnomaliesResponse + serviceAnomalies: ServiceAnomalies ) { - const anomaliesMap = ( - anomaliesResponse.aggregations?.jobs.buckets ?? [] - ).reduce<{ - [key: string]: { - max_score?: number; - actual_value?: number; - typical_value?: number; - job_id?: string; - }; - }>((previousValue, currentValue) => { - const key = getMlJobServiceName(currentValue.key.toString()); - const hitSource = currentValue.top_score_hits.hits.hits[0]._source as { - record_score: number; - actual: [number]; - typical: [number]; - job_id: string; - }; - const maxScore = hitSource.record_score; - const actualValue = hitSource.actual[0]; - const typicalValue = hitSource.typical[0]; - const jobId = hitSource.job_id; + const anomaliesMap = serviceAnomalies.reduce( + (acc, anomalyJob) => { + const serviceAnomaly: typeof acc[string] | undefined = + acc[anomalyJob.serviceName]; + const hasAnomalyJob = serviceAnomaly !== undefined; + const hasAnomalyScore = serviceAnomaly?.anomaly_score !== undefined; + const hasNewAnomalyScore = anomalyJob.anomalyScore !== undefined; + const hasNewMaxAnomalyScore = + hasNewAnomalyScore && + (!hasAnomalyScore || + (anomalyJob?.anomalyScore ?? 0) > + (serviceAnomaly?.anomaly_score ?? 0)); - if ((previousValue[key]?.max_score ?? 0) > maxScore) { - return previousValue; - } + if (!hasAnomalyJob || hasNewMaxAnomalyScore) { + acc[anomalyJob.serviceName] = { + anomaly_score: anomalyJob.anomalyScore, + actual_value: anomalyJob.actual, + typical_value: anomalyJob.typical, + ml_job_id: anomalyJob.jobId, + }; + } - return { - ...previousValue, - [key]: { - max_score: maxScore, - actual_value: actualValue, - typical_value: typicalValue, - job_id: jobId, - }, - }; - }, {}); + return acc; + }, + {} as { + [serviceName: string]: { + anomaly_score?: number; + actual_value?: number; + typical_value?: number; + ml_job_id: string; + }; + } + ); - const servicesDataWithAnomalies = nodes.map((service) => { - const serviceAnomalies = anomaliesMap[service[SERVICE_NAME]]; - if (serviceAnomalies) { - const maxScore = serviceAnomalies.max_score; + const servicesDataWithAnomalies: ServiceNode[] = nodes.map((service) => { + const serviceAnomaly = anomaliesMap[service[SERVICE_NAME]]; + if (serviceAnomaly) { + const anomalyScore = serviceAnomaly.anomaly_score; return { ...service, - max_score: maxScore, - severity: getSeverity(maxScore), - actual_value: serviceAnomalies.actual_value, - typical_value: serviceAnomalies.typical_value, - job_id: serviceAnomalies.job_id, + anomaly_score: anomalyScore, + anomaly_severity: getSeverity(anomalyScore), + actual_value: serviceAnomaly.actual_value, + typical_value: serviceAnomaly.typical_value, + ml_job_id: serviceAnomaly.ml_job_id, }; } return service; diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts index 0aa3f13b9b90cf..6c9880c2dc4dfb 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts @@ -12,7 +12,7 @@ import { SPAN_SUBTYPE, SPAN_TYPE, } from '../../../common/elasticsearch_fieldnames'; -import { AnomaliesResponse } from './get_service_map'; +import { ServiceAnomalies } from './get_service_map'; import { transformServiceMapResponses, ServiceMapResponse, @@ -36,14 +36,12 @@ const javaService = { [AGENT_NAME]: 'java', }; -const anomalies = ({ - aggregations: { jobs: { buckets: [] } }, -} as unknown) as AnomaliesResponse; +const serviceAnomalies: ServiceAnomalies = []; describe('transformServiceMapResponses', () => { it('maps external destinations to internal services', () => { const response: ServiceMapResponse = { - anomalies, + anomalies: serviceAnomalies, services: [nodejsService, javaService], discoveredServices: [ { @@ -75,7 +73,7 @@ describe('transformServiceMapResponses', () => { it('collapses external destinations based on span.destination.resource.name', () => { const response: ServiceMapResponse = { - anomalies, + anomalies: serviceAnomalies, services: [nodejsService, javaService], discoveredServices: [ { @@ -111,7 +109,7 @@ describe('transformServiceMapResponses', () => { it('picks the first span.type/subtype in an alphabetically sorted list', () => { const response: ServiceMapResponse = { - anomalies, + anomalies: serviceAnomalies, services: [javaService], discoveredServices: [], connections: [ @@ -150,7 +148,7 @@ describe('transformServiceMapResponses', () => { it('processes connections without a matching "service" aggregation', () => { const response: ServiceMapResponse = { - anomalies, + anomalies: serviceAnomalies, services: [javaService], discoveredServices: [], connections: [ diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts index 8580fed5875674..53abf54cbcf313 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts @@ -20,7 +20,7 @@ import { import { ConnectionsResponse, ServicesResponse, - AnomaliesResponse, + ServiceAnomalies, } from './get_service_map'; import { addAnomaliesDataToNodes } from './ml_helpers'; @@ -38,14 +38,10 @@ function getConnectionId(connection: Connection) { )}`; } -export type ServiceMapResponse = ConnectionsResponse & { - anomalies: AnomaliesResponse; - services: ServicesResponse; -}; - -export function transformServiceMapResponses(response: ServiceMapResponse) { - const { anomalies, discoveredServices, services, connections } = response; - +export function getAllNodes( + services: ServiceMapResponse['services'], + connections: ServiceMapResponse['connections'] +) { // Derive the rest of the map nodes from the connections and add the services // from the services data query const allNodes: ConnectionNode[] = connections @@ -58,11 +54,29 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { })) ); + return allNodes; +} + +export function getServiceNodes(allNodes: ConnectionNode[]) { // List of nodes that are services const serviceNodes = allNodes.filter( (node) => SERVICE_NAME in node ) as ServiceConnectionNode[]; + return serviceNodes; +} + +export type ServiceMapResponse = ConnectionsResponse & { + anomalies: ServiceAnomalies; + services: ServicesResponse; +}; + +export function transformServiceMapResponses(response: ServiceMapResponse) { + const { anomalies, discoveredServices, services, connections } = response; + + const allNodes = getAllNodes(services, connections); + const serviceNodes = getServiceNodes(allNodes); + // List of nodes that are externals const externalNodes = allNodes.filter( (node) => SPAN_DESTINATION_SERVICE_RESOURCE in node diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index d32d16d4c3cc83..f0a05dfc0df30c 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -31,10 +31,11 @@ import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_ import { LicensingPluginSetup } from '../../licensing/public'; import { registerApmAlerts } from './lib/alerts/register_apm_alerts'; import { createApmTelemetry } from './lib/apm_telemetry'; -import { PluginSetupContract as FeaturesPluginSetup } from '../../../plugins/features/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { APM_FEATURE } from './feature'; import { apmIndices, apmTelemetry } from './saved_objects'; import { createElasticCloudInstructions } from './tutorial/elastic_cloud'; +import { MlPluginSetup } from '../../ml/server'; export interface APMPluginSetup { config$: Observable; @@ -62,6 +63,7 @@ export class APMPlugin implements Plugin { observability?: ObservabilityPluginSetup; features: FeaturesPluginSetup; security?: SecurityPluginSetup; + ml?: MlPluginSetup; } ) { this.logger = this.initContext.logger.get(); @@ -126,6 +128,7 @@ export class APMPlugin implements Plugin { plugins: { observability: plugins.observability, security: plugins.security, + ml: plugins.ml, }, }); diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 05f52f1732c98f..bc31cb7a582af2 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -18,6 +18,7 @@ import { ObservabilityPluginSetup } from '../../../observability/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FetchOptions } from '../../public/services/rest/callApi'; import { SecurityPluginSetup } from '../../../security/public'; +import { MlPluginSetup } from '../../../ml/server'; import { APMConfig } from '..'; export interface Params { @@ -67,6 +68,7 @@ export type APMRequestHandlerContext< plugins: { observability?: ObservabilityPluginSetup; security?: SecurityPluginSetup; + ml?: MlPluginSetup; }; }; @@ -114,6 +116,7 @@ export interface ServerAPI { plugins: { observability?: ObservabilityPluginSetup; security?: SecurityPluginSetup; + ml?: MlPluginSetup; }; } ) => void; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js index 299f96ff1b4e8a..b3e735d2022208 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js @@ -41,8 +41,12 @@ ToggleArgInput.propTypes = { onValueChange: PropTypes.func.isRequired, argValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.string, PropTypes.object]).isRequired, argId: PropTypes.string.isRequired, - labelValue: PropTypes.string, - showLabelValue: PropTypes.bool, + typeInstance: PropTypes.shape({ + displayName: PropTypes.string.isRequired, + options: PropTypes.shape({ + labelValue: PropTypes.string.isRequired, + }), + }).isRequired, renderError: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/markdown.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/markdown.js index edae739ee0d3da..bebcc290a313d0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/markdown.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/markdown.js @@ -37,7 +37,7 @@ export const markdown = () => ({ argType: 'toggle', default: false, options: { - labelValue: 'Open all links in a new tab', + labelValue: strings.getOpenLinksInNewTabLabelName(), }, }, ], diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/pie.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/pie.js index f1b6a48d1e7b0e..6a32da2c3c0e93 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/pie.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/pie.js @@ -75,6 +75,9 @@ export const pie = () => ({ help: strings.getLabelsHelp(), argType: 'toggle', default: true, + options: { + labelValue: strings.getLabelsToggleSwitch(), + }, }, { name: 'seriesStyle', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/shape.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/shape.js index c3e97b4bd5dea9..5eb8b1deb219b1 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/shape.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/shape.js @@ -46,6 +46,9 @@ export const shape = () => ({ displayName: strings.getMaintainAspectDisplayName(), argType: 'toggle', help: strings.getMaintainAspectHelp(), + options: { + labelValue: strings.getMaintainAspectLabelName(), + }, }, ], }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/table.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/table.js index 73324feddcab0c..126559269f95a3 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/table.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/table.js @@ -33,6 +33,9 @@ export const table = () => ({ help: strings.getPaginateHelp(), argType: 'toggle', default: true, + options: { + labelValue: strings.getPaginateToggleSwitch(), + }, }, { name: 'showHeader', @@ -40,6 +43,9 @@ export const table = () => ({ help: strings.getShowHeaderHelp(), argType: 'toggle', default: true, + options: { + labelValue: strings.getShowHeaderToggleSwitch(), + }, }, ], }); diff --git a/x-pack/plugins/canvas/i18n/ui.ts b/x-pack/plugins/canvas/i18n/ui.ts index 1abe56c99dc89e..f69f9e747ab902 100644 --- a/x-pack/plugins/canvas/i18n/ui.ts +++ b/x-pack/plugins/canvas/i18n/ui.ts @@ -791,8 +791,12 @@ export const ViewStrings = { i18n.translate('xpack.canvas.uis.views.pie.args.labelsTitle', { defaultMessage: 'Labels', }), - getLabelsHelp: () => + getLabelsToggleSwitch: () => i18n.translate('xpack.canvas.uis.views.pie.args.labelsToggleSwitch', { + defaultMessage: 'Show labels', + }), + getLabelsHelp: () => + i18n.translate('xpack.canvas.uis.views.pie.args.labelsLabel', { defaultMessage: 'Show/hide labels', }), getLegendDisplayName: () => @@ -1075,10 +1079,14 @@ export const ViewStrings = { }), getMaintainAspectDisplayName: () => i18n.translate('xpack.canvas.uis.views.shape.args.maintainAspectTitle', { - defaultMessage: 'Fixed ratio', + defaultMessage: 'Aspect ratio settings', }), - getMaintainAspectHelp: () => + getMaintainAspectLabelName: () => i18n.translate('xpack.canvas.uis.views.shape.args.maintainAspectLabel', { + defaultMessage: 'Use a fixed ratio', + }), + getMaintainAspectHelp: () => + i18n.translate('xpack.canvas.uis.views.shape.args.maintainAspectHelpLabel', { defaultMessage: `Enable to maintain aspect ratio`, }), getShapeDisplayName: () => @@ -1099,6 +1107,10 @@ export const ViewStrings = { i18n.translate('xpack.canvas.uis.views.table.args.paginateTitle', { defaultMessage: 'Pagination', }), + getPaginateToggleSwitch: () => + i18n.translate('xpack.canvas.uis.views.table.args.paginateToggleSwitch', { + defaultMessage: 'Show pagination controls', + }), getPaginateHelp: () => i18n.translate('xpack.canvas.uis.views.table.args.paginateLabel', { defaultMessage: @@ -1116,6 +1128,10 @@ export const ViewStrings = { i18n.translate('xpack.canvas.uis.views.table.args.showHeaderTitle', { defaultMessage: 'Header', }), + getShowHeaderToggleSwitch: () => + i18n.translate('xpack.canvas.uis.views.table.args.showHeaderToggleSwitch', { + defaultMessage: 'Show the header row', + }), getShowHeaderHelp: () => i18n.translate('xpack.canvas.uis.views.table.args.showHeaderLabel', { defaultMessage: 'Show or hide the header row with titles for each column', diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx index dba97a15fee5c0..474b8f34949170 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx @@ -84,7 +84,7 @@ export const SavedElementsModal: FunctionComponent = ({ const handleEdit = async (name: string, description: string, image: string) => { if (elementToEdit) { - await updateCustomElement(elementToEdit.id, name, description, image); + updateCustomElement(elementToEdit.id, name, description, image); } hideEditModal(); }; @@ -94,7 +94,7 @@ export const SavedElementsModal: FunctionComponent = ({ const handleDelete = async () => { if (elementToDelete) { - await removeCustomElement(elementToDelete.id); + removeCustomElement(elementToDelete.id); } hideDeleteModal(); }; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.examples.tsx b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.examples.tsx index a0ab8d53485f56..8bbc3e09af4bf8 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.examples.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.examples.tsx @@ -7,6 +7,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; import { EditMenu } from '../edit_menu'; +import { PositionedElement } from '../../../../../types'; const handlers = { cutNodes: action('cutNodes'), @@ -41,11 +42,16 @@ storiesOf('components/WorkpadHeader/EditMenu', module) )) .add('single element selected', () => ( - + )) .add('single grouped element selected', () => ( ( ( { const pageId = getSelectedPage(state); const nodes = getNodes(state, pageId) as PositionedElement[]; const selectedToplevelNodes = getSelectedToplevelNodes(state); + const selectedPrimaryShapeObjects = selectedToplevelNodes .map((id: string) => nodes.find((s: PositionedElement) => s.id === id)) .filter((shape?: PositionedElement) => shape) as PositionedElement[]; + const selectedPersistentPrimaryNodes = flatten( selectedPrimaryShapeObjects.map((shape: PositionedElement) => nodes.find((n: PositionedElement) => n.id === shape.id) // is it a leaf or a persisted group? @@ -70,12 +72,18 @@ const mapStateToProps = (state: State) => { : nodes.filter((s: PositionedElement) => s.position.parent === shape.id).map((s) => s.id) ) ); - const selectedNodeIds = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes))); + + const selectedNodeIds: string[] = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes))); + const selectedNodes = selectedNodeIds + .map((id: string) => nodes.find((s) => s.id === id)) + .filter((node: PositionedElement | undefined): node is PositionedElement => { + return !!node; + }); return { pageId, selectedToplevelNodes, - selectedNodes: selectedNodeIds.map((id: string) => nodes.find((s) => s.id === id)), + selectedNodes, state, }; }; diff --git a/x-pack/plugins/canvas/public/lib/clone_subgraphs.js b/x-pack/plugins/canvas/public/lib/clone_subgraphs.ts similarity index 83% rename from x-pack/plugins/canvas/public/lib/clone_subgraphs.js rename to x-pack/plugins/canvas/public/lib/clone_subgraphs.ts index e4dfa1cefcaba4..c3a3933e06a6d7 100644 --- a/x-pack/plugins/canvas/public/lib/clone_subgraphs.js +++ b/x-pack/plugins/canvas/public/lib/clone_subgraphs.ts @@ -4,16 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +// @ts-ignore Untyped local import { arrayToMap } from './aeroelastic/functional'; import { getId } from './get_id'; +import { PositionedElement } from '../../types'; -export const cloneSubgraphs = (nodes) => { +export const cloneSubgraphs = (nodes: PositionedElement[]) => { const idMap = arrayToMap(nodes.map((n) => n.id)); + // We simultaneously provide unique id values for all elements (across all pages) // AND ensure that parent-child relationships are retained (via matching id values within page) Object.keys(idMap).forEach((key) => (idMap[key] = getId(key.split('-')[0]))); // new group names to which we can map + // must return elements in the same order, for several reasons - const newNodes = nodes.map((element) => ({ + return nodes.map((element) => ({ ...element, id: idMap[element.id], position: { @@ -21,5 +25,4 @@ export const cloneSubgraphs = (nodes) => { parent: element.position.parent ? idMap[element.position.parent] : null, }, })); - return newNodes; }; diff --git a/x-pack/plugins/canvas/public/lib/element_handler_creators.ts b/x-pack/plugins/canvas/public/lib/element_handler_creators.ts index d280c62888df00..a2bf5a62ec1f7f 100644 --- a/x-pack/plugins/canvas/public/lib/element_handler_creators.ts +++ b/x-pack/plugins/canvas/public/lib/element_handler_creators.ts @@ -37,7 +37,7 @@ export interface Props { /** * selects elements on the page */ - selectToplevelNodes: (elements: PositionedElement) => void; + selectToplevelNodes: (elements: PositionedElement[]) => void; /** * deletes elements from the page */ diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts index e6413ac03aeaef..a14cdfd035d2ec 100644 --- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts +++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts @@ -127,17 +127,19 @@ export class EmbeddableEnhancedPlugin }); dynamicActions.start().catch((error) => { - /* eslint-disable */ - console.log('Failed to start embeddable dynamic actions', embeddable); - console.error(error); + /* eslint-disable */ + + console.log('Failed to start embeddable dynamic actions', embeddable); + console.error(error); /* eslint-enable */ }); const stop = () => { dynamicActions.stop().catch((error) => { - /* eslint-disable */ - console.log('Failed to stop embeddable dynamic actions', embeddable); - console.error(error); + /* eslint-disable */ + + console.log('Failed to stop embeddable dynamic actions', embeddable); + console.error(error); /* eslint-enable */ }); }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts index 75d1b69eb61570..56d76da522ac22 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts @@ -27,6 +27,14 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadDataStreamsResponse = (response: HttpResponse = []) => { + server.respondWith('GET', `${API_BASE_PATH}/data_streams`, [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(response), + ]); + }; + const setDeleteTemplateResponse = (response: HttpResponse = []) => { server.respondWith('POST', `${API_BASE_PATH}/delete_index_templates`, [ 200, @@ -71,6 +79,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { return { setLoadTemplatesResponse, setLoadIndicesResponse, + setLoadDataStreamsResponse, setDeleteTemplateResponse, setLoadTemplateResponse, setCreateTemplateResponse, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts index 8e7755a65af3cf..f581083e28cc63 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts @@ -10,44 +10,4 @@ export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../.. export { setupEnvironment, WithAppDependencies, services } from './setup_environment'; -export type TestSubjects = - | 'aliasesTab' - | 'appTitle' - | 'cell' - | 'closeDetailsButton' - | 'createTemplateButton' - | 'createLegacyTemplateButton' - | 'deleteSystemTemplateCallOut' - | 'deleteTemplateButton' - | 'deleteTemplatesConfirmation' - | 'documentationLink' - | 'emptyPrompt' - | 'manageTemplateButton' - | 'mappingsTab' - | 'noAliasesCallout' - | 'noMappingsCallout' - | 'noSettingsCallout' - | 'indicesList' - | 'indicesTab' - | 'indexTableIncludeHiddenIndicesToggle' - | 'indexTableIndexNameLink' - | 'reloadButton' - | 'reloadIndicesButton' - | 'row' - | 'sectionError' - | 'sectionLoading' - | 'settingsTab' - | 'summaryTab' - | 'summaryTitle' - | 'systemTemplatesSwitch' - | 'templateDetails' - | 'templateDetails.manageTemplateButton' - | 'templateDetails.sectionLoading' - | 'templateDetails.tab' - | 'templateDetails.title' - | 'templateList' - | 'templateTable' - | 'templatesTab' - | 'legacyTemplateTable' - | 'viewButton' - | 'filterList.filterItem'; +export { TestSubjects } from './test_subjects'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts new file mode 100644 index 00000000000000..4e297118b0fdd9 --- /dev/null +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type TestSubjects = + | 'aliasesTab' + | 'appTitle' + | 'cell' + | 'closeDetailsButton' + | 'createLegacyTemplateButton' + | 'createTemplateButton' + | 'dataStreamsEmptyPromptTemplateLink' + | 'dataStreamTable' + | 'dataStreamTable' + | 'deleteSystemTemplateCallOut' + | 'deleteTemplateButton' + | 'deleteTemplatesConfirmation' + | 'documentationLink' + | 'emptyPrompt' + | 'filterList.filterItem' + | 'indexTable' + | 'indexTableIncludeHiddenIndicesToggle' + | 'indexTableIndexNameLink' + | 'indicesList' + | 'indicesTab' + | 'legacyTemplateTable' + | 'manageTemplateButton' + | 'mappingsTab' + | 'noAliasesCallout' + | 'noMappingsCallout' + | 'noSettingsCallout' + | 'reloadButton' + | 'reloadIndicesButton' + | 'row' + | 'sectionError' + | 'sectionLoading' + | 'settingsTab' + | 'summaryTab' + | 'summaryTitle' + | 'systemTemplatesSwitch' + | 'templateDetails' + | 'templateDetails.manageTemplateButton' + | 'templateDetails.sectionLoading' + | 'templateDetails.tab' + | 'templateDetails.title' + | 'templateList' + | 'templatesTab' + | 'templateTable' + | 'viewButton'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts new file mode 100644 index 00000000000000..ef6aca44a1754c --- /dev/null +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts @@ -0,0 +1,101 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { + registerTestBed, + TestBed, + TestBedConfig, + findTestSubject, +} from '../../../../../test_utils'; +import { DataStream } from '../../../common'; +import { IndexManagementHome } from '../../../public/application/sections/home'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { WithAppDependencies, services, TestSubjects } from '../helpers'; + +const testBedConfig: TestBedConfig = { + store: () => indexManagementStore(services as any), + memoryRouter: { + initialEntries: [`/indices`], + componentRoutePath: `/:section(indices|data_streams|templates)`, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed(WithAppDependencies(IndexManagementHome), testBedConfig); + +export interface DataStreamsTabTestBed extends TestBed { + actions: { + goToDataStreamsList: () => void; + clickEmptyPromptIndexTemplateLink: () => void; + clickReloadButton: () => void; + clickIndicesAt: (index: number) => void; + }; +} + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + /** + * User Actions + */ + + const goToDataStreamsList = () => { + testBed.find('data_streamsTab').simulate('click'); + }; + + const clickEmptyPromptIndexTemplateLink = async () => { + const { find, component, router } = testBed; + + const templateLink = find('dataStreamsEmptyPromptTemplateLink'); + + await act(async () => { + router.navigateTo(templateLink.props().href!); + }); + + component.update(); + }; + + const clickReloadButton = () => { + const { find } = testBed; + find('reloadButton').simulate('click'); + }; + + const clickIndicesAt = async (index: number) => { + const { component, table, router } = testBed; + const { rows } = table.getMetaData('dataStreamTable'); + const indicesLink = findTestSubject(rows[index].reactWrapper, 'indicesLink'); + + await act(async () => { + router.navigateTo(indicesLink.props().href!); + }); + + component.update(); + }; + + return { + ...testBed, + actions: { + goToDataStreamsList, + clickEmptyPromptIndexTemplateLink, + clickReloadButton, + clickIndicesAt, + }, + }; +}; + +export const createDataStreamPayload = (name: string): DataStream => ({ + name, + timeStampField: '@timestamp', + indices: [ + { + name: 'indexName', + uuid: 'indexId', + }, + ], + generation: 1, +}); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts new file mode 100644 index 00000000000000..efe2e2d0c74aee --- /dev/null +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts @@ -0,0 +1,137 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { API_BASE_PATH } from '../../../common/constants'; +import { setupEnvironment } from '../helpers'; + +import { DataStreamsTabTestBed, setup, createDataStreamPayload } from './data_streams_tab.helpers'; + +describe('Data Streams tab', () => { + const { server, httpRequestsMockHelpers } = setupEnvironment(); + let testBed: DataStreamsTabTestBed; + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadIndicesResponse([ + { + health: '', + status: '', + primary: '', + replica: '', + documents: '', + documents_deleted: '', + size: '', + primary_size: '', + name: 'data-stream-index', + data_stream: 'dataStream1', + }, + { + health: 'green', + status: 'open', + primary: 1, + replica: 1, + documents: 10000, + documents_deleted: 100, + size: '156kb', + primary_size: '156kb', + name: 'non-data-stream-index', + }, + ]); + + await act(async () => { + testBed = await setup(); + }); + }); + + describe('when there are no data streams', () => { + beforeEach(async () => { + const { actions, component } = testBed; + + httpRequestsMockHelpers.setLoadDataStreamsResponse([]); + httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] }); + + await act(async () => { + actions.goToDataStreamsList(); + }); + + component.update(); + }); + + test('displays an empty prompt', async () => { + const { exists } = testBed; + + expect(exists('sectionLoading')).toBe(false); + expect(exists('emptyPrompt')).toBe(true); + }); + + test('goes to index templates tab when "Get started" link is clicked', async () => { + const { actions, exists } = testBed; + + await act(async () => { + actions.clickEmptyPromptIndexTemplateLink(); + }); + + expect(exists('templateList')).toBe(true); + }); + }); + + describe('when there are data streams', () => { + beforeEach(async () => { + const { actions, component } = testBed; + + httpRequestsMockHelpers.setLoadDataStreamsResponse([ + createDataStreamPayload('dataStream1'), + createDataStreamPayload('dataStream2'), + ]); + + await act(async () => { + actions.goToDataStreamsList(); + }); + + component.update(); + }); + + test('lists them in the table', async () => { + const { table } = testBed; + + const { tableCellsValues } = table.getMetaData('dataStreamTable'); + + expect(tableCellsValues).toEqual([ + ['dataStream1', '1', '@timestamp', '1'], + ['dataStream2', '1', '@timestamp', '1'], + ]); + }); + + test('has a button to reload the data streams', async () => { + const { exists, actions } = testBed; + const totalRequests = server.requests.length; + + expect(exists('reloadButton')).toBe(true); + + await act(async () => { + actions.clickReloadButton(); + }); + + expect(server.requests.length).toBe(totalRequests + 1); + expect(server.requests[server.requests.length - 1].url).toBe(`${API_BASE_PATH}/data_streams`); + }); + + test('clicking the indices count navigates to the backing indices', async () => { + const { table, actions } = testBed; + + await actions.clickIndicesAt(0); + + expect(table.getMetaData('indexTable').tableCellsValues).toEqual([ + ['', '', '', '', '', '', '', 'dataStream1'], + ]); + }); + }); +}); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts index e995932dfa00d6..f00348aacbf085 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts @@ -6,8 +6,13 @@ import { act } from 'react-dom/test-utils'; -import { registerTestBed, TestBed, TestBedConfig } from '../../../../../test_utils'; -import { IndexList } from '../../../public/application/sections/home/index_list'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { + registerTestBed, + TestBed, + TestBedConfig, + findTestSubject, +} from '../../../../../test_utils'; +import { IndexManagementHome } from '../../../public/application/sections/home'; // eslint-disable-line @kbn/eslint/no-restricted-paths import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths import { WithAppDependencies, services, TestSubjects } from '../helpers'; @@ -15,18 +20,19 @@ const testBedConfig: TestBedConfig = { store: () => indexManagementStore(services as any), memoryRouter: { initialEntries: [`/indices?includeHiddenIndices=true`], - componentRoutePath: `/:section(indices|templates)`, + componentRoutePath: `/:section(indices|data_streams)`, }, doMountAsync: true, }; -const initTestBed = registerTestBed(WithAppDependencies(IndexList), testBedConfig); +const initTestBed = registerTestBed(WithAppDependencies(IndexManagementHome), testBedConfig); export interface IndicesTestBed extends TestBed { actions: { selectIndexDetailsTab: (tab: 'settings' | 'mappings' | 'stats' | 'edit_settings') => void; getIncludeHiddenIndicesToggleStatus: () => boolean; clickIncludeHiddenIndicesToggle: () => void; + clickDataStreamAt: (index: number) => void; }; } @@ -59,12 +65,25 @@ export const setup = async (): Promise => { component.update(); }; + const clickDataStreamAt = async (index: number) => { + const { component, table, router } = testBed; + const { rows } = table.getMetaData('indexTable'); + const dataStreamLink = findTestSubject(rows[index].reactWrapper, 'dataStreamLink'); + + await act(async () => { + router.navigateTo(dataStreamLink.props().href!); + }); + + component.update(); + }; + return { ...testBed, actions: { selectIndexDetailsTab, getIncludeHiddenIndicesToggleStatus, clickIncludeHiddenIndicesToggle, + clickDataStreamAt, }, }; }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts index 11c25ffbb590f5..c2d955bb4dfce8 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts @@ -9,6 +9,7 @@ import { act } from 'react-dom/test-utils'; import { API_BASE_PATH } from '../../../common/constants'; import { setupEnvironment, nextTick } from '../helpers'; import { IndicesTestBed, setup } from './indices_tab.helpers'; +import { createDataStreamPayload } from './data_streams_tab.helpers'; /** * The below import is required to avoid a console error warn from the "brace" package @@ -52,6 +53,49 @@ describe('', () => { }); }); + describe('data stream column', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadIndicesResponse([ + { + health: '', + status: '', + primary: '', + replica: '', + documents: '', + documents_deleted: '', + size: '', + primary_size: '', + name: 'data-stream-index', + data_stream: 'dataStream1', + }, + ]); + + httpRequestsMockHelpers.setLoadDataStreamsResponse([ + createDataStreamPayload('dataStream1'), + createDataStreamPayload('dataStream2'), + ]); + + testBed = await setup(); + + await act(async () => { + const { component } = testBed; + + await nextTick(); + component.update(); + }); + }); + + test('navigates to the data stream in the Data Streams tab', async () => { + const { table, actions } = testBed; + + await actions.clickDataStreamAt(0); + + expect(table.getMetaData('dataStreamTable').tableCellsValues).toEqual([ + ['dataStream1', '1', '@timestamp', '1'], + ]); + }); + }); + describe('index detail panel with % character in index name', () => { const indexName = 'test%'; beforeEach(async () => { diff --git a/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts b/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts new file mode 100644 index 00000000000000..9d267210a6b318 --- /dev/null +++ b/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts @@ -0,0 +1,21 @@ +/* + * 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 { DataStream, DataStreamFromEs } from '../types'; + +export function deserializeDataStreamList(dataStreamsFromEs: DataStreamFromEs[]): DataStream[] { + return dataStreamsFromEs.map(({ name, timestamp_field, indices, generation }) => ({ + name, + timeStampField: timestamp_field, + indices: indices.map( + ({ index_name, index_uuid }: { index_name: string; index_uuid: string }) => ({ + name: index_name, + uuid: index_uuid, + }) + ), + generation, + })); +} diff --git a/x-pack/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts index c67d28da2c24b4..fce4d8ccc2502b 100644 --- a/x-pack/plugins/index_management/common/lib/index.ts +++ b/x-pack/plugins/index_management/common/lib/index.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. */ + +export { deserializeDataStreamList } from './data_stream_serialization'; + export { deserializeLegacyTemplateList, deserializeTemplateList, diff --git a/x-pack/plugins/index_management/common/types/data_streams.ts b/x-pack/plugins/index_management/common/types/data_streams.ts new file mode 100644 index 00000000000000..5b743296d868bd --- /dev/null +++ b/x-pack/plugins/index_management/common/types/data_streams.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface DataStreamFromEs { + name: string; + timestamp_field: string; + indices: DataStreamIndexFromEs[]; + generation: number; +} + +export interface DataStreamIndexFromEs { + index_name: string; + index_uuid: string; +} + +export interface DataStream { + name: string; + timeStampField: string; + indices: DataStreamIndex[]; + generation: number; +} + +export interface DataStreamIndex { + name: string; + uuid: string; +} diff --git a/x-pack/plugins/index_management/common/types/index.ts b/x-pack/plugins/index_management/common/types/index.ts index 81a06156dd291d..c4ba60573d430c 100644 --- a/x-pack/plugins/index_management/common/types/index.ts +++ b/x-pack/plugins/index_management/common/types/index.ts @@ -12,4 +12,6 @@ export * from './mappings'; export * from './templates'; +export { DataStreamFromEs, DataStream, DataStreamIndex } from './data_streams'; + export * from './component_templates'; diff --git a/x-pack/plugins/index_management/public/application/app.tsx b/x-pack/plugins/index_management/public/application/app.tsx index bfd99de6949e58..92197bee30c88f 100644 --- a/x-pack/plugins/index_management/public/application/app.tsx +++ b/x-pack/plugins/index_management/public/application/app.tsx @@ -31,9 +31,9 @@ export const App = ({ history }: { history: ScopedHistory }) => { // Export this so we can test it with a different router. export const AppWithoutRouter = () => ( - - - + + + diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx new file mode 100644 index 00000000000000..a6c8b83a05f989 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -0,0 +1,104 @@ +/* + * 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 React, { Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; + +import { SectionLoading, SectionError, Error } from '../../../../components'; +import { useLoadDataStream } from '../../../../services/api'; + +interface Props { + dataStreamName: string; + onClose: () => void; +} + +/** + * NOTE: This currently isn't in use by data_stream_list.tsx because it doesn't contain any + * information that doesn't already exist in the table. We'll use it once we add additional + * info, e.g. storage size, docs count. + */ +export const DataStreamDetailPanel: React.FunctionComponent = ({ + dataStreamName, + onClose, +}) => { + const { error, data: dataStream, isLoading } = useLoadDataStream(dataStreamName); + + let content; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + + } + error={error as Error} + data-test-subj="sectionError" + /> + ); + } else if (dataStream) { + content = {JSON.stringify(dataStream)}; + } + + return ( + + + +

+ {dataStreamName} +

+
+
+ + {content} + + + + + + + + + + +
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/index.ts b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/index.ts new file mode 100644 index 00000000000000..3f45267c032edb --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DataStreamDetailPanel } from './data_stream_detail_panel'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx new file mode 100644 index 00000000000000..951c4a0d7f3c31 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx @@ -0,0 +1,127 @@ +/* + * 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 React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiTitle, EuiText, EuiSpacer, EuiEmptyPrompt, EuiLink } from '@elastic/eui'; +import { ScopedHistory } from 'kibana/public'; + +import { reactRouterNavigate } from '../../../../shared_imports'; +import { SectionError, SectionLoading, Error } from '../../../components'; +import { useLoadDataStreams } from '../../../services/api'; +import { DataStreamTable } from './data_stream_table'; + +interface MatchParams { + dataStreamName?: string; +} + +export const DataStreamList: React.FunctionComponent> = ({ + match: { + params: { dataStreamName }, + }, + history, +}) => { + const { error, isLoading, data: dataStreams, sendRequest: reload } = useLoadDataStreams(); + + let content; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + + } + error={error as Error} + /> + ); + } else if (Array.isArray(dataStreams) && dataStreams.length === 0) { + content = ( + + + + } + body={ +

+ + {i18n.translate('xpack.idxMgmt.dataStreamList.emptyPrompt.getStartedLink', { + defaultMessage: 'composable index template', + })} + + ), + }} + /> +

+ } + data-test-subj="emptyPrompt" + /> + ); + } else if (Array.isArray(dataStreams) && dataStreams.length > 0) { + content = ( + <> + {/* TODO: Add a switch for toggling on data streams created by Ingest Manager */} + + + + + + + + + + + {/* TODO: Implement this once we have something to put in here, e.g. storage size, docs count */} + {/* dataStreamName && ( + { + history.push('/data_streams'); + }} + /> + )*/} + + ); + } + + return
{content}
; +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx new file mode 100644 index 00000000000000..54b215e561b462 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx @@ -0,0 +1,139 @@ +/* + * 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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiInMemoryTable, EuiBasicTableColumn, EuiButton, EuiLink } from '@elastic/eui'; +import { ScopedHistory } from 'kibana/public'; + +import { DataStream } from '../../../../../../common/types'; +import { reactRouterNavigate } from '../../../../../shared_imports'; +import { encodePathForReactRouter } from '../../../../services/routing'; + +interface Props { + dataStreams?: DataStream[]; + reload: () => {}; + history: ScopedHistory; + filters?: string; +} + +export const DataStreamTable: React.FunctionComponent = ({ + dataStreams, + reload, + history, + filters, +}) => { + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.nameColumnTitle', { + defaultMessage: 'Name', + }), + truncateText: true, + sortable: true, + // TODO: Render as a link to open the detail panel + }, + { + field: 'indices', + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.indicesColumnTitle', { + defaultMessage: 'Indices', + }), + truncateText: true, + sortable: true, + render: (indices: DataStream['indices'], dataStream) => ( + + {indices.length} + + ), + }, + { + field: 'timeStampField', + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.timeStampFieldColumnTitle', { + defaultMessage: 'Timestamp field', + }), + truncateText: true, + sortable: true, + }, + { + field: 'generation', + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.generationFieldColumnTitle', { + defaultMessage: 'Generation', + }), + truncateText: true, + sortable: true, + }, + ]; + + const pagination = { + initialPageSize: 20, + pageSizeOptions: [10, 20, 50], + }; + + const sorting = { + sort: { + field: 'name', + direction: 'asc', + }, + } as const; + + const searchConfig = { + query: filters, + box: { + incremental: true, + }, + toolsLeft: undefined /* TODO: Actions menu */, + toolsRight: [ + + + , + ], + }; + + return ( + <> + ({ + 'data-test-subj': 'row', + })} + cellProps={() => ({ + 'data-test-subj': 'cell', + })} + data-test-subj="dataStreamTable" + message={ + + } + /> + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/index.ts b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/index.ts new file mode 100644 index 00000000000000..3922ca5c1d50ca --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DataStreamTable } from './data_stream_table'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.d.ts b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/index.ts similarity index 82% rename from x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.d.ts rename to x-pack/plugins/index_management/public/application/sections/home/data_stream_list/index.ts index f03f483c0e821b..e2f588cc2a0fb0 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.d.ts +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export declare function IndexList(match: any): any; +export { DataStreamList } from './data_stream_list'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/home.tsx b/x-pack/plugins/index_management/public/application/sections/home/home.tsx index 0ddf4fefce466f..51deaf42cc72ca 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/home.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/home.tsx @@ -19,6 +19,7 @@ import { EuiTitle, } from '@elastic/eui'; import { documentationService } from '../../services/documentation'; +import { DataStreamList } from './data_stream_list'; import { IndexList } from './index_list'; import { TemplateList } from './template_list'; import { ComponentTemplateList } from '../../components/component_templates'; @@ -26,11 +27,17 @@ import { breadcrumbService } from '../../services/breadcrumbs'; export enum Section { Indices = 'indices', + DataStreams = 'data_streams', IndexTemplates = 'templates', ComponentTemplates = 'component_templates', } -export const homeSections = [Section.Indices, Section.IndexTemplates, Section.ComponentTemplates]; +export const homeSections = [ + Section.Indices, + Section.DataStreams, + Section.IndexTemplates, + Section.ComponentTemplates, +]; interface MatchParams { section: Section; @@ -47,6 +54,15 @@ export const IndexManagementHome: React.FunctionComponent, }, + { + id: Section.DataStreams, + name: ( + + ), + }, { id: Section.IndexTemplates, name: ( @@ -122,6 +138,11 @@ export const IndexManagementHome: React.FunctionComponent + = ({ history }) => { return (
- +
); -} +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index.ts similarity index 100% rename from x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index.js rename to x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index.ts diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.d.ts b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.d.ts new file mode 100644 index 00000000000000..35ddfc48136173 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.d.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export declare function IndexTable(props: any): any; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js index f33d486520a294..c3acff087146a8 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js @@ -37,8 +37,10 @@ import { } from '@elastic/eui'; import { UIM_SHOW_DETAILS_CLICK } from '../../../../../../common/constants'; +import { reactRouterNavigate } from '../../../../../shared_imports'; import { REFRESH_RATE_INDEX_LIST } from '../../../../constants'; import { healthToColor } from '../../../../services'; +import { encodePathForReactRouter } from '../../../../services/routing'; import { AppContextConsumer } from '../../../../app_context'; import { renderBadges } from '../../../../lib/render_badges'; import { NoMatch, PageErrorForbidden } from '../../../../components'; @@ -117,6 +119,7 @@ export class IndexTable extends Component { } } } + componentWillUnmount() { clearInterval(this.interval); } @@ -146,11 +149,14 @@ export class IndexTable extends Component { const newIsSortAscending = sortField === column ? !isSortAscending : true; sortChanged(column, newIsSortAscending); }; + renderFilterError() { const { filterError } = this.state; + if (!filterError) { return; } + return ( <> @@ -169,6 +175,7 @@ export class IndexTable extends Component { ); } + onFilterChanged = ({ query, error }) => { if (error) { this.setState({ filterError: error }); @@ -177,6 +184,7 @@ export class IndexTable extends Component { this.setState({ filterError: null }); } }; + getFilters = (extensionsService) => { const { allIndices } = this.props; return extensionsService.filters.reduce((accum, filterExtension) => { @@ -184,6 +192,7 @@ export class IndexTable extends Component { return [...accum, ...filtersToAdd]; }, []); }; + toggleAll = () => { const allSelected = this.areAllItemsSelected(); if (allSelected) { @@ -243,7 +252,8 @@ export class IndexTable extends Component { } buildRowCell(fieldName, value, index, appServices) { - const { openDetailPanel, filterChanged } = this.props; + const { openDetailPanel, filterChanged, history } = this.props; + if (fieldName === 'health') { return {value}; } else if (fieldName === 'name') { @@ -261,7 +271,19 @@ export class IndexTable extends Component { {renderBadges(index, filterChanged, appServices.extensionsService)} ); + } else if (fieldName === 'data_stream') { + return ( + + {value} + + ); } + return value; } @@ -480,12 +502,14 @@ export class IndexTable extends Component {
+ {(indicesLoading && allIndices.length === 0) || indicesError ? null : ( {extensionsService.toggles.map((toggle) => { return this.renderToggleControl(toggle); })} + + + {this.renderBanners(extensionsService)} + {indicesError && this.renderError()} + {atLeastOneItemSelected ? ( @@ -523,6 +551,7 @@ export class IndexTable extends Component { /> ) : null} + {(indicesLoading && allIndices.length === 0) || indicesError ? null : ( @@ -572,11 +601,14 @@ export class IndexTable extends Component { )} + {this.renderFilterError()} + + {indices.length > 0 ? (
- + + {this.buildHeader()} + {this.buildRows(services)}
) : ( emptyState )} + + {indices.length > 0 ? this.renderPager() : null} ); diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx index ec2956973d4f6b..807229fb362676 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx @@ -38,7 +38,7 @@ import { Error, } from '../../../../../components'; import { useLoadIndexTemplate } from '../../../../../services/api'; -import { decodePath } from '../../../../../services/routing'; +import { decodePathFromReactRouter } from '../../../../../services/routing'; import { SendRequestResponse } from '../../../../../../shared_imports'; import { useServices } from '../../../../../app_context'; import { TabSummary, TabMappings, TabSettings, TabAliases } from '../../template_details/tabs'; @@ -107,7 +107,7 @@ export const LegacyTemplateDetails: React.FunctionComponent = ({ reload, }) => { const { uiMetricService } = useServices(); - const decodedTemplateName = decodePath(templateName); + const decodedTemplateName = decodePathFromReactRouter(templateName); const { error, data: templateDetails, isLoading } = useLoadIndexTemplate( decodedTemplateName, isLegacy diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx index 92fedd5d68f002..edce05018ce395 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx @@ -9,12 +9,12 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiInMemoryTable, EuiIcon, EuiButton, EuiLink, EuiBasicTableColumn } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; -import { reactRouterNavigate } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { SendRequestResponse, reactRouterNavigate } from '../../../../../../shared_imports'; import { TemplateListItem } from '../../../../../../../common'; import { UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../../../common/constants'; import { TemplateDeleteModal } from '../../../../../components'; +import { encodePathForReactRouter } from '../../../../../services/routing'; import { useServices } from '../../../../../app_context'; -import { SendRequestResponse } from '../../../../../../shared_imports'; interface Props { templates: TemplateListItem[]; @@ -52,7 +52,7 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ {...reactRouterNavigate( history, { - pathname: `/templates/${encodeURIComponent(encodeURIComponent(name))}`, + pathname: `/templates/${encodePathForReactRouter(name)}`, search: `legacy=${Boolean(item._kbnMeta.isLegacy)}`, }, () => uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK) diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx index 8bdd230f899524..82835c56a38775 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx @@ -11,7 +11,7 @@ import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; import { TemplateDeserialized } from '../../../../common'; import { TemplateForm, SectionLoading, SectionError, Error } from '../../components'; import { breadcrumbService } from '../../services/breadcrumbs'; -import { decodePath, getTemplateDetailsLink } from '../../services/routing'; +import { decodePathFromReactRouter, getTemplateDetailsLink } from '../../services/routing'; import { saveTemplate, useLoadIndexTemplate } from '../../services/api'; import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; @@ -26,7 +26,7 @@ export const TemplateClone: React.FunctionComponent { - const decodedTemplateName = decodePath(name); + const decodedTemplateName = decodePathFromReactRouter(name); const isLegacy = getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index d3e539989bc96c..7cacb5ee97a601 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -11,7 +11,7 @@ import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@e import { TemplateDeserialized } from '../../../../common'; import { breadcrumbService } from '../../services/breadcrumbs'; import { useLoadIndexTemplate, updateTemplate } from '../../services/api'; -import { decodePath, getTemplateDetailsLink } from '../../services/routing'; +import { decodePathFromReactRouter, getTemplateDetailsLink } from '../../services/routing'; import { SectionLoading, SectionError, TemplateForm, Error } from '../../components'; import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; @@ -26,7 +26,7 @@ export const TemplateEdit: React.FunctionComponent { - const decodedTemplateName = decodePath(name); + const decodedTemplateName = decodePathFromReactRouter(name); const isLegacy = getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); diff --git a/x-pack/plugins/index_management/public/application/services/api.ts b/x-pack/plugins/index_management/public/application/services/api.ts index d1950ae7145501..5ad84395d24c2b 100644 --- a/x-pack/plugins/index_management/public/application/services/api.ts +++ b/x-pack/plugins/index_management/public/application/services/api.ts @@ -31,14 +31,12 @@ import { UIM_TEMPLATE_UPDATE, UIM_TEMPLATE_CLONE, } from '../../../common/constants'; - +import { TemplateDeserialized, TemplateListItem, DataStream } from '../../../common'; +import { IndexMgmtMetricsType } from '../../types'; import { TAB_SETTINGS, TAB_MAPPING, TAB_STATS } from '../constants'; - import { useRequest, sendRequest } from './use_request'; import { httpService } from './http'; import { UiMetricService } from './ui_metric'; -import { TemplateDeserialized, TemplateListItem } from '../../../common'; -import { IndexMgmtMetricsType } from '../../types'; // Temporary hack to provide the uiMetricService instance to this file. // TODO: Refactor and export an ApiService instance through the app dependencies context @@ -48,6 +46,21 @@ export const setUiMetricService = (_uiMetricService: UiMetricService({ + path: `${API_BASE_PATH}/data_streams`, + method: 'get', + }); +} + +// TODO: Implement this API endpoint once we have content to surface in the detail panel. +export function useLoadDataStream(name: string) { + return useRequest({ + path: `${API_BASE_PATH}/data_stream/${encodeURIComponent(name)}`, + method: 'get', + }); +} + export async function loadIndices() { const response = await httpService.httpClient.get(`${API_BASE_PATH}/indices`); return response.data ? response.data : response; diff --git a/x-pack/plugins/index_management/public/application/services/routing.ts b/x-pack/plugins/index_management/public/application/services/routing.ts index a999c58f5bb429..2a895196189d09 100644 --- a/x-pack/plugins/index_management/public/application/services/routing.ts +++ b/x-pack/plugins/index_management/public/application/services/routing.ts @@ -6,10 +6,8 @@ export const getTemplateListLink = () => `/templates`; -// Need to add some additonal encoding/decoding logic to work with React Router -// For background, see: https://github.com/ReactTraining/history/issues/505 export const getTemplateDetailsLink = (name: string, isLegacy?: boolean, withHash = false) => { - const baseUrl = `/templates/${encodeURIComponent(encodeURIComponent(name))}`; + const baseUrl = `/templates/${encodePathForReactRouter(name)}`; let url = withHash ? `#${baseUrl}` : baseUrl; if (isLegacy) { url = `${url}?legacy=${isLegacy}`; @@ -18,18 +16,14 @@ export const getTemplateDetailsLink = (name: string, isLegacy?: boolean, withHas }; export const getTemplateEditLink = (name: string, isLegacy?: boolean) => { - return encodeURI( - `/edit_template/${encodeURIComponent(encodeURIComponent(name))}?legacy=${isLegacy === true}` - ); + return encodeURI(`/edit_template/${encodePathForReactRouter(name)}?legacy=${isLegacy === true}`); }; export const getTemplateCloneLink = (name: string, isLegacy?: boolean) => { - return encodeURI( - `/clone_template/${encodeURIComponent(encodeURIComponent(name))}?legacy=${isLegacy === true}` - ); + return encodeURI(`/clone_template/${encodePathForReactRouter(name)}?legacy=${isLegacy === true}`); }; -export const decodePath = (pathname: string): string => { +export const decodePathFromReactRouter = (pathname: string): string => { let decodedPath; try { decodedPath = decodeURI(pathname); @@ -39,3 +33,8 @@ export const decodePath = (pathname: string): string => { } return decodeURIComponent(decodedPath); }; + +// Need to add some additonal encoding/decoding logic to work with React Router +// For background, see: https://github.com/ReactTraining/history/issues/505 +export const encodePathForReactRouter = (pathname: string): string => + encodeURIComponent(encodeURIComponent(pathname)); diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index 89423672615113..afd5a5cf650e1e 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -31,3 +31,5 @@ export { export { getFormRow, Field } from '../../../../src/plugins/es_ui_shared/static/forms/components'; export { isJSON } from '../../../../src/plugins/es_ui_shared/static/validators/string'; + +export { reactRouterNavigate } from '../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/index_management/server/client/elasticsearch.ts b/x-pack/plugins/index_management/server/client/elasticsearch.ts index b51f7d924dba70..6b1bf47512b211 100644 --- a/x-pack/plugins/index_management/server/client/elasticsearch.ts +++ b/x-pack/plugins/index_management/server/client/elasticsearch.ts @@ -10,6 +10,46 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) Client.prototype.dataManagement = components.clientAction.namespaceFactory(); const dataManagement = Client.prototype.dataManagement.prototype; + // Data streams + dataManagement.getDataStreams = ca({ + urls: [ + { + fmt: '/_data_stream', + }, + ], + method: 'GET', + }); + + // We don't allow the user to create a data stream in the UI or API. We're just adding this here + // to enable the API integration tests. + dataManagement.createDataStream = ca({ + urls: [ + { + fmt: '/_data_stream/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'PUT', + }); + + dataManagement.deleteDataStream = ca({ + urls: [ + { + fmt: '/_data_stream/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'DELETE', + }); + // Component templates dataManagement.getComponentTemplates = ca({ urls: [ @@ -71,4 +111,33 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) ], method: 'GET', }); + + dataManagement.saveComposableIndexTemplate = ca({ + urls: [ + { + fmt: '/_index_template/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + needBody: true, + method: 'PUT', + }); + + dataManagement.deleteComposableIndexTemplate = ca({ + urls: [ + { + fmt: '/_index_template/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'DELETE', + }); }; diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/index.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/index.ts new file mode 100644 index 00000000000000..56c514e30f2427 --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { RouteDependencies } from '../../../types'; + +import { registerGetAllRoute } from './register_get_route'; + +export function registerDataStreamRoutes(dependencies: RouteDependencies) { + registerGetAllRoute(dependencies); +} diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts new file mode 100644 index 00000000000000..9128556130bf45 --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.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 { deserializeDataStreamList } from '../../../../common/lib'; +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../index'; + +export function registerGetAllRoute({ router, license, lib: { isEsError } }: RouteDependencies) { + router.get( + { path: addBasePath('/data_streams'), validate: false }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + + try { + const dataStreams = await callAsCurrentUser('dataManagement.getDataStreams'); + const body = deserializeDataStreamList(dataStreams); + + return res.ok({ body }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +} diff --git a/x-pack/plugins/index_management/server/routes/index.ts b/x-pack/plugins/index_management/server/routes/index.ts index 1e5aaf8087624a..202e6919f7b13c 100644 --- a/x-pack/plugins/index_management/server/routes/index.ts +++ b/x-pack/plugins/index_management/server/routes/index.ts @@ -6,6 +6,7 @@ import { RouteDependencies } from '../types'; +import { registerDataStreamRoutes } from './api/data_streams'; import { registerIndicesRoutes } from './api/indices'; import { registerTemplateRoutes } from './api/templates'; import { registerMappingRoute } from './api/mapping'; @@ -15,6 +16,7 @@ import { registerComponentTemplateRoutes } from './api/component_templates'; export class ApiRoutes { setup(dependencies: RouteDependencies) { + registerDataStreamRoutes(dependencies); registerIndicesRoutes(dependencies); registerTemplateRoutes(dependencies); registerSettingsRoutes(dependencies); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx new file mode 100644 index 00000000000000..0b43883728a6f7 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx @@ -0,0 +1,119 @@ +/* + * 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 { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; +import { alertTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/alert_type_registry.mock'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +import { AlertContextMeta, MetricExpression } from '../types'; +import { IIndexPattern } from 'src/plugins/data/public'; +import { InfraSource } from '../../../../common/http_api/source_api'; +import React from 'react'; +import { ExpressionChart } from './expression_chart'; +import { act } from 'react-dom/test-utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { Aggregators, Comparator } from '../../../../server/lib/alerting/metric_threshold/types'; +import { MetricsExplorerResponse } from '../../../../common/http_api'; + +describe('ExpressionChart', () => { + async function setup( + expression: MetricExpression, + response: MetricsExplorerResponse | null, + filterQuery?: string, + groupBy?: string + ) { + const mocks = coreMock.createSetup(); + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); + + const context: AlertsContextValue = { + http: mocks.http, + toastNotifications: mocks.notifications.toasts, + actionTypeRegistry: actionTypeRegistryMock.create() as any, + alertTypeRegistry: alertTypeRegistryMock.create() as any, + docLinks: mocks.docLinks, + capabilities: { + ...capabilities, + actions: { + delete: true, + save: true, + show: true, + }, + }, + }; + const derivedIndexPattern: IIndexPattern = { + title: 'metricbeat-*', + fields: [], + }; + + const source: InfraSource = { + id: 'default', + origin: 'fallback', + configuration: { + name: 'default', + description: 'The default configuration', + logColumns: [], + metricAlias: 'metricbeat-*', + logAlias: 'filebeat-*', + fields: { + timestamp: '@timestamp', + message: ['message'], + container: 'container.id', + host: 'host.name', + pod: 'kubernetes.pod.uid', + tiebreaker: '_doc', + }, + }, + }; + + mocks.http.fetch.mockImplementation(() => Promise.resolve(response)); + + const wrapper = mountWithIntl( + + ); + + const update = async () => + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + await update(); + + return { wrapper, update, fetchMock: mocks.http.fetch }; + } + + it('should display no data message', async () => { + const expression: MetricExpression = { + aggType: Aggregators.AVERAGE, + timeSize: 1, + timeUnit: 'm', + sourceId: 'default', + threshold: [1], + comparator: Comparator.GT_OR_EQ, + }; + const response = { + pageInfo: { + afterKey: null, + total: 0, + }, + series: [{ id: 'Everything', rows: [], columns: [] }], + }; + const { wrapper } = await setup(expression, response); + expect(wrapper.find('[data-test-subj~="noChartData"]').exists()).toBeTruthy(); + }); +}); 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 a886dccf105cfb..64a9d279614998 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 @@ -111,10 +111,15 @@ export const ExpressionChart: React.FC = ({ // Creating a custom series where the ID is changed to 0 // so that we can get a proper domian const firstSeries = first(data.series); - if (!firstSeries) { + if (!firstSeries || !firstSeries.rows || firstSeries.rows.length === 0) { return ( - Oops, no chart data available + + + ); } diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx index a6c9efb5e17d18..5fcee6193b357e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx @@ -44,7 +44,7 @@ export const GroupOfNodes: React.FC = ({ {group.nodes.map((node) => ( last(node.path), - value: 'metric.value', + value: (node: SnapshotNode) => node.metric.value || 0, }; export const sortNodes = (sort: WaffleSortOption, nodes: SnapshotNode[]) => { diff --git a/x-pack/plugins/ingest_manager/dev_docs/api/epm.md b/x-pack/plugins/ingest_manager/dev_docs/api/epm.md new file mode 100644 index 00000000000000..12fecc928419ba --- /dev/null +++ b/x-pack/plugins/ingest_manager/dev_docs/api/epm.md @@ -0,0 +1,24 @@ +This document is part of the original drafts for ingest management documentation in `docs/ingest_manager` and may be outdated. +Overall documentation of Ingest Management is now maintained in the `elastic/stack-docs` repository. + +# Elastic Package Manager API + +The Package Manager offers an API. Here an example on how they can be used. + +List installed packages: + +``` +curl localhost:5601/api/ingest_manager/epm/packages +``` + +Install a package: + +``` +curl -X POST localhost:5601/api/ingest_manager/epm/packages/iptables-1.0.4 +``` + +Delete a package: + +``` +curl -X DELETE localhost:5601/api/ingest_manager/epm/packages/iptables-1.0.4 +``` diff --git a/x-pack/plugins/ingest_manager/dev_docs/definitions.md b/x-pack/plugins/ingest_manager/dev_docs/definitions.md new file mode 100644 index 00000000000000..0d9e285ab80d32 --- /dev/null +++ b/x-pack/plugins/ingest_manager/dev_docs/definitions.md @@ -0,0 +1,71 @@ +This document is part of the original drafts for ingest management documentation in `docs/ingest_manager` and may be outdated. +Overall documentation of Ingest Management is now maintained in the `elastic/stack-docs` repository. + +# Ingest Management Definitions + +This section is to define terms used across ingest management. + +## Data Source + +A data source is a definition on how to collect data from a service, for example `nginx`. A data source contains +definitions for one or multiple inputs and each input can contain one or multiple streams. + +With the example of the nginx Data Source, it contains to inputs: `logs` and `nginx/metrics`. Logs and metrics are collected +differently. The `logs` input contains two streams, `access` and `error`, the `nginx/metrics` input contains the stubstatus stream. + + +## Data Stream + +Data Streams are a [new concept](https://github.com/elastic/elasticsearch/issues/53100) in Elasticsearch which simplify +ingesting data and the setup of Elasticsearch. + +## Elastic Agent + +A single, unified agent that users can deploy to hosts or containers. It controls which data is collected from the host or containers and where the data is sent. It will run Beats, Endpoint or other monitoring programs as needed. It can operate standalone or pull a configuration policy from Fleet. + + +## Elastic Package Registry + +The Elastic Package Registry (EPR) is a service which runs under [https://epr.elastic.co]. It serves the packages through its API. +More details about the registry can be found [here](https://github.com/elastic/package-registry). + +## Fleet + +Fleet is the part of the Ingest Manager UI in Kibana that handles the part of enrolling Elastic Agents, +managing agents and sending configurations to the Elastic Agent. + +## Indexing Strategy + +Ingest Management + Elastic Agent follow a strict new indexing strategy: `{type}-{dataset}-{namespace}`. An example +for this is `logs-nginx.access-default`. More details about it can be found in the Index Strategy below. All data of +the index strategy is sent to Data Streams. + +## Input + +An input is the configuration unit in an Agent Config that defines the options on how to collect data from +an endpoint. This could be username / password which are need to authenticate with a service or a host url +as an example. + +An input is part of a Data Source and contains streams. + +## Integration + +An integration is a package with the type integration. An integration package has at least 1 data source +and usually collects data from / about a service. + +## Namespace + +A user-specified string that will be used to part of the index name in Elasticsearch. It helps users identify logs coming from a specific environment (like prod or test), an application, or other identifiers. + +## Package + +A package contains all the assets for the Elastic Stack. A more detailed definition of a +package can be found under https://github.com/elastic/package-registry. + +Besides the assets, a package contains the data source definitions with its inputs and streams. + +## Stream + +A stream is a configuration unit in the Elastic Agent config. A stream is part of an input and defines how the data +fetched by this input should be processed and which Data Stream to send it to. + diff --git a/x-pack/plugins/ingest_manager/dev_docs/epm.md b/x-pack/plugins/ingest_manager/dev_docs/epm.md new file mode 100644 index 00000000000000..abcab718f7ab9a --- /dev/null +++ b/x-pack/plugins/ingest_manager/dev_docs/epm.md @@ -0,0 +1,30 @@ +This document is part of the original drafts for ingest management documentation in `docs/ingest_manager` and may be outdated. +Overall documentation of Ingest Management is now maintained in the `elastic/stack-docs` repository. + +# Package Upgrades + +When upgrading a package between a bugfix or a minor version, no breaking changes should happen. Upgrading a package has the following effect: + +* Removal of existing dashboards +* Installation of new dashboards +* Write new ingest pipelines with the version +* Write new Elasticsearch alias templates +* Trigger a rollover for all the affected indices + +The new ingest pipeline is expected to still work with the data coming from older configurations. In most cases this means some of the fields can be missing. For this to work, each event must contain the version of config / package it is coming from to make such a decision. + +In case of a breaking change in the data structure, the new ingest pipeline is also expected to deal with this change. In case there are breaking changes which cannot be dealt with in an ingest pipeline, a new package has to be created. + +Each package lists its minimal required agent version. In case there are agents enrolled with an older version, the user is notified to upgrade these agents as otherwise the new configs cannot be rolled out. + +# Generated assets + +When a package is installed or upgraded, certain Kibana and Elasticsearch assets are generated from . These follow the naming conventions explained above (see "indexing strategy") and contain configuration for the elastic stack that makes ingesting and displaying data work with as little user interaction as possible. + +## Elasticsearch Index Templates + +### Generation + +* Index templates are generated from `YAML` files contained in the package. +* There is one index template per dataset. +* For the generation of an index template, all `yml` files contained in the package subdirectory `dataset/DATASET_NAME/fields/` are used. \ No newline at end of file diff --git a/x-pack/plugins/ingest_manager/dev_docs/indexing_strategy.md b/x-pack/plugins/ingest_manager/dev_docs/indexing_strategy.md new file mode 100644 index 00000000000000..fd7edcb7fcca0b --- /dev/null +++ b/x-pack/plugins/ingest_manager/dev_docs/indexing_strategy.md @@ -0,0 +1,77 @@ +This document is part of the original drafts for ingest management documentation in `docs/ingest_manager` and may be outdated. +Overall documentation of Ingest Management is now maintained in the `elastic/stack-docs` repository. + +# Indexing Strategy + +Ingest Management enforces an indexing strategy to allow the system to automatically detect indices and run queries on it. In short the indexing strategy looks as following: + +``` +{dataset.type}-{dataset.name}-{dataset.namespace} +``` + +The `{dataset.type}` can be `logs` or `metrics`. The `{dataset.namespace}` is the part where the user can use free form. The only two requirement are that it has only characters allowed in an Elasticsearch index name and does NOT contain a `-`. The `dataset` is defined by the data that is indexed. The same requirements as for the namespace apply. It is expected that the fields for type, namespace and dataset are part of each event and are constant keywords. If there is a dataset or a namespace with a `-` inside, it is recommended to replace it either by a `.` or a `_`. + +Note: More `{dataset.type}`s might be added in the future like `traces`. + +This indexing strategy has a few advantages: + +* Each index contains only the fields which are relevant for the dataset. This leads to more dense indices and better field completion. +* ILM policies can be applied per namespace per dataset. +* Rollups can be specified per namespace per dataset. +* Having the namespace user configurable makes setting security permissions possible. +* Having a global metrics and logs template, allows to create new indices on demand which still follow the convention. This is common in the case of k8s as an example. +* Constant keywords allow to narrow down the indices we need to access for querying very efficiently. This is especially relevant in environments which a large number of indices or with indices on slower nodes. + +Overall it creates smaller indices in size, makes querying more efficient and allows users to define their own naming parts in namespace and still benefiting from all features that can be built on top of the indexing startegy. + +## Ingest Pipeline + +The ingest pipelines for a specific dataset will have the following naming scheme: + +``` +{dataset.type}-{dataset.name}-{package.version} +``` + +As an example, the ingest pipeline for the Nginx access logs is called `logs-nginx.access-3.4.1`. The same ingest pipeline is used for all namespaces. It is possible that a dataset has multiple ingest pipelines in which case a suffix is added to the name. + +The version is included in each pipeline to allow upgrades. The pipeline itself is listed in the index template and is automatically applied at ingest time. + +## Templates & ILM Policies + +To make the above strategy possible, alias templates are required. For each type there is a basic alias template with a default ILM policy. These default templates apply to all indices which follow the indexing strategy and do not have a more specific dataset alias template. + +The `metrics` and `logs` alias template contain all the basic fields from ECS. + +Each type template contains an ILM policy. Modifying this default ILM policy will affect all data covered by the default templates. + +The templates for a dataset are called as following: + +``` +{dataset.type}-{dataset.name} +``` + +The pattern used inside the index template is `{type}-{dataset}-*` to match all namespaces. + +## Defaults + +If the Elastic Agent is used to ingest data and only the type is specified, `default` for the namespace is used and `generic` for the dataset. + +## Data filtering + +Filtering for data in queries for example in visualizations or dashboards should always be done on the constant keyword fields. Visualizations needing data for the nginx.access dataset should query on `type:logs AND dataset:nginx.access`. As these are constant keywords the prefiltering is very efficient. + +## Security permissions + +Security permissions can be set on different levels. To set special permissions for the access on the prod namespace, use the following index pattern: + +``` +/(logs|metrics)-[^-]+-prod-$/ +``` + +To set specific permissions on the logs index, the following can be used: + +``` +/^(logs|metrics)-.*/ +``` + +Todo: The above queries need to be tested. diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index a2ee3215260520..ab56ae427120bc 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -7,11 +7,18 @@ import { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/pu import { AppServices } from './application'; export { + AuthorizationProvider, + Error, + NotAuthorizedSection, + SectionError, + SectionLoading, + sendRequest, SendRequestConfig, SendRequestResponse, - UseRequestConfig, - sendRequest, + useAuthorizationContext, useRequest, + UseRequestConfig, + WithPrivileges, } from '../../../../src/plugins/es_ui_shared/public/'; export { @@ -41,14 +48,4 @@ export { isEmptyString, } from '../../../../src/plugins/es_ui_shared/static/validators/string'; -export { - SectionLoading, - WithPrivileges, - AuthorizationProvider, - SectionError, - Error, - useAuthorizationContext, - NotAuthorizedSection, -} from '../../../../src/plugins/es_ui_shared/public'; - export const useKibana = () => _useKibana(); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts index 69cba215beafda..e82e05323e6449 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts @@ -5,7 +5,7 @@ */ import { RouteDependencies } from '../../types'; import { API_BASE_PATH, APP_CLUSTER_REQUIRED_PRIVILEGES } from '../../../common/constants'; -import { Privileges } from '../../../../../../src/plugins/es_ui_shared/public'; +import { Privileges } from '../../../../../../src/plugins/es_ui_shared/common'; const extractMissingPrivileges = (privilegesObject: { [key: string]: boolean } = {}): string[] => Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => { diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 33f4f46681a283..53498a8e5afa1e 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -5,6 +5,7 @@ */ import React from 'react'; +import { Observable } from 'rxjs'; import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { App } from './app'; @@ -13,7 +14,11 @@ import { AppMountParameters } from 'kibana/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { Document, SavedObjectStore } from '../persistence'; import { mount } from 'enzyme'; -import { SavedObjectSaveModal } from '../../../../../src/plugins/saved_objects/public'; +import { + SavedObjectSaveModal, + checkForDuplicateTitle, +} from '../../../../../src/plugins/saved_objects/public'; +import { createMemoryHistory, History } from 'history'; import { esFilters, FilterManager, @@ -30,6 +35,17 @@ import { coreMock } from 'src/core/public/mocks'; jest.mock('../persistence'); jest.mock('src/core/public'); +jest.mock('../../../../../src/plugins/saved_objects/public', () => { + // eslint-disable-next-line no-shadow + const { SavedObjectSaveModal, SavedObjectSaveModalOrigin } = jest.requireActual( + '../../../../../src/plugins/saved_objects/public' + ); + return { + SavedObjectSaveModal, + SavedObjectSaveModalOrigin, + checkForDuplicateTitle: jest.fn(), + }; +}); const navigationStartMock = navigationPluginMock.createStartContract(); @@ -90,6 +106,8 @@ function createMockTimefilter() { return unsubscribe; }, }), + getRefreshInterval: () => {}, + getRefreshIntervalDefaults: () => {}, }; } @@ -114,6 +132,7 @@ describe('Lens App', () => { ) => void; originatingApp: string | undefined; onAppLeave: AppMountParameters['onAppLeave']; + history: History; }> { return ({ navigation: navigationStartMock, @@ -134,6 +153,7 @@ describe('Lens App', () => { timefilter: { timefilter: createMockTimefilter(), }, + state$: new Observable(), }, indexPatterns: { get: jest.fn((id) => { @@ -157,6 +177,7 @@ describe('Lens App', () => { ) => {} ), onAppLeave: jest.fn(), + history: createMemoryHistory(), } as unknown) as jest.Mocked<{ navigation: typeof navigationStartMock; editorFrame: EditorFrameInstance; @@ -173,6 +194,7 @@ describe('Lens App', () => { ) => void; originatingApp: string | undefined; onAppLeave: AppMountParameters['onAppLeave']; + history: History; }>; } @@ -186,6 +208,8 @@ describe('Lens App', () => { return { from: 'now-7d', to: 'now' }; } else if (type === UI_SETTINGS.SEARCH_QUERY_LANGUAGE) { return 'kuery'; + } else if (type === 'state:storeInSessionStorage') { + return false; } else { return []; } @@ -634,6 +658,46 @@ describe('Lens App', () => { }); }); + it('checks for duplicate title before saving', async () => { + const args = defaultArgs; + args.editorFrame = frame; + (args.docStorage.save as jest.Mock).mockReturnValue(Promise.resolve({ id: '123' })); + + instance = mount(); + + const onChange = frame.mount.mock.calls[0][1].onChange; + await act(async () => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: '123', expression: 'valid expression' } as unknown) as Document, + }) + ); + instance.update(); + await act(async () => { + getButton(instance).run(instance.getDOMNode()); + }); + instance.update(); + + const onTitleDuplicate = jest.fn(); + + await act(async () => { + instance.find(SavedObjectSaveModal).prop('onSave')({ + onTitleDuplicate, + isTitleDuplicateConfirmed: false, + newCopyOnSave: false, + newDescription: '', + newTitle: 'test', + }); + }); + + expect(checkForDuplicateTitle).toHaveBeenCalledWith( + expect.objectContaining({ id: '123' }), + false, + onTitleDuplicate, + expect.anything() + ); + }); + it('does not show the copy button on first save', async () => { const args = defaultArgs; args.editorFrame = frame; diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 1349d33983fcd0..fc8d5dd9eb3951 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -8,14 +8,23 @@ import _ from 'lodash'; import React, { useState, useEffect, useCallback } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { Query, DataPublicPluginStart } from 'src/plugins/data/public'; import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; import { AppMountContext, AppMountParameters, NotificationsStart } from 'kibana/public'; -import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { History } from 'history'; +import { + Query, + DataPublicPluginStart, + syncQueryStateWithUrl, +} from '../../../../../src/plugins/data/public'; +import { + createKbnUrlStateStorage, + IStorageWrapper, +} from '../../../../../src/plugins/kibana_utils/public'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { SavedObjectSaveModalOrigin, OnSaveProps, + checkForDuplicateTitle, } from '../../../../../src/plugins/saved_objects/public'; import { Document, SavedObjectStore } from '../persistence'; import { EditorFrameInstance } from '../types'; @@ -59,6 +68,7 @@ export function App({ originatingAppFromUrl, navigation, onAppLeave, + history, }: { editorFrame: EditorFrameInstance; data: DataPublicPluginStart; @@ -75,6 +85,7 @@ export function App({ ) => void; originatingAppFromUrl?: string | undefined; onAppLeave: AppMountParameters['onAppLeave']; + history: History; }) { const language = storage.get('kibana.userQueryLanguage') || @@ -129,7 +140,17 @@ export function App({ }, }); + const kbnUrlStateStorage = createKbnUrlStateStorage({ + history, + useHash: core.uiSettings.get('state:storeInSessionStorage'), + }); + const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl( + data.query, + kbnUrlStateStorage + ); + return () => { + stopSyncingQueryServiceStateWithUrl(); filterSubscription.unsubscribe(); timeSubscription.unsubscribe(); }; @@ -232,9 +253,11 @@ export function App({ // state.persistedDoc, ]); - const runSave = ( + const runSave = async ( saveProps: Omit & { returnToOrigin: boolean; + onTitleDuplicate?: OnSaveProps['onTitleDuplicate']; + newDescription?: string; } ) => { if (!lastKnownDoc) { @@ -256,10 +279,30 @@ export function App({ const doc = { ...lastDocWithoutPinned, + description: saveProps.newDescription, id: saveProps.newCopyOnSave ? undefined : lastKnownDoc.id, title: saveProps.newTitle, }; + await checkForDuplicateTitle( + { + ...doc, + copyOnSave: saveProps.newCopyOnSave, + lastSavedTitle: lastKnownDoc?.title, + getEsType: () => 'lens', + getDisplayName: () => + i18n.translate('xpack.lens.app.saveModalType', { + defaultMessage: 'Lens visualization', + }), + }, + saveProps.isTitleDuplicateConfirmed, + saveProps.onTitleDuplicate, + { + savedObjectsClient: core.savedObjects.client, + overlays: core.overlays, + } + ); + const newlyCreated: boolean = saveProps.newCopyOnSave || !lastKnownDoc?.id; docStorage .save(doc) @@ -472,6 +515,7 @@ export function App({ documentInfo={{ id: lastKnownDoc.id, title: lastKnownDoc.title || '', + description: lastKnownDoc.description || '', }} objectType={i18n.translate('xpack.lens.app.saveModalType', { defaultMessage: 'Lens visualization', diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 032ce8325dca1f..e6a9119ad43068 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -93,6 +93,7 @@ export async function mountApp( } originatingAppFromUrl={originatingAppFromUrl} onAppLeave={params.onAppLeave} + history={routeProps.history} /> ); }; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts index b292299c569af8..d62f3dbcf029a8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts @@ -48,6 +48,7 @@ export function getSavedObjectFormat({ return { id: state.persistedId, title: state.title, + description: state.description, type: 'lens', visualizationType: state.visualization.activeId, expression: expression ? toExpression(expression) : '', diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts index 71aabaae3c65c5..e1151b92aac511 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts @@ -388,6 +388,7 @@ describe('editor_frame state management', () => { filters: [], }, title: 'heyo!', + description: 'My lens', type: 'lens', visualizationType: 'line', }, @@ -406,6 +407,7 @@ describe('editor_frame state management', () => { }, persistedId: 'b', title: 'heyo!', + description: 'My lens', visualization: { activeId: 'line', state: { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts index bb6daf5641a64a..09674ebf2ade2c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts @@ -18,6 +18,7 @@ export interface PreviewState { export interface EditorFrameState extends PreviewState { persistedId?: string; title: string; + description?: string; stagedPreview?: PreviewState; activeDatasourceId: string | null; } @@ -157,6 +158,7 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta ...state, persistedId: action.doc.id, title: action.doc.title, + description: action.doc.description, datasourceStates: Object.entries(action.doc.state.datasourceStates).reduce( (stateMap, [datasourceId, datasourceState]) => ({ ...stateMap, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index a9e9efa8d10395..4397ad65d63a5c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -467,7 +467,7 @@ describe('IndexPatternDimensionEditorPanel', () => { expect(setState).not.toHaveBeenCalled(); }); - it('should update label on label input changes', () => { + it('should update label and custom label flag on label input changes', () => { wrapper = mount(); act(() => { @@ -485,6 +485,7 @@ describe('IndexPatternDimensionEditorPanel', () => { ...state.layers.first.columns, col1: expect.objectContaining({ label: 'New Label', + customLabel: true, // Other parts of this don't matter for this test }), }, @@ -493,6 +494,104 @@ describe('IndexPatternDimensionEditorPanel', () => { }); }); + it('should not keep the label as long as it is the default label', () => { + wrapper = mount( + + ); + + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); + }); + + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + label: 'Minimum of bytes', + }), + }, + }, + }, + }); + }); + + it('should keep the label on operation change if it is custom', () => { + wrapper = mount( + + ); + + act(() => { + wrapper.find('button[data-test-subj="lns-indexPatternDimension-min"]').simulate('click'); + }); + + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + label: 'Custom label', + customLabel: true, + }), + }, + }, + }, + }); + }); + describe('transient invalid state', () => { it('should not set the state if selecting an operation incompatible with the current field', () => { wrapper = mount(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx index 6bd4263014b135..7bed770e63fd23 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx @@ -314,17 +314,23 @@ export function PopoverEditor(props: PopoverEditorProps) { data-test-subj="indexPattern-label-edit" value={selectedColumn.label} onChange={(e) => { - setState( - changeColumn({ - state, - layerId, - columnId, - newColumn: { - ...selectedColumn, - label: e.target.value, + setState({ + ...state, + layers: { + ...state.layers, + [layerId]: { + ...state.layers[layerId], + columns: { + ...state.layers[layerId].columns, + [columnId]: { + ...selectedColumn, + label: e.target.value, + customLabel: true, + }, + }, }, - }) - ); + }, + }); }} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts index 639e982142f573..3244eeb94d1e27 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts @@ -16,6 +16,7 @@ export interface BaseIndexPatternColumn extends Operation { operationType: string; sourceField: string; suggestedPriority?: DimensionPriority; + customLabel?: boolean; } // Formatting can optionally be added to any column diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts index 1e3251a8dedd8a..074cb8f5bde17a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts @@ -331,6 +331,61 @@ describe('state_helpers', () => { ); }); + it('should carry over label if customLabel flag is set', () => { + const state: IndexPatternPrivateState = { + indexPatternRefs: [], + existingFields: {}, + indexPatterns: {}, + currentIndexPatternId: '1', + showEmptyFields: false, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'My custom label', + customLabel: true, + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'h', + }, + }, + }, + }, + }, + }; + expect( + changeColumn({ + state, + layerId: 'first', + columnId: 'col2', + newColumn: { + label: 'Date histogram of order_date', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'order_date', + params: { + interval: 'w', + }, + }, + }).layers.first.columns.col1 + ).toEqual( + expect.objectContaining({ + label: 'My custom label', + customLabel: true, + }) + ); + }); + it('should execute adjustments for other columns', () => { const termsColumn: TermsIndexPatternColumn = { label: 'Top values of source', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts index a34d0c4187485f..3a1aaaa819dc0a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts @@ -87,6 +87,11 @@ export function changeColumn({ ? { ...newColumn, params: oldColumn.params } : newColumn; + if (oldColumn && oldColumn.customLabel) { + updatedColumn.customLabel = true; + updatedColumn.label = oldColumn.label; + } + const newColumns = adjustColumnReferencesForChangedColumn( { ...state.layers[layerId].columns, diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts index 515d008d825864..f7caac65493892 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts @@ -25,6 +25,7 @@ describe('LensStore', () => { const { client, store } = testStore('FOO'); const doc = await store.save({ title: 'Hello', + description: 'My doc', visualizationType: 'bar', expression: '', state: { @@ -43,6 +44,7 @@ describe('LensStore', () => { expect(doc).toEqual({ id: 'FOO', title: 'Hello', + description: 'My doc', visualizationType: 'bar', expression: '', state: { @@ -61,6 +63,7 @@ describe('LensStore', () => { expect(client.create).toHaveBeenCalledTimes(1); expect(client.create).toHaveBeenCalledWith('lens', { title: 'Hello', + description: 'My doc', visualizationType: 'bar', expression: '', state: { diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts index 015f4b9b825f42..7632be3d820465 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -13,6 +13,7 @@ export interface Document { type?: string; visualizationType: string | null; title: string; + description?: string; expression: string | null; state: { datasourceMetaData: { diff --git a/x-pack/plugins/lens/public/vis_type_alias.ts b/x-pack/plugins/lens/public/vis_type_alias.ts index a58288191325cb..3bb2dbbae1f9c7 100644 --- a/x-pack/plugins/lens/public/vis_type_alias.ts +++ b/x-pack/plugins/lens/public/vis_type_alias.ts @@ -34,10 +34,11 @@ export const getLensAliasConfig = (): VisTypeAlias => ({ searchFields: ['title^3'], toListItem(savedObject) { const { id, type, attributes } = savedObject; - const { title } = attributes as { title: string }; + const { title, description } = attributes as { title: string; description?: string }; return { id, title, + description, editUrl: getEditPath(id), editApp: 'lens', icon: 'lensApp', diff --git a/x-pack/plugins/lens/server/saved_objects.ts b/x-pack/plugins/lens/server/saved_objects.ts index 1f7d22e2b5642a..a16cc3dab7967a 100644 --- a/x-pack/plugins/lens/server/saved_objects.ts +++ b/x-pack/plugins/lens/server/saved_objects.ts @@ -29,6 +29,9 @@ export function setupSavedObjects(core: CoreSetup) { title: { type: 'text', }, + description: { + type: 'text', + }, visualizationType: { type: 'keyword', }, diff --git a/x-pack/plugins/license_management/public/application/store/actions/start_basic.js b/x-pack/plugins/license_management/public/application/store/actions/start_basic.js index 5ae93bf84c2f86..bce6195caecf09 100644 --- a/x-pack/plugins/license_management/public/application/store/actions/start_basic.js +++ b/x-pack/plugins/license_management/public/application/store/actions/start_basic.js @@ -45,7 +45,8 @@ export const startBasicLicense = (currentLicenseType, ack) => async ( 'xpack.licenseMgmt.replacingCurrentLicenseWithBasicLicenseWarningMessage', { //eslint-disable-next-line - defaultMessage: 'Some functionality will be lost if you replace your {currentLicenseType} license with a BASIC license. Review the list of features below.', + defaultMessage: + 'Some functionality will be lost if you replace your {currentLicenseType} license with a BASIC license. Review the list of features below.', values: { currentLicenseType: currentLicenseType.toUpperCase(), }, diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts index 0a5e861d84483f..ee6a2aa0b339a7 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts @@ -6,9 +6,6 @@ /* eslint-disable @typescript-eslint/camelcase */ -// TODO: You cannot import a stream from common into the front end code! CHANGE THIS -import { Readable } from 'stream'; - import * as t from 'io-ts'; import { file } from '../common/schemas'; @@ -20,17 +17,3 @@ export const importListItemSchema = t.exact( ); export type ImportListItemSchema = t.TypeOf; - -// TODO: You cannot import a stream from common into the front end code! CHANGE THIS -export interface HapiReadableStream extends Readable { - hapi: { - filename: string; - }; -} - -/** - * Special interface since we are streaming in a file through a reader - */ -export interface ImportListItemHapiFileSchema { - file: HapiReadableStream; -} diff --git a/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts b/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts index ecc771279b3ab9..9651be0d04e8c8 100644 --- a/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts +++ b/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts @@ -54,3 +54,18 @@ export const fetchExceptionListItemById = async ({ signal, }: ApiCallByIdProps): Promise => Promise.resolve(getExceptionListItemSchemaMock()); + +export const deleteExceptionListById = async ({ + http, + id, + namespaceType, + signal, +}: ApiCallByIdProps): Promise => Promise.resolve(getExceptionListSchemaMock()); + +export const deleteExceptionListItemById = async ({ + http, + id, + namespaceType, + signal, +}: ApiCallByIdProps): Promise => + Promise.resolve(getExceptionListItemSchemaMock()); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.tsx new file mode 100644 index 00000000000000..edf65839c07cfd --- /dev/null +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.tsx @@ -0,0 +1,257 @@ +/* + * 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 { 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 { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; +import { HttpStart } from '../../../../../../src/core/public'; +import { ApiCallByIdProps } from '../types'; + +import { ExceptionsApi, useApi } from './use_api'; + +jest.mock('../api'); + +const mockKibanaHttpService = createKibanaCoreStartMock().http; + +describe('useApi', () => { + const onErrorMock = jest.fn(); + + afterEach(() => { + onErrorMock.mockClear(); + jest.clearAllMocks(); + }); + + test('it invokes "deleteExceptionListItemById" when "deleteExceptionItem" used', async () => { + const payload = getExceptionListItemSchemaMock(); + const onSuccessMock = jest.fn(); + const spyOnDeleteExceptionListItemById = jest + .spyOn(api, 'deleteExceptionListItemById') + .mockResolvedValue(payload); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + const { id, namespace_type: namespaceType } = payload; + + await result.current.deleteExceptionItem({ + id, + namespaceType, + onError: jest.fn(), + onSuccess: onSuccessMock, + }); + + const expected: ApiCallByIdProps = { + http: mockKibanaHttpService, + id, + namespaceType, + signal: new AbortController().signal, + }; + + expect(spyOnDeleteExceptionListItemById).toHaveBeenCalledWith(expected); + expect(onSuccessMock).toHaveBeenCalled(); + }); + }); + + test('invokes "onError" callback if "deleteExceptionListItemById" fails', async () => { + const mockError = new Error('failed to delete item'); + jest.spyOn(api, 'deleteExceptionListItemById').mockRejectedValue(mockError); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + const { id, namespace_type: namespaceType } = getExceptionListItemSchemaMock(); + + await result.current.deleteExceptionItem({ + id, + namespaceType, + onError: onErrorMock, + onSuccess: jest.fn(), + }); + + expect(onErrorMock).toHaveBeenCalledWith(mockError); + }); + }); + + test('it invokes "deleteExceptionListById" when "deleteExceptionList" used', async () => { + const payload = getExceptionListSchemaMock(); + const onSuccessMock = jest.fn(); + const spyOnDeleteExceptionListById = jest + .spyOn(api, 'deleteExceptionListById') + .mockResolvedValue(payload); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + const { id, namespace_type: namespaceType } = payload; + + await result.current.deleteExceptionList({ + id, + namespaceType, + onError: jest.fn(), + onSuccess: onSuccessMock, + }); + + const expected: ApiCallByIdProps = { + http: mockKibanaHttpService, + id, + namespaceType, + signal: new AbortController().signal, + }; + + expect(spyOnDeleteExceptionListById).toHaveBeenCalledWith(expected); + expect(onSuccessMock).toHaveBeenCalled(); + }); + }); + + test('invokes "onError" callback if "deleteExceptionListById" fails', async () => { + const mockError = new Error('failed to delete item'); + jest.spyOn(api, 'deleteExceptionListById').mockRejectedValue(mockError); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + const { id, namespace_type: namespaceType } = getExceptionListSchemaMock(); + + await result.current.deleteExceptionList({ + id, + namespaceType, + onError: onErrorMock, + onSuccess: jest.fn(), + }); + + expect(onErrorMock).toHaveBeenCalledWith(mockError); + }); + }); + + test('it invokes "fetchExceptionListItemById" when "getExceptionItem" used', async () => { + const payload = getExceptionListItemSchemaMock(); + const onSuccessMock = jest.fn(); + const spyOnFetchExceptionListItemById = jest + .spyOn(api, 'fetchExceptionListItemById') + .mockResolvedValue(payload); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + const { id, namespace_type: namespaceType } = payload; + + await result.current.getExceptionItem({ + id, + namespaceType, + onError: jest.fn(), + onSuccess: onSuccessMock, + }); + + const expected: ApiCallByIdProps = { + http: mockKibanaHttpService, + id, + namespaceType, + signal: new AbortController().signal, + }; + + expect(spyOnFetchExceptionListItemById).toHaveBeenCalledWith(expected); + expect(onSuccessMock).toHaveBeenCalled(); + }); + }); + + test('invokes "onError" callback if "fetchExceptionListItemById" fails', async () => { + const mockError = new Error('failed to delete item'); + jest.spyOn(api, 'fetchExceptionListItemById').mockRejectedValue(mockError); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + const { id, namespace_type: namespaceType } = getExceptionListSchemaMock(); + + await result.current.getExceptionItem({ + id, + namespaceType, + onError: onErrorMock, + onSuccess: jest.fn(), + }); + + expect(onErrorMock).toHaveBeenCalledWith(mockError); + }); + }); + + test('it invokes "fetchExceptionListById" when "getExceptionList" used', async () => { + const payload = getExceptionListSchemaMock(); + const onSuccessMock = jest.fn(); + const spyOnFetchExceptionListById = jest + .spyOn(api, 'fetchExceptionListById') + .mockResolvedValue(payload); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + const { id, namespace_type: namespaceType } = payload; + + await result.current.getExceptionList({ + id, + namespaceType, + onError: jest.fn(), + onSuccess: onSuccessMock, + }); + + const expected: ApiCallByIdProps = { + http: mockKibanaHttpService, + id, + namespaceType, + signal: new AbortController().signal, + }; + + expect(spyOnFetchExceptionListById).toHaveBeenCalledWith(expected); + expect(onSuccessMock).toHaveBeenCalled(); + }); + }); + + test('invokes "onError" callback if "fetchExceptionListById" fails', async () => { + const mockError = new Error('failed to delete item'); + jest.spyOn(api, 'fetchExceptionListById').mockRejectedValue(mockError); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + const { id, namespace_type: namespaceType } = getExceptionListSchemaMock(); + + await result.current.getExceptionList({ + id, + namespaceType, + onError: onErrorMock, + onSuccess: jest.fn(), + }); + + expect(onErrorMock).toHaveBeenCalledWith(mockError); + }); + }); +}); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.tsx b/x-pack/plugins/lists/public/exceptions/hooks/use_api.tsx new file mode 100644 index 00000000000000..45e180d9d617c6 --- /dev/null +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_api.tsx @@ -0,0 +1,111 @@ +/* + * 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 * as Api from '../api'; +import { HttpStart } from '../../../../../../src/core/public'; +import { ExceptionListItemSchema, ExceptionListSchema } from '../../../common/schemas'; +import { ApiCallMemoProps } from '../types'; + +export interface ExceptionsApi { + deleteExceptionItem: (arg: ApiCallMemoProps) => Promise; + deleteExceptionList: (arg: ApiCallMemoProps) => Promise; + getExceptionItem: ( + arg: ApiCallMemoProps & { onSuccess: (arg: ExceptionListItemSchema) => void } + ) => Promise; + getExceptionList: ( + arg: ApiCallMemoProps & { onSuccess: (arg: ExceptionListSchema) => void } + ) => Promise; +} + +export const useApi = (http: HttpStart): ExceptionsApi => { + return useMemo( + (): ExceptionsApi => ({ + async deleteExceptionItem({ + id, + namespaceType, + onSuccess, + onError, + }: ApiCallMemoProps): Promise { + const abortCtrl = new AbortController(); + + try { + await Api.deleteExceptionListItemById({ + http, + id, + namespaceType, + signal: abortCtrl.signal, + }); + onSuccess(); + } catch (error) { + onError(error); + } + }, + async deleteExceptionList({ + id, + namespaceType, + onSuccess, + onError, + }: ApiCallMemoProps): Promise { + const abortCtrl = new AbortController(); + + try { + await Api.deleteExceptionListById({ + http, + id, + namespaceType, + signal: abortCtrl.signal, + }); + onSuccess(); + } catch (error) { + onError(error); + } + }, + async getExceptionItem({ + id, + namespaceType, + onSuccess, + onError, + }: ApiCallMemoProps & { onSuccess: (arg: ExceptionListItemSchema) => void }): Promise { + const abortCtrl = new AbortController(); + + try { + const item = await Api.fetchExceptionListItemById({ + http, + id, + namespaceType, + signal: abortCtrl.signal, + }); + onSuccess(item); + } catch (error) { + onError(error); + } + }, + async getExceptionList({ + id, + namespaceType, + onSuccess, + onError, + }: ApiCallMemoProps & { onSuccess: (arg: ExceptionListSchema) => void }): Promise { + const abortCtrl = new AbortController(); + + try { + const list = await Api.fetchExceptionListById({ + http, + id, + namespaceType, + signal: abortCtrl.signal, + }); + onSuccess(list); + } catch (error) { + onError(error); + } + }, + }), + [http] + ); +}; diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx index a6a25ab4d4e9d8..eeb3ac63ee3181 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx @@ -10,7 +10,8 @@ import * as api from '../api'; import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; -import { ExceptionListAndItems, UseExceptionListProps } from '../types'; +import { ExceptionListItemSchema } from '../../../common/schemas'; +import { ExceptionList, UseExceptionListProps, UseExceptionListSuccess } from '../types'; import { ReturnExceptionListAndItems, useExceptionList } from './use_exception_list'; @@ -34,47 +35,69 @@ describe('useExceptionList', () => { >(() => useExceptionList({ http: mockKibanaHttpService, - id: 'myListId', - namespaceType: 'single', + lists: [{ id: 'myListId', namespaceType: 'single' }], onError: onErrorMock, }) ); await waitForNextUpdate(); - expect(result.current).toEqual([true, null, result.current[2]]); - expect(typeof result.current[2]).toEqual('function'); + expect(result.current).toEqual([ + true, + [], + [], + { + page: 1, + perPage: 20, + total: 0, + }, + null, + ]); }); }); test('fetch exception list and items', async () => { await act(async () => { + const onSuccessMock = jest.fn(); const { result, waitForNextUpdate } = renderHook< UseExceptionListProps, ReturnExceptionListAndItems >(() => useExceptionList({ http: mockKibanaHttpService, - id: 'myListId', - namespaceType: 'single', + lists: [{ id: 'myListId', namespaceType: 'single' }], onError: onErrorMock, + onSuccess: onSuccessMock, }) ); await waitForNextUpdate(); await waitForNextUpdate(); - const expectedResult: ExceptionListAndItems = { - ...getExceptionListSchemaMock(), - exceptionItems: { - items: [{ ...getExceptionListItemSchemaMock() }], - pagination: { - page: 1, - perPage: 20, - total: 1, - }, - }, + const expectedListResult: ExceptionList[] = [ + { ...getExceptionListSchemaMock(), totalItems: 1 }, + ]; + + const expectedListItemsResult: ExceptionListItemSchema[] = [ + { ...getExceptionListItemSchemaMock() }, + ]; + + const expectedResult: UseExceptionListSuccess = { + exceptions: expectedListItemsResult, + lists: expectedListResult, + pagination: { page: 1, perPage: 20, total: 1 }, }; - expect(result.current).toEqual([false, expectedResult, result.current[2]]); + expect(result.current).toEqual([ + false, + expectedListResult, + expectedListItemsResult, + { + page: 1, + perPage: 20, + total: 1, + }, + result.current[4], + ]); + expect(onSuccessMock).toHaveBeenCalledWith(expectedResult); }); }); @@ -86,22 +109,21 @@ describe('useExceptionList', () => { UseExceptionListProps, ReturnExceptionListAndItems >( - ({ filterOptions, http, id, namespaceType, pagination, onError }) => - useExceptionList({ filterOptions, http, id, namespaceType, onError, pagination }), + ({ filterOptions, http, lists, pagination, onError, onSuccess }) => + useExceptionList({ filterOptions, http, lists, onError, onSuccess, pagination }), { initialProps: { http: mockKibanaHttpService, - id: 'myListId', - namespaceType: 'single', + lists: [{ id: 'myListId', namespaceType: 'single' }], onError: onErrorMock, + onSuccess: jest.fn(), }, } ); await waitForNextUpdate(); rerender({ http: mockKibanaHttpService, - id: 'newListId', - namespaceType: 'single', + lists: [{ id: 'newListId', namespaceType: 'single' }], onError: onErrorMock, }); await waitForNextUpdate(); @@ -121,14 +143,19 @@ describe('useExceptionList', () => { >(() => useExceptionList({ http: mockKibanaHttpService, - id: 'myListId', - namespaceType: 'single', + lists: [{ id: 'myListId', namespaceType: 'single' }], onError: onErrorMock, }) ); await waitForNextUpdate(); await waitForNextUpdate(); - result.current[2](); + + expect(typeof result.current[4]).toEqual('function'); + + if (result.current[4] != null) { + result.current[4](); + } + await waitForNextUpdate(); expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(2); @@ -147,8 +174,7 @@ describe('useExceptionList', () => { () => useExceptionList({ http: mockKibanaHttpService, - id: 'myListId', - namespaceType: 'single', + lists: [{ id: 'myListId', namespaceType: 'single' }], onError: onErrorMock, }) ); @@ -170,8 +196,7 @@ describe('useExceptionList', () => { () => useExceptionList({ http: mockKibanaHttpService, - id: 'myListId', - namespaceType: 'single', + lists: [{ id: 'myListId', namespaceType: 'single' }], onError: onErrorMock, }) ); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.tsx b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.tsx index 116233cd893481..9595cb7b7558ec 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.tsx @@ -4,28 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { fetchExceptionListById, fetchExceptionListItemsByListId } from '../api'; -import { ExceptionListAndItems, UseExceptionListProps } from '../types'; +import { ExceptionIdentifiers, ExceptionList, Pagination, UseExceptionListProps } from '../types'; +import { ExceptionListItemSchema } from '../../../common/schemas'; -export type ReturnExceptionListAndItems = [boolean, ExceptionListAndItems | null, () => void]; +type Func = () => void; +export type ReturnExceptionListAndItems = [ + boolean, + ExceptionList[], + ExceptionListItemSchema[], + Pagination, + Func | null +]; /** * Hook for using to get an ExceptionList and it's ExceptionListItems * * @param http Kibana http service - * @param id desired ExceptionList ID (not list_id) - * @param namespaceType list namespaceType determines list space + * @param lists array of ExceptionIdentifiers for all lists to fetch * @param onError error callback + * @param onSuccess callback when all lists fetched successfully * @param filterOptions optional - filter by fields or tags * @param pagination optional * */ export const useExceptionList = ({ http, - id, - namespaceType, + lists, pagination = { page: 1, perPage: 20, @@ -36,20 +43,37 @@ export const useExceptionList = ({ tags: [], }, onError, + onSuccess, }: UseExceptionListProps): ReturnExceptionListAndItems => { - const [exceptionListAndItems, setExceptionList] = useState(null); - const [shouldRefresh, setRefresh] = useState(true); - const refreshExceptionList = useCallback(() => setRefresh(true), [setRefresh]); + const [exceptionLists, setExceptionLists] = useState([]); + const [exceptionItems, setExceptionListItems] = useState([]); + const [paginationInfo, setPagination] = useState(pagination); + const fetchExceptionList = useRef(null); const [loading, setLoading] = useState(true); - const tags = filterOptions.tags.sort().join(); + const tags = useMemo(() => filterOptions.tags.sort().join(), [filterOptions.tags]); + const listIds = useMemo( + () => + lists + .map((t) => t.id) + .sort() + .join(), + [lists] + ); useEffect( () => { - let isSubscribed = true; - const abortCtrl = new AbortController(); + let isSubscribed = false; + let abortCtrl: AbortController; + + const fetchLists = async (): Promise => { + isSubscribed = true; + abortCtrl = new AbortController(); - const fetchData = async (idToFetch: string): Promise => { - if (shouldRefresh) { + // TODO: workaround until api updated, will be cleaned up + let exceptions: ExceptionListItemSchema[] = []; + let exceptionListsReturned: ExceptionList[] = []; + + const fetchData = async ({ id, namespaceType }: ExceptionIdentifiers): Promise => { try { setLoading(true); @@ -59,7 +83,7 @@ export const useExceptionList = ({ ...restOfExceptionList } = await fetchExceptionListById({ http, - id: idToFetch, + id, namespaceType, signal: abortCtrl.signal, }); @@ -72,40 +96,68 @@ export const useExceptionList = ({ signal: abortCtrl.signal, }); - setRefresh(false); - if (isSubscribed) { - setExceptionList({ - list_id, - namespace_type, - ...restOfExceptionList, - exceptionItems: { - items: [...fetchListItemsResult.data], + exceptionListsReturned = [ + ...exceptionListsReturned, + { + list_id, + namespace_type, + ...restOfExceptionList, + totalItems: fetchListItemsResult.total, + }, + ]; + setExceptionLists(exceptionListsReturned); + setPagination({ + page: fetchListItemsResult.page, + perPage: fetchListItemsResult.per_page, + total: fetchListItemsResult.total, + }); + + exceptions = [...exceptions, ...fetchListItemsResult.data]; + setExceptionListItems(exceptions); + + if (onSuccess != null) { + onSuccess({ + exceptions, + lists: exceptionListsReturned, pagination: { page: fetchListItemsResult.page, perPage: fetchListItemsResult.per_page, total: fetchListItemsResult.total, }, - }, - }); + }); + } } } catch (error) { - setRefresh(false); if (isSubscribed) { - setExceptionList(null); + setExceptionLists([]); + setExceptionListItems([]); + setPagination({ + page: 1, + perPage: 20, + total: 0, + }); 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); } }; - if (id != null) { - fetchData(id); - } + fetchLists(); + + fetchExceptionList.current = fetchLists; return (): void => { isSubscribed = false; abortCtrl.abort(); @@ -113,9 +165,9 @@ export const useExceptionList = ({ }, // eslint-disable-next-line react-hooks/exhaustive-deps [ http, - id, - onError, - shouldRefresh, + listIds, + setExceptionLists, + setExceptionListItems, pagination.page, pagination.perPage, filterOptions.filter, @@ -123,5 +175,5 @@ export const useExceptionList = ({ ] ); - return [loading, exceptionListAndItems, refreshExceptionList]; + return [loading, exceptionLists, exceptionItems, paginationInfo, fetchExceptionList.current]; }; diff --git a/x-pack/plugins/lists/public/exceptions/types.ts b/x-pack/plugins/lists/public/exceptions/types.ts index cf6b6c3ec1c598..013788cddc0760 100644 --- a/x-pack/plugins/lists/public/exceptions/types.ts +++ b/x-pack/plugins/lists/public/exceptions/types.ts @@ -24,15 +24,6 @@ export interface Pagination { total: number; } -export interface ExceptionItemsAndPagination { - items: ExceptionListItemSchema[]; - pagination: Pagination; -} - -export interface ExceptionListAndItems extends ExceptionListSchema { - exceptionItems: ExceptionItemsAndPagination; -} - export type AddExceptionList = ExceptionListSchema | CreateExceptionListSchemaPartial; export type AddExceptionListItem = CreateExceptionListItemSchemaPartial | ExceptionListItemSchema; @@ -42,13 +33,29 @@ export interface PersistHookProps { onError: (arg: Error) => void; } +export interface ExceptionList extends ExceptionListSchema { + totalItems: number; +} + +export interface UseExceptionListSuccess { + lists: ExceptionList[]; + exceptions: ExceptionListItemSchema[]; + pagination: Pagination; +} + export interface UseExceptionListProps { - filterOptions?: FilterExceptionsOptions; http: HttpStart; - id: string | undefined; - namespaceType: NamespaceType; + lists: ExceptionIdentifiers[]; onError: (arg: Error) => void; + filterOptions?: FilterExceptionsOptions; pagination?: Pagination; + onSuccess?: (arg: UseExceptionListSuccess) => void; +} + +export interface ExceptionIdentifiers { + id: string; + namespaceType: NamespaceType; + type?: string; } export interface ApiCallByListIdProps { @@ -67,6 +74,13 @@ export interface ApiCallByIdProps { signal: AbortSignal; } +export interface ApiCallMemoProps { + id: string; + namespaceType: NamespaceType; + onError: (arg: Error) => void; + onSuccess: () => void; +} + export interface AddExceptionListProps { http: HttpStart; list: AddExceptionList; diff --git a/x-pack/plugins/lists/public/index.tsx b/x-pack/plugins/lists/public/index.tsx index fb4d5de06ae54d..1e25275a0d38ba 100644 --- a/x-pack/plugins/lists/public/index.tsx +++ b/x-pack/plugins/lists/public/index.tsx @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ // Exports to be shared with plugins +export { useApi } from './exceptions/hooks/use_api'; export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item'; export { usePersistExceptionList } from './exceptions/hooks/persist_exception_list'; export { useExceptionList } from './exceptions/hooks/use_exception_list'; +export { ExceptionList, ExceptionIdentifiers } from './exceptions/types'; export { mockNewExceptionItem, mockNewExceptionList } from './exceptions/mock'; 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 36cf9bac373eb8..c951c9b3371311 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 @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Readable } from 'stream'; + import { IRouter } from 'kibana/server'; import { LIST_ITEM_URL } from '../../common/constants'; @@ -13,15 +15,23 @@ import { transformError, validate, } from '../siem_server_deps'; -import { - ImportListItemHapiFileSchema, - importListItemQuerySchema, - importListItemSchema, - listSchema, -} from '../../common/schemas'; +import { importListItemQuerySchema, importListItemSchema, listSchema } from '../../common/schemas'; import { getListClient } from '.'; +export interface HapiReadableStream extends Readable { + hapi: { + filename: string; + }; +} + +/** + * Special interface since we are streaming in a file through a reader + */ +export interface ImportListItemHapiFileSchema { + file: HapiReadableStream; +} + export const importListItemRoute = (router: IRouter): void => { router.post( { diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_detection.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_detection.json new file mode 100644 index 00000000000000..306195f4226e39 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_detection.json @@ -0,0 +1,9 @@ +{ + "list_id": "detection_list", + "_tags": ["detection"], + "tags": ["detection", "sample_tag"], + "type": "detection", + "description": "This is a sample detection type exception list", + "name": "Sample Detection Exception List", + "namespace_type": "single" +} 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 d68a26eb8ffe23..c89c7a8f080cf6 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 @@ -5,6 +5,7 @@ "type": "simple", "description": "This is a sample endpoint type exception that has no item_id so it creates a new id each time", "name": "Sample Endpoint Exception List", + "comment": [], "entries": [ { "field": "actingProcess.file.signer", diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_detection_auto_id.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_detection_auto_id.json new file mode 100644 index 00000000000000..3fe4458a737693 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_detection_auto_id.json @@ -0,0 +1,26 @@ +{ + "list_id": "detection_list", + "_tags": ["detection"], + "tags": ["test_tag", "detection", "no_more_bad_guys"], + "type": "simple", + "description": "This is a sample detection type exception that has no item_id so it creates a new id each time", + "name": "Sample Detection Exception List Item", + "comment": [], + "entries": [ + { + "field": "host.name", + "operator": "included", + "match": "sampleHostName" + }, + { + "field": "event.category", + "operator": "included", + "match_any": ["process", "malware"] + }, + { + "field": "event.action", + "operator": "included", + "match": "user-password-change" + } + ] +} diff --git a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts index 9413bf9a3d84e5..a283269271bd0c 100644 --- a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts +++ b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TestReadable } from '../../../common/test_readable.mock'; - import { BufferLines } from './buffer_lines'; +import { TestReadable } from './test_readable.mock'; describe('buffer_lines', () => { test('it can read a single line', (done) => { diff --git a/x-pack/plugins/lists/common/test_readable.mock.ts b/x-pack/plugins/lists/server/services/items/test_readable.mock.ts similarity index 100% rename from x-pack/plugins/lists/common/test_readable.mock.ts rename to x-pack/plugins/lists/server/services/items/test_readable.mock.ts diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts index 3d9902e1d43dd0..ccacdfbb5ff654 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts @@ -3,11 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { TestReadable } from '../../../common/test_readable.mock'; import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { ImportListItemsToStreamOptions, WriteBufferToItemsOptions } from '../items'; import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from '../../../common/constants.mock'; +import { TestReadable } from './test_readable.mock'; + export const getImportListItemsToStreamOptionsMock = (): ImportListItemsToStreamOptions => ({ callCluster: getCallClusterMock(), listId: LIST_ID, diff --git a/x-pack/plugins/maps/public/classes/fields/ems_file_field.ts b/x-pack/plugins/maps/public/classes/fields/ems_file_field.ts index 73d6c1ef9f790c..2e9c5c9fe60c23 100644 --- a/x-pack/plugins/maps/public/classes/fields/ems_file_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/ems_file_field.ts @@ -30,12 +30,6 @@ export class EMSFileField extends AbstractField implements IField { } async getLabel(): Promise { - const emsFileLayer = await this._source.getEMSFileLayer(); - // TODO remove any and @ts-ignore when emsFileLayer type defined - // @ts-ignore - const emsFields: any[] = emsFileLayer.getFieldsInLanguage(); - // Map EMS field name to language specific label - const emsField = emsFields.find((field) => field.name === this.getName()); - return emsField ? emsField.description : this.getName(); + return this._source.getEmsFieldLabel(this.getName()); } } diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/create_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/create_source_editor.tsx index e398af4acea3ba..a78a49032503b8 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/create_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/create_source_editor.tsx @@ -8,8 +8,8 @@ import React, { Component } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { getEMSClient } from '../../../meta'; +import { FileLayer } from '@elastic/ems-client'; +import { getEmsFileLayers } from '../../../meta'; import { getEmsUnavailableMessage } from '../ems_unavailable_message'; import { EMSFileSourceDescriptor } from '../../../../common/descriptor_types'; @@ -33,15 +33,10 @@ export class EMSFileCreateSourceEditor extends Component { }; _loadFileOptions = async () => { - // @ts-ignore - const emsClient = getEMSClient(); - // @ts-ignore - const fileLayers: unknown[] = await emsClient.getFileLayers(); + const fileLayers: FileLayer[] = await getEmsFileLayers(); const options = fileLayers.map((fileLayer) => { return { - // @ts-ignore value: fileLayer.getId(), - // @ts-ignore label: fileLayer.getDisplayName(), }; }); diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx index 24c111a72ac059..52524d0c9a5fae 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx @@ -11,17 +11,8 @@ jest.mock('../../layers/vector_layer/vector_layer', () => {}); function makeEMSFileSource(tooltipProperties: string[]) { const emsFileSource = new EMSFileSource({ tooltipProperties }); - emsFileSource.getEMSFileLayer = async () => { - return { - getFieldsInLanguage() { - return [ - { - name: 'iso2', - description: 'ISO 2 CODE', - }, - ]; - }, - }; + emsFileSource.getEmsFieldLabel = async (name: string) => { + return name === 'iso2' ? 'ISO 2 CODE' : name; }; return emsFileSource; } diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx index 94f5bb0d2ba07f..f7fb0078764c43 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx @@ -8,12 +8,12 @@ import React, { ReactElement } from 'react'; import { i18n } from '@kbn/i18n'; import { Feature } from 'geojson'; import { Adapters } from 'src/plugins/inspector/public'; +import { FileLayer } from '@elastic/ems-client'; import { Attribution, ImmutableSourceProperty, SourceEditorArgs } from '../source'; import { AbstractVectorSource, GeoJsonWithMeta, IVectorSource } from '../vector_source'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { SOURCE_TYPES, FIELD_ORIGIN } from '../../../../common/constants'; -// @ts-ignore -import { getEMSClient } from '../../../meta'; +import { getEmsFileLayers } from '../../../meta'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { UpdateSourceEditor } from './update_source_editor'; import { EMSFileField } from '../../fields/ems_file_field'; @@ -23,7 +23,7 @@ import { EMSFileSourceDescriptor } from '../../../../common/descriptor_types'; import { ITooltipProperty } from '../../tooltips/tooltip_property'; export interface IEmsFileSource extends IVectorSource { - getEMSFileLayer(): Promise; + getEmsFieldLabel(emsFieldName: string): Promise; createField({ fieldName }: { fieldName: string }): IField; } @@ -72,13 +72,9 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc ); } - async getEMSFileLayer(): Promise { - // @ts-ignore - const emsClient = getEMSClient(); - // @ts-ignore - const emsFileLayers = await emsClient.getFileLayers(); + async getEMSFileLayer(): Promise { + const emsFileLayers = await getEmsFileLayers(); const emsFileLayer = emsFileLayers.find( - // @ts-ignore (fileLayer) => fileLayer.getId() === this._descriptor.id ); if (!emsFileLayer) { @@ -94,19 +90,25 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc return emsFileLayer; } + // Map EMS field name to language specific label + async getEmsFieldLabel(emsFieldName: string): Promise { + const emsFileLayer = await this.getEMSFileLayer(); + const emsFields = emsFileLayer.getFieldsInLanguage(); + + const emsField = emsFields.find((field) => field.name === emsFieldName); + return emsField ? emsField.description : emsFieldName; + } + async getGeoJsonWithMeta(): Promise { const emsFileLayer = await this.getEMSFileLayer(); // @ts-ignore const featureCollection = await AbstractVectorSource.getGeoJson({ - // @ts-ignore format: emsFileLayer.getDefaultFormatType(), featureCollectionPath: 'data', - // @ts-ignore fetchUrl: emsFileLayer.getDefaultFormatUrl(), }); - // @ts-ignore - const emsIdField = emsFileLayer._config.fields.find((field) => { + const emsIdField = emsFileLayer.getFields().find((field) => { return field.type === 'id'; }); featureCollection.features.forEach((feature: Feature, index: number) => { @@ -123,7 +125,6 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc let emsLink; try { const emsFileLayer = await this.getEMSFileLayer(); - // @ts-ignore emsLink = emsFileLayer.getEMSHotLink(); } catch (error) { // ignore error if EMS layer id could not be found @@ -147,7 +148,6 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc async getDisplayName(): Promise { try { const emsFileLayer = await this.getEMSFileLayer(); - // @ts-ignore return emsFileLayer.getDisplayName(); } catch (error) { return this._descriptor.id; @@ -156,15 +156,12 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc async getAttributions(): Promise { const emsFileLayer = await this.getEMSFileLayer(); - // @ts-ignore return emsFileLayer.getAttributions(); } async getLeftJoinFields() { const emsFileLayer = await this.getEMSFileLayer(); - // @ts-ignore const fields = emsFileLayer.getFieldsInLanguage(); - // @ts-ignore return fields.map((f) => this.createField({ fieldName: f.name })); } diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx index daeb1f8bc6b2b5..ac69505a9bed5e 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx @@ -8,8 +8,7 @@ import React, { Component, Fragment } from 'react'; import { EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { TooltipSelector } from '../../../components/tooltip_selector'; -// @ts-ignore -import { getEMSClient } from '../../../meta'; +import { getEmsFileLayers } from '../../../meta'; import { IEmsFileSource } from './ems_file_source'; import { IField } from '../../fields/field'; import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/view'; @@ -42,22 +41,18 @@ export class UpdateSourceEditor extends Component { } async loadFields() { - let fields; + let fields: IField[] = []; try { - // @ts-ignore - const emsClient = getEMSClient(); - // @ts-ignore - const emsFiles = await emsClient.getFileLayers(); - // @ts-ignore - const taregetEmsFile = emsFiles.find((emsFile) => emsFile.getId() === this.props.layerId); - // @ts-ignore - const emsFields = taregetEmsFile.getFieldsInLanguage(); - // @ts-ignore - fields = emsFields.map((field) => this.props.source.createField({ fieldName: field.name })); + const emsFiles = await getEmsFileLayers(); + const targetEmsFile = emsFiles.find((emsFile) => emsFile.getId() === this.props.layerId); + if (targetEmsFile) { + fields = targetEmsFile + .getFieldsInLanguage() + .map((field) => this.props.source.createField({ fieldName: field.name })); + } } catch (e) { // When a matching EMS-config cannot be found, the source already will have thrown errors during the data request. // This will propagate to the vector-layer and be displayed in the UX - fields = []; } if (this._isMounted) { diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js index 36c9e424a8b22b..83c87eb53d4fe7 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js @@ -7,7 +7,7 @@ import _ from 'lodash'; import React from 'react'; import { AbstractTMSSource } from '../tms_source'; -import { getEMSClient } from '../../../meta'; +import { getEmsTmsServices } from '../../../meta'; import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -66,8 +66,7 @@ export class EMSTMSSource extends AbstractTMSSource { } async _getEMSTMSService() { - const emsClient = getEMSClient(); - const emsTMSServices = await emsClient.getTMSServices(); + const emsTMSServices = await getEmsTmsServices(); const emsTileLayerId = this.getTileLayerId(); const tmsService = emsTMSServices.find((tmsService) => tmsService.getId() === emsTileLayerId); if (!tmsService) { diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.test.js b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.test.js index 08c54299d721b0..2f466add282624 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.test.js +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.test.js @@ -6,7 +6,7 @@ jest.mock('../../../meta', () => { return { - getEMSClient: () => { + getEmsTmsServices: () => { class MockTMSService { constructor(config) { this._config = config; @@ -19,20 +19,16 @@ jest.mock('../../../meta', () => { } } - return { - async getTMSServices() { - return [ - new MockTMSService({ - id: 'road_map', - attributionMarkdown: '[foobar](http://foobar.org) | [foobaz](http://foobaz.org)', - }), - new MockTMSService({ - id: 'satellite', - attributionMarkdown: '[satellite](http://satellite.org)', - }), - ]; - }, - }; + return [ + new MockTMSService({ + id: 'road_map', + attributionMarkdown: '[foobar](http://foobar.org) | [foobaz](http://foobaz.org)', + }), + new MockTMSService({ + id: 'satellite', + attributionMarkdown: '[satellite](http://satellite.org)', + }), + ]; }, }; }); diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.js b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.js index 4d5d6655609c12..3931e441ff2548 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.js +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.js @@ -7,7 +7,7 @@ import React from 'react'; import { EuiSelect, EuiFormRow } from '@elastic/eui'; -import { getEMSClient } from '../../../meta'; +import { getEmsTmsServices } from '../../../meta'; import { getEmsUnavailableMessage } from '../ems_unavailable_message'; import { i18n } from '@kbn/i18n'; @@ -29,8 +29,7 @@ export class TileServiceSelect extends React.Component { } _loadTmsOptions = async () => { - const emsClient = getEMSClient(); - const emsTMSServices = await emsClient.getTMSServices(); + const emsTMSServices = await getEmsTmsServices(); if (!this._isMounted) { return; diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx index b778dc00764590..ca78aaefe404f7 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx @@ -12,13 +12,12 @@ import { KibanaRegionmapSource, sourceTitle } from './kibana_regionmap_source'; import { VectorLayer } from '../../layers/vector_layer/vector_layer'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; -// @ts-ignore import { getKibanaRegionList } from '../../../meta'; export const kibanaRegionMapLayerWizardConfig: LayerWizard = { - checkVisibility: () => { + checkVisibility: async () => { const regions = getKibanaRegionList(); - return regions.length; + return regions.length > 0; }, description: i18n.translate('xpack.maps.source.kbnRegionMapDescription', { defaultMessage: 'Vector data from hosted GeoJSON configured in kibana.yml', diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx index 227c0182b98dee..84d2e5e74fa9a1 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx @@ -12,12 +12,12 @@ import { CreateSourceEditor } from './create_source_editor'; // @ts-ignore import { KibanaTilemapSource, sourceTitle } from './kibana_tilemap_source'; import { TileLayer } from '../../layers/tile_layer/tile_layer'; -// @ts-ignore import { getKibanaTileMap } from '../../../meta'; export const kibanaBasemapLayerWizardConfig: LayerWizard = { checkVisibility: async () => { const tilemap = getKibanaTileMap(); + // @ts-ignore return !!tilemap.url; }, description: i18n.translate('xpack.maps.source.kbnTMSDescription', { diff --git a/x-pack/plugins/maps/public/meta.js b/x-pack/plugins/maps/public/meta.js deleted file mode 100644 index 46c5e5cda36175..00000000000000 --- a/x-pack/plugins/maps/public/meta.js +++ /dev/null @@ -1,111 +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 { - getHttp, - getLicenseId, - getIsEmsEnabled, - getRegionmapLayers, - getTilemap, - getEmsFileApiUrl, - getEmsTileApiUrl, - getEmsLandingPageUrl, - getEmsFontLibraryUrl, - getProxyElasticMapsServiceInMaps, - getKibanaVersion, -} from './kibana_services'; -import { - GIS_API_PATH, - EMS_FILES_CATALOGUE_PATH, - EMS_TILES_CATALOGUE_PATH, - EMS_GLYPHS_PATH, - EMS_APP_NAME, - FONTS_API_PATH, -} from '../common/constants'; -import { i18n } from '@kbn/i18n'; -import { EMSClient } from '@elastic/ems-client'; - -import fetch from 'node-fetch'; - -const GIS_API_RELATIVE = `../${GIS_API_PATH}`; - -export function getKibanaRegionList() { - return getRegionmapLayers(); -} - -export function getKibanaTileMap() { - return getTilemap(); -} - -function relativeToAbsolute(url) { - const a = document.createElement('a'); - a.setAttribute('href', url); - return a.href; -} - -function fetchFunction(...args) { - return fetch(...args); -} - -let emsClient = null; -let latestLicenseId = null; -export function getEMSClient() { - if (!emsClient) { - const isEmsEnabled = getIsEmsEnabled(); - if (isEmsEnabled) { - const proxyElasticMapsServiceInMaps = getProxyElasticMapsServiceInMaps(); - const proxyPath = ''; - const tileApiUrl = proxyElasticMapsServiceInMaps - ? relativeToAbsolute(`${GIS_API_RELATIVE}/${EMS_TILES_CATALOGUE_PATH}`) - : getEmsTileApiUrl(); - const fileApiUrl = proxyElasticMapsServiceInMaps - ? relativeToAbsolute(`${GIS_API_RELATIVE}/${EMS_FILES_CATALOGUE_PATH}`) - : getEmsFileApiUrl(); - - emsClient = new EMSClient({ - language: i18n.getLocale(), - appVersion: getKibanaVersion(), - appName: EMS_APP_NAME, - tileApiUrl, - fileApiUrl, - landingPageUrl: getEmsLandingPageUrl(), - fetchFunction: fetchFunction, //import this from client-side, so the right instance is returned (bootstrapped from common/* would not work - proxyPath, - }); - } else { - //EMS is turned off. Mock API. - emsClient = { - async getFileLayers() { - return []; - }, - async getTMSServices() { - return []; - }, - addQueryParams() {}, - }; - } - } - const licenseId = getLicenseId(); - if (latestLicenseId !== licenseId) { - latestLicenseId = licenseId; - emsClient.addQueryParams({ license: licenseId }); - } - return emsClient; -} - -export function getGlyphUrl() { - if (!getIsEmsEnabled()) { - return getHttp().basePath.prepend(`/${FONTS_API_PATH}/{fontstack}/{range}`); - } - return getProxyElasticMapsServiceInMaps() - ? relativeToAbsolute(`../${GIS_API_PATH}/${EMS_TILES_CATALOGUE_PATH}/${EMS_GLYPHS_PATH}`) + - `/{fontstack}/{range}` - : getEmsFontLibraryUrl(); -} - -export function isRetina() { - return window.devicePixelRatio === 2; -} diff --git a/x-pack/plugins/maps/public/meta.ts b/x-pack/plugins/maps/public/meta.ts new file mode 100644 index 00000000000000..54c5eac7fe1b0c --- /dev/null +++ b/x-pack/plugins/maps/public/meta.ts @@ -0,0 +1,111 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EMSClient, FileLayer, TMSService } from '@elastic/ems-client'; + +import fetch from 'node-fetch'; +import { + GIS_API_PATH, + EMS_FILES_CATALOGUE_PATH, + EMS_TILES_CATALOGUE_PATH, + EMS_GLYPHS_PATH, + EMS_APP_NAME, + FONTS_API_PATH, +} from '../common/constants'; +import { + getHttp, + getLicenseId, + getIsEmsEnabled, + getRegionmapLayers, + getTilemap, + getEmsFileApiUrl, + getEmsTileApiUrl, + getEmsLandingPageUrl, + getEmsFontLibraryUrl, + getProxyElasticMapsServiceInMaps, + getKibanaVersion, +} from './kibana_services'; + +const GIS_API_RELATIVE = `../${GIS_API_PATH}`; + +export function getKibanaRegionList(): unknown[] { + return getRegionmapLayers(); +} + +export function getKibanaTileMap(): unknown { + return getTilemap(); +} + +export async function getEmsFileLayers(): Promise { + if (!getIsEmsEnabled()) { + return []; + } + + return getEMSClient().getFileLayers(); +} + +export async function getEmsTmsServices(): Promise { + if (!getIsEmsEnabled()) { + return []; + } + + return getEMSClient().getTMSServices(); +} + +function relativeToAbsolute(url: string): string { + const a = document.createElement('a'); + a.setAttribute('href', url); + return a.href; +} + +let emsClient: EMSClient | null = null; +let latestLicenseId: string | null = null; +export function getEMSClient(): EMSClient { + if (!emsClient) { + const proxyElasticMapsServiceInMaps = getProxyElasticMapsServiceInMaps(); + const proxyPath = ''; + const tileApiUrl = proxyElasticMapsServiceInMaps + ? relativeToAbsolute(`${GIS_API_RELATIVE}/${EMS_TILES_CATALOGUE_PATH}`) + : getEmsTileApiUrl(); + const fileApiUrl = proxyElasticMapsServiceInMaps + ? relativeToAbsolute(`${GIS_API_RELATIVE}/${EMS_FILES_CATALOGUE_PATH}`) + : getEmsFileApiUrl(); + + emsClient = new EMSClient({ + language: i18n.getLocale(), + appVersion: getKibanaVersion(), + appName: EMS_APP_NAME, + tileApiUrl, + fileApiUrl, + landingPageUrl: getEmsLandingPageUrl(), + fetchFunction(url: string) { + return fetch(url); + }, + proxyPath, + }); + } + const licenseId = getLicenseId(); + if (latestLicenseId !== licenseId) { + latestLicenseId = licenseId; + emsClient.addQueryParams({ license: licenseId }); + } + return emsClient; +} + +export function getGlyphUrl(): string { + if (!getIsEmsEnabled()) { + return getHttp().basePath.prepend(`/${FONTS_API_PATH}/{fontstack}/{range}`); + } + return getProxyElasticMapsServiceInMaps() + ? relativeToAbsolute(`../${GIS_API_PATH}/${EMS_TILES_CATALOGUE_PATH}/${EMS_GLYPHS_PATH}`) + + `/{fontstack}/{range}` + : getEmsFontLibraryUrl(); +} + +export function isRetina(): boolean { + return window.devicePixelRatio === 2; +} diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index a265cf80c73cd0..f2331b9a1a9600 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -23,7 +23,8 @@ import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/s import { emsBoundariesSpecProvider } from './tutorials/ems'; // @ts-ignore import { initRoutes } from './routes'; -import { ILicense, LicensingPluginSetup } from '../../licensing/public'; +import { ILicense } from '../../licensing/common/types'; +import { LicensingPluginSetup } from '../../licensing/server'; import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; interface SetupDeps { diff --git a/x-pack/plugins/ml/server/shared.ts b/x-pack/plugins/ml/server/shared.ts index 1e50950bc3bced..be27ee2d44a827 100644 --- a/x-pack/plugins/ml/server/shared.ts +++ b/x-pack/plugins/ml/server/shared.ts @@ -5,3 +5,4 @@ */ export * from '../common/types/anomalies'; +export * from '../common/types/anomaly_detection_jobs'; diff --git a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts index 73696dfdeef868..880aebfde409c3 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts @@ -6,12 +6,13 @@ import { APICaller } from 'kibana/server'; import { LicenseCheck } from '../license_checks'; +import { Job } from '../../../common/types/anomaly_detection_jobs'; export interface AnomalyDetectorsProvider { anomalyDetectorsProvider( callAsCurrentUser: APICaller ): { - jobs(jobId?: string): Promise; + jobs(jobId?: string): Promise<{ count: number; jobs: Job[] }>; }; } diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 7db8e57421d02b..b0b9d70a76c32b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -143,6 +143,9 @@ export type Throttle = t.TypeOf; export const throttleOrNull = t.union([throttle, t.null]); export type ThrottleOrNull = t.TypeOf; +export const throttleOrNullOrUndefined = t.union([throttle, t.null, t.undefined]); +export type ThrottleOrUndefinedOrNull = t.TypeOf; + export const anomaly_threshold = PositiveInteger; export type AnomalyThreshold = t.TypeOf; @@ -156,11 +159,8 @@ export const machineLearningJobIdOrUndefined = t.union([machine_learning_job_id, export type MachineLearningJobIdOrUndefined = t.TypeOf; /** - * Note that this is a plain unknown object because we allow the UI - * to send us extra additional information as "meta" which can be anything. - * - * TODO: Strip away extra information and possibly even "freeze" this object - * so we have tighter control over 3rd party data structures. + * Note that this is a non-exact io-ts type as we allow extra meta information + * to be added to the meta object */ export const meta = t.object; export type Meta = t.TypeOf; @@ -192,8 +192,10 @@ export const severityOrUndefined = t.union([severity, t.undefined]); export type SeverityOrUndefined = t.TypeOf; export const status = t.keyof({ open: null, closed: null }); +export type Status = t.TypeOf; export const job_status = t.keyof({ succeeded: null, failed: null, 'going to run': null }); +export type JobStatus = t.TypeOf; // TODO: Create a regular expression type or custom date math part type here export const to = t.string; @@ -307,10 +309,20 @@ export const versionOrUndefined = t.union([version, t.undefined]); export type VersionOrUndefined = t.TypeOf; export const last_success_at = IsoDateString; +export type LastSuccessAt = t.TypeOf; + export const last_success_message = t.string; +export type LastSuccessMessage = t.TypeOf; + export const last_failure_at = IsoDateString; +export type LastFailureAt = t.TypeOf; + export const last_failure_message = t.string; +export type LastFailureMessage = t.TypeOf; + export const status_date = IsoDateString; +export type StatusDate = t.TypeOf; + export const rules_installed = PositiveInteger; export const rules_updated = PositiveInteger; export const status_code = PositiveInteger; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts index a9ab6f8959e24e..2847bd32df514d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts @@ -7,19 +7,30 @@ import { CreateRulesSchema, CreateRulesSchemaDecoded } from './create_rules_schema'; import { DEFAULT_MAX_SIGNALS } from '../../../constants'; -export const getCreateRulesSchemaMock = (): CreateRulesSchema => ({ - description: 'some description', +export const getCreateRulesSchemaMock = (ruleId = 'rule-1'): CreateRulesSchema => ({ + description: 'Detecting root and admin users', name: 'Query with a rule id', query: 'user.name: root or user.name: admin', severity: 'high', type: 'query', risk_score: 55, language: 'kuery', - rule_id: 'rule-1', + rule_id: ruleId, }); +export const getCreateMlRulesSchemaMock = (ruleId = 'rule-1') => { + const { query, language, index, ...mlParams } = getCreateRulesSchemaMock(ruleId); + + return { + ...mlParams, + type: 'machine_learning', + anomaly_threshold: 58, + machine_learning_job_id: 'typical-ml-job-id', + }; +}; + export const getCreateRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => ({ - description: 'some description', + description: 'Detecting root and admin users', name: 'Query with a rule id', query: 'user.name: root or user.name: admin', severity: 'high', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts index 92fab202c9ddc1..aaeb90ffc5bcf0 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts @@ -7,7 +7,7 @@ import { ImportRulesSchema, ImportRulesSchemaDecoded } from './import_rules_schema'; import { DEFAULT_MAX_SIGNALS } from '../../../constants'; -export const getImportRulesSchemaMock = (): ImportRulesSchema => ({ +export const getImportRulesSchemaMock = (ruleId = 'rule-1'): ImportRulesSchema => ({ description: 'some description', name: 'Query with a rule id', query: 'user.name: root or user.name: admin', @@ -15,7 +15,19 @@ export const getImportRulesSchemaMock = (): ImportRulesSchema => ({ type: 'query', risk_score: 55, language: 'kuery', - rule_id: 'rule-1', + rule_id: ruleId, +}); + +export const getImportRulesWithIdSchemaMock = (ruleId = 'rule-1'): ImportRulesSchema => ({ + id: '6afb8ce1-ea94-4790-8653-fd0b021d2113', + description: 'some description', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'query', + risk_score: 55, + language: 'kuery', + rule_id: ruleId, }); export const getImportRulesSchemaDecodedMock = (): ImportRulesSchemaDecoded => ({ @@ -42,3 +54,22 @@ export const getImportRulesSchemaDecodedMock = (): ImportRulesSchemaDecoded => ( rule_id: 'rule-1', immutable: false, }); + +/** + * Given an array of rules, builds an NDJSON string of rules + * as we might import/export + * @param rules Array of rule objects with which to generate rule JSON + */ +export const rulesToNdJsonString = (rules: ImportRulesSchema[]) => { + return rules.map((rule) => JSON.stringify(rule)).join('\r\n'); +}; + +/** + * Given an array of rule IDs, builds an NDJSON string of rules + * as we might import + * @param ruleIds Array of ruleIds with which to generate rule JSON + */ +export const ruleIdsToNdJsonString = (ruleIds: string[]) => { + const rules = ruleIds.map((ruleId) => getImportRulesSchemaMock(ruleId)); + return rulesToNdJsonString(rules); +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.mocks.ts new file mode 100644 index 00000000000000..52b2de316ddd58 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.mocks.ts @@ -0,0 +1,17 @@ +/* + * 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 { ErrorSchema } from './error_schema'; + +export const getErrorSchemaMock = ( + id: string = '819eded6-e9c8-445b-a647-519aea39e063' +): ErrorSchema => ({ + id, + error: { + status_code: 404, + message: 'id: "819eded6-e9c8-445b-a647-519aea39e063" not found', + }, +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.test.ts index 2a4d75522d010b..d9e16105351bd1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.test.ts @@ -7,15 +7,15 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { getErrorPayload } from './__mocks__/utils'; import { errorSchema, ErrorSchema } from './error_schema'; import { exactCheck } from '../../../exact_check'; import { foldLeftRight, getPaths } from '../../../test_utils'; +import { getErrorSchemaMock } from './error_schema.mocks'; describe('error_schema', () => { test('it should validate an error with a UUID given for id', () => { - const error = getErrorPayload(); - const decoded = errorSchema.decode(getErrorPayload()); + const error = getErrorSchemaMock(); + const decoded = errorSchema.decode(getErrorSchemaMock()); const checked = exactCheck(error, decoded); const message = pipe(checked, foldLeftRight); @@ -24,7 +24,7 @@ describe('error_schema', () => { }); test('it should validate an error with a plain string given for id since sometimes we echo the user id which might not be a UUID back out to them', () => { - const error = getErrorPayload('fake id'); + const error = getErrorSchemaMock('fake id'); const decoded = errorSchema.decode(error); const checked = exactCheck(error, decoded); const message = pipe(checked, foldLeftRight); @@ -35,7 +35,7 @@ describe('error_schema', () => { test('it should NOT validate an error when it has extra data next to a valid payload element', () => { type InvalidError = ErrorSchema & { invalid_extra_data?: string }; - const error: InvalidError = getErrorPayload(); + const error: InvalidError = getErrorSchemaMock(); error.invalid_extra_data = 'invalid_extra_data'; const decoded = errorSchema.decode(error); const checked = exactCheck(error, decoded); @@ -46,7 +46,7 @@ describe('error_schema', () => { }); test('it should NOT validate an error when it has required elements deleted from it', () => { - const error = getErrorPayload(); + const error = getErrorSchemaMock(); delete error.error; const decoded = errorSchema.decode(error); const checked = exactCheck(error, decoded); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_rules_schema.mocks.ts new file mode 100644 index 00000000000000..28c3eea761b404 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_rules_schema.mocks.ts @@ -0,0 +1,15 @@ +/* + * 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 { FindRulesSchema } from './find_rules_schema'; +import { getRulesSchemaMock } from './rules_schema.mocks'; + +export const getFindRulesSchemaMock = (): FindRulesSchema => ({ + page: 1, + perPage: 1, + total: 1, + data: [getRulesSchemaMock()], +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_rules_schema.test.ts index 51163c3d76ed6e..fc1ab9b87795d1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_rules_schema.test.ts @@ -6,31 +6,32 @@ import { findRulesSchema, FindRulesSchema } from './find_rules_schema'; import { pipe } from 'fp-ts/lib/pipeable'; -import { getFindResponseSingle, getBaseResponsePayload } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { RulesSchema } from './rules_schema'; import { exactCheck } from '../../../exact_check'; import { foldLeftRight, getPaths } from '../../../test_utils'; +import { getRulesSchemaMock } from './rules_schema.mocks'; +import { getFindRulesSchemaMock } from './find_rules_schema.mocks'; describe('find_rules_schema', () => { test('it should validate a typical single find rules response', () => { - const payload = getFindResponseSingle(); + const payload = getFindRulesSchemaMock(); const decoded = findRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getFindResponseSingle()); + expect(message.schema).toEqual(getFindRulesSchemaMock()); }); test('it should validate an empty find rules response', () => { - const payload = getFindResponseSingle(); + const payload = getFindRulesSchemaMock(); payload.data = []; const decoded = findRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getFindResponseSingle(); + const expected = getFindRulesSchemaMock(); expected.data = []; expect(getPaths(left(message.errors))).toEqual([]); @@ -38,7 +39,7 @@ describe('find_rules_schema', () => { }); test('it should invalidate a typical single find rules response if it is has an extra property on it', () => { - const payload: FindRulesSchema & { invalid_data?: 'invalid' } = getFindResponseSingle(); + const payload: FindRulesSchema & { invalid_data?: 'invalid' } = getFindRulesSchemaMock(); payload.invalid_data = 'invalid'; const decoded = findRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); @@ -49,8 +50,8 @@ describe('find_rules_schema', () => { }); test('it should invalidate a typical single find rules response if the rules are invalid within it', () => { - const payload = getFindResponseSingle(); - const invalidRule: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + const payload = getFindRulesSchemaMock(); + const invalidRule: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); invalidRule.invalid_extra_data = 'invalid_data'; payload.data = [invalidRule]; const decoded = findRulesSchema.decode(payload); @@ -62,8 +63,8 @@ describe('find_rules_schema', () => { }); test('it should invalidate a typical single find rules response if the rule is missing a required field such as name', () => { - const payload = getFindResponseSingle(); - const invalidRule = getBaseResponsePayload(); + const payload = getFindRulesSchemaMock(); + const invalidRule = getRulesSchemaMock(); delete invalidRule.name; payload.data = [invalidRule]; const decoded = findRulesSchema.decode(payload); @@ -77,7 +78,7 @@ describe('find_rules_schema', () => { }); test('it should invalidate a typical single find rules response if it is missing perPage', () => { - const payload = getFindResponseSingle(); + const payload = getFindRulesSchemaMock(); delete payload.perPage; const decoded = findRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); @@ -90,7 +91,7 @@ describe('find_rules_schema', () => { }); test('it should invalidate a typical single find rules response if it has a negative perPage number', () => { - const payload = getFindResponseSingle(); + const payload = getFindRulesSchemaMock(); payload.perPage = -1; const decoded = findRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); @@ -101,7 +102,7 @@ describe('find_rules_schema', () => { }); test('it should invalidate a typical single find rules response if it has a negative page number', () => { - const payload = getFindResponseSingle(); + const payload = getFindRulesSchemaMock(); payload.page = -1; const decoded = findRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); @@ -112,7 +113,7 @@ describe('find_rules_schema', () => { }); test('it should invalidate a typical single find rules response if it has a negative total', () => { - const payload = getFindResponseSingle(); + const payload = getFindRulesSchemaMock(); payload.total = -1; const decoded = findRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.mocks.ts new file mode 100644 index 00000000000000..b55809e6b27ea3 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.mocks.ts @@ -0,0 +1,10 @@ +/* + * 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 { RulesBulkSchema } from './rules_bulk_schema'; +import { getRulesSchemaMock } from './rules_schema.mocks'; + +export const getRulesBulkSchemaMock = (): RulesBulkSchema => [getRulesSchemaMock()]; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts index 04cf012f36dbac..a169ac3deb9b9e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts @@ -7,46 +7,48 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { getBaseResponsePayload, getErrorPayload } from './__mocks__/utils'; import { RulesBulkSchema, rulesBulkSchema } from './rules_bulk_schema'; import { RulesSchema } from './rules_schema'; import { ErrorSchema } from './error_schema'; import { exactCheck } from '../../../exact_check'; import { foldLeftRight, getPaths } from '../../../test_utils'; +import { getRulesSchemaMock } from './rules_schema.mocks'; +import { getErrorSchemaMock } from './error_schema.mocks'; + describe('prepackaged_rule_schema', () => { test('it should validate a regular message and and error together with a uuid', () => { - const payload: RulesBulkSchema = [getBaseResponsePayload(), getErrorPayload()]; + const payload: RulesBulkSchema = [getRulesSchemaMock(), getErrorSchemaMock()]; const decoded = rulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([getBaseResponsePayload(), getErrorPayload()]); + expect(message.schema).toEqual([getRulesSchemaMock(), getErrorSchemaMock()]); }); test('it should validate a regular message and and error together when the error has a non UUID', () => { - const payload: RulesBulkSchema = [getBaseResponsePayload(), getErrorPayload('fake id')]; + const payload: RulesBulkSchema = [getRulesSchemaMock(), getErrorSchemaMock('fake id')]; const decoded = rulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([getBaseResponsePayload(), getErrorPayload('fake id')]); + expect(message.schema).toEqual([getRulesSchemaMock(), getErrorSchemaMock('fake id')]); }); test('it should validate an error', () => { - const payload: RulesBulkSchema = [getErrorPayload('fake id')]; + const payload: RulesBulkSchema = [getErrorSchemaMock('fake id')]; const decoded = rulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([getErrorPayload('fake id')]); + expect(message.schema).toEqual([getErrorSchemaMock('fake id')]); }); test('it should NOT validate a rule with a deleted value', () => { - const rule = getBaseResponsePayload(); + const rule = getRulesSchemaMock(); delete rule.name; const payload: RulesBulkSchema = [rule]; const decoded = rulesBulkSchema.decode(payload); @@ -61,7 +63,7 @@ describe('prepackaged_rule_schema', () => { }); test('it should NOT validate an invalid error message with a deleted value', () => { - const error = getErrorPayload('fake id'); + const error = getErrorSchemaMock('fake id'); delete error.error; const payload: RulesBulkSchema = [error]; const decoded = rulesBulkSchema.decode(payload); @@ -76,7 +78,7 @@ describe('prepackaged_rule_schema', () => { }); test('it should NOT validate a type of "query" when it has extra data', () => { - const rule: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + const rule: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); rule.invalid_extra_data = 'invalid_extra_data'; const payload: RulesBulkSchema = [rule]; const decoded = rulesBulkSchema.decode(payload); @@ -88,9 +90,9 @@ describe('prepackaged_rule_schema', () => { }); test('it should NOT validate a type of "query" when it has extra data next to a valid error', () => { - const rule: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + const rule: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); rule.invalid_extra_data = 'invalid_extra_data'; - const payload: RulesBulkSchema = [getErrorPayload(), rule]; + const payload: RulesBulkSchema = [getErrorSchemaMock(), rule]; const decoded = rulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -101,7 +103,7 @@ describe('prepackaged_rule_schema', () => { test('it should NOT validate an error when it has extra data', () => { type InvalidError = ErrorSchema & { invalid_extra_data?: string }; - const error: InvalidError = getErrorPayload(); + const error: InvalidError = getErrorSchemaMock(); error.invalid_extra_data = 'invalid'; const payload: RulesBulkSchema = [error]; const decoded = rulesBulkSchema.decode(payload); @@ -114,9 +116,9 @@ describe('prepackaged_rule_schema', () => { test('it should NOT validate an error when it has extra data next to a valid payload element', () => { type InvalidError = ErrorSchema & { invalid_extra_data?: string }; - const error: InvalidError = getErrorPayload(); + const error: InvalidError = getErrorSchemaMock(); error.invalid_extra_data = 'invalid'; - const payload: RulesBulkSchema = [getBaseResponsePayload(), error]; + const payload: RulesBulkSchema = [getRulesSchemaMock(), error]; const decoded = rulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/__mocks__/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts similarity index 65% rename from x-pack/plugins/security_solution/common/detection_engine/schemas/response/__mocks__/utils.ts rename to x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts index fef6bcf42e49f0..ecbf0321cdc670 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts @@ -4,14 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RulesSchema } from '../rules_schema'; -import { RulesBulkSchema } from '../rules_bulk_schema'; -import { ErrorSchema } from '../error_schema'; -import { FindRulesSchema } from '../find_rules_schema'; +import { RulesSchema } from './rules_schema'; export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; -export const getBaseResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesSchema => ({ +export const getPartialRulesSchemaMock = (): Partial => ({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: ['some fake tag 1', 'some fake tag 2'], + to: 'now', + type: 'query', + note: '', +}); + +export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => ({ id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', created_at: new Date(anchorDate).toISOString(), updated_at: new Date(anchorDate).toISOString(), @@ -75,10 +98,8 @@ export const getBaseResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesS ], }); -export const getRulesBulkPayload = (): RulesBulkSchema => [getBaseResponsePayload()]; - -export const getMlRuleResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesSchema => { - const basePayload = getBaseResponsePayload(anchorDate); +export const getRulesMlSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => { + const basePayload = getRulesSchemaMock(anchorDate); const { filters, index, query, language, ...rest } = basePayload; return { @@ -88,20 +109,3 @@ export const getMlRuleResponsePayload = (anchorDate: string = ANCHOR_DATE): Rule machine_learning_job_id: 'some_machine_learning_job_id', }; }; - -export const getErrorPayload = ( - id: string = '819eded6-e9c8-445b-a647-519aea39e063' -): ErrorSchema => ({ - id, - error: { - status_code: 404, - message: 'id: "819eded6-e9c8-445b-a647-519aea39e063" not found', - }, -}); - -export const getFindResponseSingle = (): FindRulesSchema => ({ - page: 1, - perPage: 1, - total: 1, - data: [getBaseResponsePayload()], -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts index 8ed9c30507f4f8..90aef656db3695 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts @@ -18,28 +18,28 @@ import { addTimelineTitle, addMlFields, } from './rules_schema'; -import { getBaseResponsePayload, getMlRuleResponsePayload } from './__mocks__/utils'; import { exactCheck } from '../../../exact_check'; import { foldLeftRight, getPaths } from '../../../test_utils'; import { TypeAndTimelineOnly } from './type_timeline_only_schema'; +import { getRulesSchemaMock, getRulesMlSchemaMock } from './rules_schema.mocks'; export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; describe('rules_schema', () => { test('it should validate a type of "query" without anything extra', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); const decoded = rulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getBaseResponsePayload(); + const expected = getRulesSchemaMock(); expect(getPaths(left(message.errors))).toEqual([]); expect(message.schema).toEqual(expected); }); test('it should NOT validate a type of "query" when it has extra data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); payload.invalid_extra_data = 'invalid_extra_data'; const decoded = rulesSchema.decode(payload); @@ -51,7 +51,7 @@ describe('rules_schema', () => { }); test('it should NOT validate invalid_data for the type', () => { - const payload: Omit & { type: string } = getBaseResponsePayload(); + const payload: Omit & { type: string } = getRulesSchemaMock(); payload.type = 'invalid_data'; const decoded = rulesSchema.decode(payload); @@ -65,7 +65,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "query" with a saved_id together', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.type = 'query'; payload.saved_id = 'save id 123'; @@ -78,14 +78,14 @@ describe('rules_schema', () => { }); test('it should validate a type of "saved_query" with a "saved_id" dependent', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.type = 'saved_query'; payload.saved_id = 'save id 123'; const decoded = rulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getBaseResponsePayload(); + const expected = getRulesSchemaMock(); expected.type = 'saved_query'; expected.saved_id = 'save id 123'; @@ -95,7 +95,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "saved_query" without a "saved_id" dependent', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.type = 'saved_query'; delete payload.saved_id; @@ -110,7 +110,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "saved_query" when it has extra data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); payload.type = 'saved_query'; payload.saved_id = 'save id 123'; payload.invalid_extra_data = 'invalid_extra_data'; @@ -124,14 +124,14 @@ describe('rules_schema', () => { }); test('it should validate a type of "timeline_id" if there is a "timeline_title" dependent', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.timeline_id = 'some timeline id'; payload.timeline_title = 'some timeline title'; const decoded = rulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getBaseResponsePayload(); + const expected = getRulesSchemaMock(); expected.timeline_id = 'some timeline id'; expected.timeline_title = 'some timeline title'; @@ -140,7 +140,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "timeline_id" if there is "timeline_title" dependent when it has extra invalid data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); payload.timeline_id = 'some timeline id'; payload.timeline_title = 'some timeline title'; payload.invalid_extra_data = 'invalid_extra_data'; @@ -154,7 +154,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "timeline_id" if there is NOT a "timeline_title" dependent', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.timeline_id = 'some timeline id'; const decoded = rulesSchema.decode(payload); @@ -168,7 +168,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "timeline_title" if there is NOT a "timeline_id" dependent', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.timeline_title = 'some timeline title'; const decoded = rulesSchema.decode(payload); @@ -180,7 +180,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_title" but there is NOT a "timeline_id"', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.saved_id = 'some saved id'; payload.type = 'saved_query'; payload.timeline_title = 'some timeline title'; @@ -194,7 +194,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_id" but there is NOT a "timeline_title"', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.saved_id = 'some saved id'; payload.type = 'saved_query'; payload.timeline_id = 'some timeline id'; @@ -211,19 +211,19 @@ describe('rules_schema', () => { describe('checkTypeDependents', () => { test('it should validate a type of "query" without anything extra', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); const decoded = checkTypeDependents(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getBaseResponsePayload(); + const expected = getRulesSchemaMock(); expect(getPaths(left(message.errors))).toEqual([]); expect(message.schema).toEqual(expected); }); test('it should NOT validate invalid_data for the type', () => { - const payload: Omit & { type: string } = getBaseResponsePayload(); + const payload: Omit & { type: string } = getRulesSchemaMock(); payload.type = 'invalid_data'; const decoded = checkTypeDependents(payload); @@ -237,7 +237,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "query" with a saved_id together', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.type = 'query'; payload.saved_id = 'save id 123'; @@ -250,14 +250,14 @@ describe('rules_schema', () => { }); test('it should validate a type of "saved_query" with a "saved_id" dependent', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.type = 'saved_query'; payload.saved_id = 'save id 123'; const decoded = checkTypeDependents(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getBaseResponsePayload(); + const expected = getRulesSchemaMock(); expected.type = 'saved_query'; expected.saved_id = 'save id 123'; @@ -267,7 +267,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "saved_query" without a "saved_id" dependent', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.type = 'saved_query'; delete payload.saved_id; @@ -282,7 +282,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "saved_query" when it has extra data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); payload.type = 'saved_query'; payload.saved_id = 'save id 123'; payload.invalid_extra_data = 'invalid_extra_data'; @@ -296,14 +296,14 @@ describe('rules_schema', () => { }); test('it should validate a type of "timeline_id" if there is a "timeline_title" dependent', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.timeline_id = 'some timeline id'; payload.timeline_title = 'some timeline title'; const decoded = checkTypeDependents(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getBaseResponsePayload(); + const expected = getRulesSchemaMock(); expected.timeline_id = 'some timeline id'; expected.timeline_title = 'some timeline title'; @@ -312,7 +312,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "timeline_id" if there is "timeline_title" dependent when it has extra invalid data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); payload.timeline_id = 'some timeline id'; payload.timeline_title = 'some timeline title'; payload.invalid_extra_data = 'invalid_extra_data'; @@ -326,7 +326,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "timeline_id" if there is NOT a "timeline_title" dependent', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.timeline_id = 'some timeline id'; const decoded = checkTypeDependents(payload); @@ -340,7 +340,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "timeline_title" if there is NOT a "timeline_id" dependent', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.timeline_title = 'some timeline title'; const decoded = checkTypeDependents(payload); @@ -352,7 +352,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_title" but there is NOT a "timeline_id"', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.saved_id = 'some saved id'; payload.type = 'saved_query'; payload.timeline_title = 'some timeline title'; @@ -366,7 +366,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_id" but there is NOT a "timeline_title"', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.saved_id = 'some saved id'; payload.type = 'saved_query'; payload.timeline_id = 'some timeline id'; @@ -384,20 +384,20 @@ describe('rules_schema', () => { describe('getDependents', () => { test('it should validate a type of "query" without anything extra', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); const dependents = getDependents(payload); const decoded = dependents.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getBaseResponsePayload(); + const expected = getRulesSchemaMock(); expect(getPaths(left(message.errors))).toEqual([]); expect(message.schema).toEqual(expected); }); test('it should NOT validate invalid_data for the type', () => { - const payload: Omit & { type: string } = getBaseResponsePayload(); + const payload: Omit & { type: string } = getRulesSchemaMock(); payload.type = 'invalid_data'; const dependents = getDependents((payload as unknown) as TypeAndTimelineOnly); @@ -412,7 +412,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "query" with a saved_id together', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.type = 'query'; payload.saved_id = 'save id 123'; @@ -426,7 +426,7 @@ describe('rules_schema', () => { }); test('it should validate a type of "saved_query" with a "saved_id" dependent', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.type = 'saved_query'; payload.saved_id = 'save id 123'; @@ -434,7 +434,7 @@ describe('rules_schema', () => { const decoded = dependents.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getBaseResponsePayload(); + const expected = getRulesSchemaMock(); expected.type = 'saved_query'; expected.saved_id = 'save id 123'; @@ -444,7 +444,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "saved_query" without a "saved_id" dependent', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.type = 'saved_query'; delete payload.saved_id; @@ -460,7 +460,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "saved_query" when it has extra data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); payload.type = 'saved_query'; payload.saved_id = 'save id 123'; payload.invalid_extra_data = 'invalid_extra_data'; @@ -475,7 +475,7 @@ describe('rules_schema', () => { }); test('it should validate a type of "timeline_id" if there is a "timeline_title" dependent', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.timeline_id = 'some timeline id'; payload.timeline_title = 'some timeline title'; @@ -483,7 +483,7 @@ describe('rules_schema', () => { const decoded = dependents.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getBaseResponsePayload(); + const expected = getRulesSchemaMock(); expected.timeline_id = 'some timeline id'; expected.timeline_title = 'some timeline title'; @@ -492,7 +492,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "timeline_id" if there is "timeline_title" dependent when it has extra invalid data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); payload.timeline_id = 'some timeline id'; payload.timeline_title = 'some timeline title'; payload.invalid_extra_data = 'invalid_extra_data'; @@ -507,7 +507,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "timeline_id" if there is NOT a "timeline_title" dependent', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.timeline_id = 'some timeline id'; const dependents = getDependents(payload); @@ -522,7 +522,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "timeline_title" if there is NOT a "timeline_id" dependent', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.timeline_title = 'some timeline title'; const dependents = getDependents(payload); @@ -535,7 +535,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_title" but there is NOT a "timeline_id"', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.saved_id = 'some saved id'; payload.type = 'saved_query'; payload.timeline_title = 'some timeline title'; @@ -549,7 +549,7 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_id" but there is NOT a "timeline_title"', () => { - const payload = getBaseResponsePayload(); + const payload = getRulesSchemaMock(); payload.saved_id = 'some saved id'; payload.type = 'saved_query'; payload.timeline_id = 'some timeline id'; @@ -566,13 +566,13 @@ describe('rules_schema', () => { }); test('it validates an ML rule response', () => { - const payload = getMlRuleResponsePayload(); + const payload = getRulesMlSchemaMock(); const dependents = getDependents(payload); const decoded = dependents.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getMlRuleResponsePayload(); + const expected = getRulesMlSchemaMock(); expect(getPaths(left(message.errors))).toEqual([]); expect(message.schema).toEqual(expected); @@ -580,8 +580,8 @@ describe('rules_schema', () => { test('it rejects a response with both ML and query properties', () => { const payload = { - ...getBaseResponsePayload(), - ...getMlRuleResponsePayload(), + ...getRulesSchemaMock(), + ...getRulesMlSchemaMock(), }; const dependents = getDependents(payload); diff --git a/x-pack/plugins/security_solution/common/exact_check.ts b/x-pack/plugins/security_solution/common/exact_check.ts index 48afc35b56ba13..041d0fb324df73 100644 --- a/x-pack/plugins/security_solution/common/exact_check.ts +++ b/x-pack/plugins/security_solution/common/exact_check.ts @@ -47,7 +47,7 @@ export const exactCheck = ( return pipe(decoded, fold(onLeft, onRight)); }; -export const findDifferencesRecursive = (original: T, decodedValue: T): string[] => { +export const findDifferencesRecursive = (original: unknown, decodedValue: T): string[] => { if (decodedValue === null && original === null) { // both the decodedValue and the original are null which indicates that they are equal // so do not report differences diff --git a/x-pack/plugins/security_solution/cypress/README.md b/x-pack/plugins/security_solution/cypress/README.md index ad3af2aaa4e8a1..1a2ae696373b18 100644 --- a/x-pack/plugins/security_solution/cypress/README.md +++ b/x-pack/plugins/security_solution/cypress/README.md @@ -135,6 +135,17 @@ Note that with this type of execution you don't need to have running a kibana an As in this case we want to mimic a CI execution we want to execute the tests with the same set of data, this is why in this case does not make sense to override Cypress environment variables. +Note: To `run-as-ci` with the Cypress UI, update [x-pack/test/security_solution_cypress/runner.ts](https://github.com/elastic/kibana/blob/master/x-pack/test/security_solution_cypress/runner.ts#L25) from +``` ts +args: ['cypress:run'], +``` +to +``` ts +args: ['cypress:open'], +``` +This is helpful for debugging specific failed tests from CI without having to run the entire suite. + +Note: Please don't commit this change. ### Test data As mentioned above, when running the tests as Jenkins the tests are populated with data ("archives") found in: `x-pack/test/security_solution_cypress/es_archives`. diff --git a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts index 82b4f4f0fbe342..82395de91abfa9 100644 --- a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts @@ -32,7 +32,7 @@ import { resetFields, waitsForEventsToBeLoaded, } from '../tasks/hosts/events'; -import { clearSearchBar, kqlSearch } from '../tasks/siem_header'; +import { clearSearchBar, kqlSearch } from '../tasks/security_header'; import { HOSTS_PAGE } from '../urls/navigation'; diff --git a/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts index b058c9fb51fd1f..b0dbf94c0efb92 100644 --- a/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts @@ -28,7 +28,7 @@ import { resetFields, } from '../tasks/fields_browser'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/siem_main'; +import { openTimeline } from '../tasks/security_main'; import { openTimelineFieldsBrowser, populateTimeline } from '../tasks/timeline'; import { HOSTS_PAGE } from '../urls/navigation'; diff --git a/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts b/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts index d770eb6c761cfe..0529c797ee07a5 100644 --- a/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts @@ -5,14 +5,14 @@ */ import { - INSPECT_HOSTS_BUTTONS_IN_SIEM, + INSPECT_HOSTS_BUTTONS_IN_SECURITY, INSPECT_MODAL, - INSPECT_NETWORK_BUTTONS_IN_SIEM, + INSPECT_NETWORK_BUTTONS_IN_SECURITY, } from '../screens/inspect'; import { closesModal, openStatsAndTables } from '../tasks/inspect'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/siem_main'; +import { openTimeline } from '../tasks/security_main'; import { executeTimelineKQL, openTimelineInspectButton, @@ -30,7 +30,7 @@ describe('Inspect', () => { closesModal(); }); - INSPECT_HOSTS_BUTTONS_IN_SIEM.forEach((table) => + INSPECT_HOSTS_BUTTONS_IN_SECURITY.forEach((table) => it(`inspects the ${table.title}`, () => { openStatsAndTables(table); cy.get(INSPECT_MODAL).should('be.visible'); @@ -46,7 +46,7 @@ describe('Inspect', () => { closesModal(); }); - INSPECT_NETWORK_BUTTONS_IN_SIEM.forEach((table) => + INSPECT_NETWORK_BUTTONS_IN_SECURITY.forEach((table) => it(`inspects the ${table.title}`, () => { openStatsAndTables(table); cy.get(INSPECT_MODAL).should('be.visible'); diff --git a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts index e96c1fe6919663..785df4a0c2f9e6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KQL_INPUT } from '../screens/siem_header'; +import { KQL_INPUT } from '../screens/security_header'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; diff --git a/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts index a9e5a848d54a84..2014e34c118864 100644 --- a/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts @@ -3,14 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { DETECTIONS, HOSTS, NETWORK, OVERVIEW, TIMELINES } from '../screens/siem_header'; +import { DETECTIONS, HOSTS, NETWORK, OVERVIEW, TIMELINES } from '../screens/security_header'; import { loginAndWaitForPage } from '../tasks/login'; -import { navigateFromHeaderTo } from '../tasks/siem_header'; +import { navigateFromHeaderTo } from '../tasks/security_header'; import { TIMELINES_PAGE } from '../urls/navigation'; -describe('top-level navigation common to all pages in the SIEM app', () => { +describe('top-level navigation common to all pages in the Security app', () => { before(() => { loginAndWaitForPage(TIMELINES_PAGE); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts index 55bcbfafaa0923..284deb67e1386b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts @@ -13,7 +13,7 @@ import { OVERVIEW_PAGE } from '../urls/navigation'; describe('Overview Page', () => { before(() => { - cy.stubSIEMapi('overview'); + cy.stubSecurityApi('overview'); loginAndWaitForPage(OVERVIEW_PAGE); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts index e430520fe1dc4a..5dc3182cd9f836 100644 --- a/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts @@ -12,7 +12,7 @@ import { openAuthentications, openUncommonProcesses } from '../tasks/hosts/main' import { waitForUncommonProcessesToBeLoaded } from '../tasks/hosts/uncommon_processes'; import { loginAndWaitForPage } from '../tasks/login'; import { goToFirstPage, goToThirdPage } from '../tasks/pagination'; -import { refreshPage } from '../tasks/siem_header'; +import { refreshPage } from '../tasks/security_header'; import { HOSTS_PAGE_TAB_URLS } from '../urls/navigation'; diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts index 243886752706de..33394760c4da90 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts @@ -19,7 +19,7 @@ import { } from '../tasks/hosts/all_hosts'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/siem_main'; +import { openTimeline } from '../tasks/security_main'; import { createNewTimeline } from '../tasks/timeline'; import { HOSTS_PAGE } from '../urls/navigation'; diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts index 02da7cbc284629..e462d6ade5dc44 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts @@ -8,7 +8,7 @@ import { TIMELINE_FLYOUT_HEADER, TIMELINE_NOT_READY_TO_DROP_BUTTON } from '../sc import { dragFirstHostToTimeline, waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline, openTimelineIfClosed } from '../tasks/siem_main'; +import { openTimeline, openTimelineIfClosed } from '../tasks/security_main'; import { createNewTimeline } from '../tasks/timeline'; import { HOSTS_PAGE } from '../urls/navigation'; @@ -29,14 +29,13 @@ describe('timeline flyout button', () => { cy.get(TIMELINE_FLYOUT_HEADER).should('have.css', 'visibility', 'visible'); }); - // FLAKY: https://github.com/elastic/kibana/issues/60369 - it.skip('sets the flyout button background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the flyout button', () => { + it('sets the flyout button background to euiColorSuccess with a 20% alpha channel when the user starts dragging a host, but is not hovering over the flyout button', () => { dragFirstHostToTimeline(); cy.get(TIMELINE_NOT_READY_TO_DROP_BUTTON).should( 'have.css', 'background', - 'rgba(1, 125, 115, 0.1) none repeat scroll 0% 0% / auto padding-box border-box' + 'rgba(1, 125, 115, 0.2) none repeat scroll 0% 0% / auto padding-box border-box' ); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts index 0668b91ca4fc1f..00994f7a87a7bf 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts @@ -7,7 +7,7 @@ import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/siem_main'; +import { openTimeline } from '../tasks/security_main'; import { executeTimelineKQL } from '../tasks/timeline'; import { HOSTS_PAGE } from '../urls/navigation'; diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts index 7b2c6f3b55b2e9..841d41782b3509 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts @@ -12,7 +12,7 @@ import { } from '../screens/timeline'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/siem_main'; +import { openTimeline } from '../tasks/security_main'; import { checkIdToggleField, createNewTimeline, diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index cf69a83dbf9517..425ed23bdafec0 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -13,7 +13,7 @@ import { } from '../screens/date_picker'; import { HOSTS_NAMES } from '../screens/hosts/all_hosts'; import { ANOMALIES_TAB } from '../screens/hosts/main'; -import { BREADCRUMBS, HOSTS, KQL_INPUT, NETWORK } from '../screens/siem_header'; +import { BREADCRUMBS, HOSTS, KQL_INPUT, NETWORK } from '../screens/security_header'; import { SERVER_SIDE_EVENT_COUNT, TIMELINE_TITLE } from '../screens/timeline'; import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../tasks/login'; @@ -29,8 +29,8 @@ import { openFirstHostDetails, waitForAllHostsToBeLoaded } from '../tasks/hosts/ import { openAllHosts } from '../tasks/hosts/main'; import { waitForIpsTableToBeLoaded } from '../tasks/network/flows'; -import { clearSearchBar, kqlSearch, navigateFromHeaderTo } from '../tasks/siem_header'; -import { openTimeline } from '../tasks/siem_main'; +import { clearSearchBar, kqlSearch, navigateFromHeaderTo } from '../tasks/security_header'; +import { openTimeline } from '../tasks/security_main'; import { addDescriptionToTimeline, addNameToTimeline, @@ -243,7 +243,7 @@ describe('url state', () => { cy.wrap(intCount).should('be.above', 0); }); - const timelineName = 'SIEM'; + const timelineName = 'Security'; addNameToTimeline(timelineName); addDescriptionToTimeline('This is the best timeline of the world'); cy.wait(5000); diff --git a/x-pack/plugins/security_solution/cypress/screens/inspect.ts b/x-pack/plugins/security_solution/cypress/screens/inspect.ts index 8f2ff4ef401e7e..f472509bda6b5f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/inspect.ts +++ b/x-pack/plugins/security_solution/cypress/screens/inspect.ts @@ -14,7 +14,7 @@ export interface InspectButtonMetadata { tabId?: string; } -export const INSPECT_HOSTS_BUTTONS_IN_SIEM: InspectButtonMetadata[] = [ +export const INSPECT_HOSTS_BUTTONS_IN_SECURITY: InspectButtonMetadata[] = [ { id: '[data-test-subj="stat-hosts"]', title: 'Hosts Stat', @@ -50,7 +50,7 @@ export const INSPECT_HOSTS_BUTTONS_IN_SIEM: InspectButtonMetadata[] = [ }, ]; -export const INSPECT_NETWORK_BUTTONS_IN_SIEM: InspectButtonMetadata[] = [ +export const INSPECT_NETWORK_BUTTONS_IN_SECURITY: InspectButtonMetadata[] = [ { id: '[data-test-subj="stat-networkEvents"]', title: 'Network events Stat', diff --git a/x-pack/plugins/security_solution/cypress/screens/siem_header.ts b/x-pack/plugins/security_solution/cypress/screens/security_header.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/screens/siem_header.ts rename to x-pack/plugins/security_solution/cypress/screens/security_header.ts diff --git a/x-pack/plugins/security_solution/cypress/screens/siem_main.ts b/x-pack/plugins/security_solution/cypress/screens/security_main.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/screens/siem_main.ts rename to x-pack/plugins/security_solution/cypress/screens/security_main.ts diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js index d086981cb98f76..8b75f068a53da2 100644 --- a/x-pack/plugins/security_solution/cypress/support/commands.js +++ b/x-pack/plugins/security_solution/cypress/support/commands.js @@ -30,7 +30,7 @@ // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) -Cypress.Commands.add('stubSIEMapi', function (dataFileName) { +Cypress.Commands.add('stubSecurityApi', function (dataFileName) { cy.on('window:before:load', (win) => { // @ts-ignore no null, this is a temp hack see issue above win.fetch = null; diff --git a/x-pack/plugins/security_solution/cypress/support/index.d.ts b/x-pack/plugins/security_solution/cypress/support/index.d.ts index 5d5173170a9f96..12c11ffd277504 100644 --- a/x-pack/plugins/security_solution/cypress/support/index.d.ts +++ b/x-pack/plugins/security_solution/cypress/support/index.d.ts @@ -6,6 +6,6 @@ declare namespace Cypress { interface Chainable { - stubSIEMapi(dataFileName: string): Chainable; + stubSecurityApi(dataFileName: string): Chainable; } } diff --git a/x-pack/plugins/security_solution/cypress/tasks/configure_cases.ts b/x-pack/plugins/security_solution/cypress/tasks/configure_cases.ts index 8ff8fbf4b0cb70..b39cbeba7b85e7 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/configure_cases.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/configure_cases.ts @@ -15,7 +15,7 @@ import { URL, USERNAME, } from '../screens/configure_cases'; -import { MAIN_PAGE } from '../screens/siem_main'; +import { MAIN_PAGE } from '../screens/security_main'; import { Connector } from '../objects/case'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/detections.ts b/x-pack/plugins/security_solution/cypress/tasks/detections.ts index f53dd83635d850..a6596bfbcc3e97 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/detections.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/detections.ts @@ -16,7 +16,7 @@ import { ALERTS, ALERT_CHECKBOX, } from '../screens/detections'; -import { REFRESH_BUTTON } from '../screens/siem_header'; +import { REFRESH_BUTTON } from '../screens/security_header'; export const closeFirstAlert = () => { cy.get(OPEN_CLOSE_ALERT_BTN).first().click({ force: true }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/hosts/authentications.ts b/x-pack/plugins/security_solution/cypress/tasks/hosts/authentications.ts index 7ba7f227964ab4..cd64fe4ff1726d 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/hosts/authentications.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/hosts/authentications.ts @@ -5,7 +5,7 @@ */ import { AUTHENTICATIONS_TABLE } from '../../screens/hosts/authentications'; -import { REFRESH_BUTTON } from '../../screens/siem_header'; +import { REFRESH_BUTTON } from '../../screens/security_header'; export const waitForAuthenticationsToBeLoaded = () => { cy.get(AUTHENTICATIONS_TABLE).should('exist'); diff --git a/x-pack/plugins/security_solution/cypress/tasks/hosts/uncommon_processes.ts b/x-pack/plugins/security_solution/cypress/tasks/hosts/uncommon_processes.ts index c86b169d60a132..598def9ed41d06 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/hosts/uncommon_processes.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/hosts/uncommon_processes.ts @@ -5,7 +5,7 @@ */ import { UNCOMMON_PROCESSES_TABLE } from '../../screens/hosts/uncommon_processes'; -import { REFRESH_BUTTON } from '../../screens/siem_header'; +import { REFRESH_BUTTON } from '../../screens/security_header'; export const waitForUncommonProcessesToBeLoaded = () => { cy.get(UNCOMMON_PROCESSES_TABLE).should('exist'); diff --git a/x-pack/plugins/security_solution/cypress/tasks/siem_header.ts b/x-pack/plugins/security_solution/cypress/tasks/security_header.ts similarity index 90% rename from x-pack/plugins/security_solution/cypress/tasks/siem_header.ts rename to x-pack/plugins/security_solution/cypress/tasks/security_header.ts index 2cc9199a42bbbf..7427104a9d889f 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/siem_header.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/security_header.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KQL_INPUT, REFRESH_BUTTON } from '../screens/siem_header'; +import { KQL_INPUT, REFRESH_BUTTON } from '../screens/security_header'; export const clearSearchBar = () => { cy.get(KQL_INPUT).clear().type('{enter}'); diff --git a/x-pack/plugins/security_solution/cypress/tasks/siem_main.ts b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts similarity index 86% rename from x-pack/plugins/security_solution/cypress/tasks/siem_main.ts rename to x-pack/plugins/security_solution/cypress/tasks/security_main.ts index eece7edcb2b442..47b73db8b96dfe 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/siem_main.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MAIN_PAGE, TIMELINE_TOGGLE_BUTTON } from '../screens/siem_main'; +import { MAIN_PAGE, TIMELINE_TOGGLE_BUTTON } from '../screens/security_main'; export const openTimeline = () => { cy.get(TIMELINE_TOGGLE_BUTTON).click(); diff --git a/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson b/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson index c2e779feeca773..dcbfa9d0dd16ef 100644 --- a/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson +++ b/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson @@ -1,2 +1,2 @@ -{"actions":[],"created_at":"2020-03-26T10:09:07.569Z","updated_at":"2020-03-26T10:09:08.021Z","created_by":"elastic","description":"Rule 1","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","id":"49db5bd1-bdd5-4821-be26-bb70a815dedb","immutable":false,"index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"0cea4194-03f2-4072-b281-d31b72221d9d","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":50,"name":"Rule 1","query":"host.name:*","references":[],"meta":{"from":"1m","throttle":"no_actions"},"severity":"low","updated_by":"elastic","tags":["rule1"],"to":"now","type":"query","threat":[],"throttle":"no_actions","version":1} +{"actions":[],"created_at":"2020-03-26T10:09:07.569Z","updated_at":"2020-03-26T10:09:08.021Z","created_by":"elastic","description":"Rule 1","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","id":"49db5bd1-bdd5-4821-be26-bb70a815dedb","immutable":false,"index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"0cea4194-03f2-4072-b281-d31b72221d9d","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":50,"name":"Rule 1","query":"host.name:*","references":[],"meta":{"from":"1m","throttle":"no_actions"},"severity":"low","updated_by":"elastic","tags":["rule1"],"to":"now","type":"query","threat":[],"throttle":"no_actions","version":1,"exceptions_list":[]} {"exported_count":1,"missing_rules":[],"missing_rules_count":0} diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 31abea53462fae..00000000000000 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,441 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is "multi" 1`] = ` - - - , - "title": "Severity", - }, - Object { - "description": 21, - "title": "Risk score", - }, - Object { - "description": -
    -
  • - - www.test.co - -
  • -
-
, - "title": "Reference URLs", - }, - ] - } - /> -
- - -
    -
  • - test -
  • -
- , - "title": "False positive examples", - }, - Object { - "description": - - - - - - - - - - - - - - , - "title": "MITRE ATT&CKâ„¢", - }, - Object { - "description": - - - tag1 - - - - - tag2 - - - , - "title": "Tags", - }, - Object { - "description": -
- # this is some markdown documentation -
-
, - "title": "Investigation guide", - }, - ] - } - /> -
-
-`; - -exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is "single" 1`] = ` - - - , - "title": "Severity", - }, - Object { - "description": 21, - "title": "Risk score", - }, - Object { - "description": -
    -
  • - - www.test.co - -
  • -
-
, - "title": "Reference URLs", - }, - Object { - "description": -
    -
  • - test -
  • -
-
, - "title": "False positive examples", - }, - Object { - "description": - - - - - - - - - - - - - - , - "title": "MITRE ATT&CKâ„¢", - }, - Object { - "description": - - - tag1 - - - - - tag2 - - - , - "title": "Tags", - }, - Object { - "description": -
- # this is some markdown documentation -
-
, - "title": "Investigation guide", - }, - ] - } - /> -
-
-`; - -exports[`description_step StepRuleDescriptionComponent renders correctly against snapshot when columns is "singleSplit 1`] = ` - - - , - "title": "Severity", - }, - Object { - "description": 21, - "title": "Risk score", - }, - Object { - "description": -
    -
  • - - www.test.co - -
  • -
-
, - "title": "Reference URLs", - }, - Object { - "description": -
    -
  • - test -
  • -
-
, - "title": "False positive examples", - }, - Object { - "description": - - - - - - - - - - - - - - , - "title": "MITRE ATT&CKâ„¢", - }, - Object { - "description": - - - tag1 - - - - - tag2 - - - , - "title": "Tags", - }, - Object { - "description": -
- # this is some markdown documentation -
-
, - "title": "Investigation guide", - }, - ] - } - type="column" - /> -
-
-`; diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.tsx index 091065eedfc223..a0d43c3abf5c17 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/helpers.tsx @@ -225,6 +225,10 @@ export const buildSeverityDescription = (label: string, value: string): ListItem }, ]; +const MyRefUrlLink = styled(EuiLink)` + word-break: break-word; +`; + export const buildUrlsDescription = (label: string, values: string[]): ListItems[] => { if (isNotEmptyArray(values)) { return [ @@ -237,9 +241,9 @@ export const buildUrlsDescription = (label: string, values: string[]): ListItems .filter((v) => !isEmpty(v)) .map((val, index) => (
  • - + {val} - +
  • ))} diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx index b8f81f6d7e5f7a..2bd90f17daf0c2 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx @@ -54,27 +54,24 @@ describe('description_step', () => { }); describe('StepRuleDescriptionComponent', () => { - test('renders correctly against snapshot when columns is "multi"', () => { + test('renders tow columns when "columns" is "multi"', () => { const wrapper = shallow( ); - expect(wrapper).toMatchSnapshot(); expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(2); }); - test('renders correctly against snapshot when columns is "single"', () => { + test('renders single column when "columns" is "single"', () => { const wrapper = shallow( ); - expect(wrapper).toMatchSnapshot(); expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); }); - test('renders correctly against snapshot when columns is "singleSplit', () => { + test('renders one column with title and description split when "columns" is "singleSplit', () => { const wrapper = shallow( ); - expect(wrapper).toMatchSnapshot(); expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); expect( wrapper.find('[data-test-subj="singleSplitStepRuleDescriptionList"]').at(0).prop('type') diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/translations.ts index b43ef8c615003f..4d2ba8b861cce9 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/translations.ts +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/translations.ts @@ -17,7 +17,7 @@ export const PRE_BUILT_MSG = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage', { defaultMessage: - 'Elastic SIEM comes with prebuilt detection rules that run in the background and create alerts when their conditions are met. By default, all prebuilt rules are disabled and you select which rules you want to activate.', + 'Elastic Security comes with prebuilt detection rules that run in the background and create alerts when their conditions are met. By default, all prebuilt rules are disabled and you select which rules you want to activate.', } ); diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/translations.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/translations.tsx index a371db72d2ee1e..860ed1831fdc6a 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/translations.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_define_rule/translations.tsx @@ -23,7 +23,7 @@ export const INVALID_CUSTOM_QUERY = i18n.translate( export const CONFIG_INDICES = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesFromConfigDescription', { - defaultMessage: 'Use Elasticsearch indices from SIEM advanced settings', + defaultMessage: 'Use Elasticsearch indices from Security Solution advanced settings', } ); @@ -38,7 +38,7 @@ export const INDEX_HELPER_TEXT = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesHelperDescription', { defaultMessage: - 'Enter the pattern of Elasticsearch indices where you would like this rule to run. By default, these will include index patterns defined in SIEM advanced settings.', + 'Enter the pattern of Elasticsearch indices where you would like this rule to run. By default, these will include index patterns defined in Security Solution advanced settings.', } ); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.tsx index fc1fee1077bd65..a353be80c7239f 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.tsx @@ -17,7 +17,7 @@ export const DetectionEngineUserUnauthenticated = React.memo(() => { = ({ )} )} + {ruleDetailTab === RuleDetailTabs.exceptions && ( + + )} {ruleDetailTab === RuleDetailTabs.failures && } diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/translations.ts index 9cf510f4a9b5d0..94dfdc3e9daa02 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/translations.ts @@ -89,3 +89,10 @@ export const TYPE_FAILED = i18n.translate( defaultMessage: 'Failed', } ); + +export const EXCEPTIONS_TAB = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab', + { + defaultMessage: 'Exceptions', + } +); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts index 32f59093e6a042..eee2aa9ff40cc2 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts @@ -485,7 +485,7 @@ export const IMPORT_RULE_BTN_TITLE = i18n.translate( export const SELECT_RULE = i18n.translate( 'xpack.securitySolution.detectionEngine.components.importRuleModal.selectRuleDescription', { - defaultMessage: 'Select a SIEM rule (as exported from the Detection Engine view) to import', + defaultMessage: 'Select a Security rule (as exported from the Detection Engine view) to import', } ); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/translations.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/translations.ts index a19422d0de2c1a..8e91d9848d1ed3 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/translations.ts +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/translations.ts @@ -48,7 +48,7 @@ export const PANEL_SUBTITLE_SHOWING = i18n.translate( export const EMPTY_TITLE = i18n.translate('xpack.securitySolution.detectionEngine.emptyTitle', { defaultMessage: - 'It looks like you don’t have any indices relevant to the detection engine in the SIEM application', + 'It looks like you don’t have any indices relevant to the detection engine in the Security application', }); export const EMPTY_ACTION_PRIMARY = i18n.translate( diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/__examples__/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/__examples__/index.stories.tsx index f939cf81d1bd3d..7465d3ca1e63ac 100644 --- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/__examples__/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/__examples__/index.stories.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { storiesOf } from '@storybook/react'; -import React from 'react'; +import React, { ReactNode } from 'react'; import { ThemeProvider } from 'styled-components'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; @@ -14,26 +14,21 @@ import { AndOrBadge } from '..'; const sampleText = 'Doggo ipsum i am bekom fat snoot wow such tempt waggy wags floofs, ruff heckin good boys and girls mlem. Ruff heckin good boys and girls mlem stop it fren borkf borking doggo very hand that feed shibe, you are doing me the shock big ol heck smol borking doggo with a long snoot for pats heckin good boys. You are doing me the shock smol borking doggo with a long snoot for pats wow very biscit, length boy. Doggo ipsum i am bekom fat snoot wow such tempt waggy wags floofs, ruff heckin good boys and girls mlem. Ruff heckin good boys and girls mlem stop it fren borkf borking doggo very hand that feed shibe, you are doing me the shock big ol heck smol borking doggo with a long snoot for pats heckin good boys.'; +const withTheme = (storyFn: () => ReactNode) => ( + ({ eui: euiLightVars, darkMode: true })}>{storyFn()} +); + storiesOf('components/AndOrBadge', module) - .add('and', () => ( - ({ eui: euiLightVars, darkMode: true })}> - - - )) - .add('or', () => ( - ({ eui: euiLightVars, darkMode: true })}> - - - )) + .addDecorator(withTheme) + .add('and', () => ) + .add('or', () => ) .add('antennas', () => ( - ({ eui: euiLightVars, darkMode: true })}> - - - - - -

    {sampleText}

    -
    -
    -
    + + + + + +

    {sampleText}

    +
    +
    )); diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx index ed918a59a514a0..f2c7d6884bae30 100644 --- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx @@ -20,8 +20,20 @@ describe('AndOrBadge', () => { ); expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND'); - expect(wrapper.find('EuiFlexItem[data-test-subj="andOrBadgeBarTop"]')).toHaveLength(1); - expect(wrapper.find('EuiFlexItem[data-test-subj="andOrBadgeBarBottom"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="andOrBadgeBarTop"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="andOrBadgeBarBottom"]').exists()).toBeTruthy(); + }); + + test('it does not render top and bottom antenna bars when "includeAntennas" is false', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('OR'); + expect(wrapper.find('[data-test-subj="andOrBadgeBarTop"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="andOrBadgeBarBottom"]').exists()).toBeFalsy(); }); test('it renders "and" when "type" is "and"', () => { @@ -32,7 +44,6 @@ describe('AndOrBadge', () => { ); expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND'); - expect(wrapper.find('EuiFlexItem[data-test-subj="and-or-badge-bar"]')).toHaveLength(0); }); test('it renders "or" when "type" is "or"', () => { @@ -43,6 +54,5 @@ describe('AndOrBadge', () => { ); expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('OR'); - expect(wrapper.find('EuiFlexItem[data-test-subj="and-or-badge-bar"]')).toHaveLength(0); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.tsx index ba3f880d9757eb..e427e57a2c6163 100644 --- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.tsx @@ -3,70 +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 { EuiFlexGroup, EuiBadge, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import styled, { css } from 'styled-components'; - -import * as i18n from './translations'; - -const AndOrBadgeAntenna = styled(EuiFlexItem)` - ${({ theme }) => css` - background: ${theme.eui.euiColorLightShade}; - position: relative; - width: 2px; - &:after { - background: ${theme.eui.euiColorLightShade}; - content: ''; - height: 8px; - right: -4px; - position: absolute; - width: 9px; - clip-path: circle(); - } - &.topAndOrBadgeAntenna { - &:after { - top: -1px; - } - } - &.bottomAndOrBadgeAntenna { - &:after { - bottom: -1px; - } - } - &.euiFlexItem { - margin: 0 12px 0 0; - } - `} -`; - -const EuiFlexItemWrapper = styled(EuiFlexItem)` - &.euiFlexItem { - margin: 0 12px 0 0; - } -`; -const RoundedBadge = (styled(EuiBadge)` - align-items: center; - border-radius: 100%; - display: inline-flex; - font-size: 9px; - height: 34px; - justify-content: center; - margin: 0 5px 0 5px; - padding: 7px 6px 4px 6px; - user-select: none; - width: 34px; - .euiBadge__content { - position: relative; - top: -1px; - } - .euiBadge__text { - text-overflow: clip; - } -` as unknown) as typeof EuiBadge; - -RoundedBadge.displayName = 'RoundedBadge'; +import { RoundedBadge } from './rounded_badge'; +import { RoundedBadgeAntenna } from './rounded_badge_antenna'; export type AndOr = 'and' | 'or'; @@ -74,34 +14,7 @@ export type AndOr = 'and' | 'or'; // Ref: https://github.com/elastic/eui/issues/1655 export const AndOrBadge = React.memo<{ type: AndOr; includeAntennas?: boolean }>( ({ type, includeAntennas = false }) => { - const getBadge = () => ( - - {type === 'and' ? i18n.AND : i18n.OR} - - ); - - const getBadgeWithAntennas = () => ( - - - {getBadge()} - - - ); - - return includeAntennas ? getBadgeWithAntennas() : getBadge(); + return includeAntennas ? : ; } ); diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.test.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.test.tsx new file mode 100644 index 00000000000000..14d9627d48ad70 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.test.tsx @@ -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 React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { RoundedBadge } from './rounded_badge'; + +describe('RoundedBadge', () => { + test('it renders "and" when "type" is "and"', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND'); + }); + + test('it renders "or" when "type" is "or"', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('OR'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.tsx new file mode 100644 index 00000000000000..1a03e8c73f2522 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.tsx @@ -0,0 +1,42 @@ +/* + * 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 { EuiBadge } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import * as i18n from './translations'; +import { AndOr } from '.'; + +const RoundBadge = (styled(EuiBadge)` + align-items: center; + border-radius: 100%; + display: inline-flex; + font-size: 9px; + height: 34px; + justify-content: center; + margin: 0 5px 0 5px; + padding: 7px 6px 4px 6px; + user-select: none; + width: 34px; + .euiBadge__content { + position: relative; + top: -1px; + } + .euiBadge__text { + text-overflow: clip; + } +` as unknown) as typeof EuiBadge; + +RoundBadge.displayName = 'RoundBadge'; + +export const RoundedBadge: React.FC<{ type: AndOr }> = ({ type }) => ( + + {type === 'and' ? i18n.AND : i18n.OR} + +); + +RoundedBadge.displayName = 'RoundedBadge'; diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.test.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.test.tsx new file mode 100644 index 00000000000000..e6362f8798fa61 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.test.tsx @@ -0,0 +1,46 @@ +/* + * 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 React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { RoundedBadgeAntenna } from './rounded_badge_antenna'; + +describe('RoundedBadgeAntenna', () => { + test('it renders top and bottom antenna bars', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND'); + expect(wrapper.find('[data-test-subj="andOrBadgeBarTop"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="andOrBadgeBarBottom"]').exists()).toBeTruthy(); + }); + + test('it renders "and" when "type" is "and"', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND'); + }); + + test('it renders "or" when "type" is "or"', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('OR'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.tsx new file mode 100644 index 00000000000000..1076d8b41b9556 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.tsx @@ -0,0 +1,62 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import styled, { css } from 'styled-components'; + +import { RoundedBadge } from './rounded_badge'; +import { AndOr } from '.'; + +const antennaStyles = css` + background: ${({ theme }) => theme.eui.euiColorLightShade}; + position: relative; + width: 2px; + margin: 0 12px 0 0; + &:after { + background: ${({ theme }) => theme.eui.euiColorLightShade}; + content: ''; + height: 8px; + right: -4px; + position: absolute; + width: 10px; + clip-path: circle(); + } +`; + +const TopAntenna = styled(EuiFlexItem)` + ${antennaStyles} + &:after { + top: 0; + } +`; +const BottomAntenna = styled(EuiFlexItem)` + ${antennaStyles} + &:after { + bottom: 0; + } +`; + +const EuiFlexItemWrapper = styled(EuiFlexItem)` + margin: 0 12px 0 0; +`; + +export const RoundedBadgeAntenna: React.FC<{ type: AndOr }> = ({ type }) => ( + + + + + + + +); + +RoundedBadgeAntenna.displayName = 'RoundedBadgeAntenna'; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index 4eeefdbe2fca0b..f916f42fe41cdc 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -13,7 +13,7 @@ import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { useKibana } from '../../lib/kibana'; import { createFilter } from '../add_filter_to_global_search_bar'; -import { StatefulTopN } from '../top_n'; +import { ACTIVE_TIMELINE_REDUX_ID, StatefulTopN } from '../top_n'; import { allowTopN } from './helpers'; import * as i18n from './translations'; @@ -34,7 +34,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ field, onFilterAdded, showTopN, - timelineId, + timelineId = ACTIVE_TIMELINE_REDUX_ID, toggleTopN, value, }) => { @@ -44,13 +44,11 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ kibana.services.data.query.filterManager, ]); const { getTimelineFilterManager } = useManageTimeline(); - const filterManager = useMemo( - () => - timelineId - ? getTimelineFilterManager(timelineId) ?? filterManagerBackup - : filterManagerBackup, - [timelineId, getTimelineFilterManager, filterManagerBackup] - ); + const filterManager = useMemo(() => getTimelineFilterManager(timelineId) ?? filterManagerBackup, [ + timelineId, + getTimelineFilterManager, + filterManagerBackup, + ]); const filterForValue = useCallback(() => { const filter = value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/__examples__/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/__examples__/index.stories.tsx deleted file mode 100644 index b6620ed103bc84..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/__examples__/index.stories.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { storiesOf } from '@storybook/react'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; - -import { ExceptionItem } from '../viewer'; -import { Operator } from '../types'; -import { getExceptionItemMock } from '../mocks'; - -storiesOf('components/exceptions', module) - .add('ExceptionItem/with os', () => { - const payload = getExceptionItemMock(); - payload.description = ''; - payload.comments = []; - payload.entries = [ - { - field: 'actingProcess.file.signer', - type: 'match', - operator: Operator.INCLUSION, - value: 'Elastic, N.V.', - }, - ]; - - return ( - ({ eui: euiLightVars, darkMode: false })}> - {}} - handleEdit={() => {}} - /> - - ); - }) - .add('ExceptionItem/with description', () => { - const payload = getExceptionItemMock(); - payload._tags = []; - payload.comments = []; - payload.entries = [ - { - field: 'actingProcess.file.signer', - type: 'match', - operator: Operator.INCLUSION, - value: 'Elastic, N.V.', - }, - ]; - - return ( - ({ eui: euiLightVars, darkMode: false })}> - {}} - handleEdit={() => {}} - /> - - ); - }) - .add('ExceptionItem/with comments', () => { - const payload = getExceptionItemMock(); - payload._tags = []; - payload.description = ''; - payload.entries = [ - { - field: 'actingProcess.file.signer', - type: 'match', - operator: Operator.INCLUSION, - value: 'Elastic, N.V.', - }, - ]; - - return ( - ({ eui: euiLightVars, darkMode: false })}> - {}} - handleEdit={() => {}} - /> - - ); - }) - .add('ExceptionItem/with nested entries', () => { - const payload = getExceptionItemMock(); - payload._tags = []; - payload.description = ''; - payload.comments = []; - - return ( - ({ eui: euiLightVars, darkMode: false })}> - {}} - handleEdit={() => {}} - /> - - ); - }) - .add('ExceptionItem/with everything', () => { - const payload = getExceptionItemMock(); - - return ( - ({ eui: euiLightVars, darkMode: false })}> - {}} - handleEdit={() => {}} - /> - - ); - }); 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 223eabb0ea4eeb..2893c7dc961f26 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 @@ -10,7 +10,7 @@ import moment from 'moment-timezone'; import { getOperatorType, getExceptionOperatorSelect, - determineIfIsNested, + isEntryNested, getFormattedEntries, formatEntry, getOperatingSystems, @@ -159,21 +159,21 @@ describe('Exception helpers', () => { }); }); - describe('#determineIfIsNested', () => { + describe('#isEntryNested', () => { test('it returns true if type NestedExceptionEntry', () => { const payload: NestedExceptionEntry = { field: 'actingProcess.file.signer', type: 'nested', entries: [], }; - const result = determineIfIsNested(payload); + const result = isEntryNested(payload); expect(result).toBeTruthy(); }); test('it returns false if NOT type NestedExceptionEntry', () => { const payload = getExceptionItemEntryMock(); - const result = determineIfIsNested(payload); + const result = isEntryNested(payload); expect(result).toBeFalsy(); }); @@ -439,7 +439,7 @@ describe('Exception helpers', () => { describe('#getFormattedComments', () => { test('it returns formatted comment object with username and timestamp', () => { - const payload = getExceptionItemMock().comments; + const payload = getExceptionItemMock().comment; const result = getFormattedComments(payload); expect(result[0].username).toEqual('user_name'); @@ -447,7 +447,7 @@ describe('Exception helpers', () => { }); test('it returns formatted timeline icon with comment users initial', () => { - const payload = getExceptionItemMock().comments; + const payload = getExceptionItemMock().comment; const result = getFormattedComments(payload); const wrapper = mount(result[0].timelineIcon as React.ReactElement); @@ -456,7 +456,7 @@ describe('Exception helpers', () => { }); test('it returns comment text', () => { - const payload = getExceptionItemMock().comments; + const payload = getExceptionItemMock().comment; const result = getFormattedComments(payload); const wrapper = mount(result[0].children as React.ReactElement); 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 bd22de636bf6c8..155c8a3e2926c0 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 @@ -56,7 +56,7 @@ export const getExceptionOperatorSelect = (entry: ExceptionEntry): OperatorOptio return foundOperator ?? isOperator; }; -export const determineIfIsNested = ( +export const isEntryNested = ( tbd: ExceptionEntry | NestedExceptionEntry ): tbd is NestedExceptionEntry => { if (tbd.type === 'nested') { @@ -75,7 +75,7 @@ export const getFormattedEntries = ( entries: Array ): FormattedEntry[] => { const formattedEntries = entries.map((entry) => { - if (determineIfIsNested(entry)) { + if (isEntryNested(entry)) { const parent = { fieldName: entry.field, operator: null, value: null, isNested: false }; return entry.entries.reduce( (acc, nestedEntry) => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/mocks.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/mocks.ts index 15aec3533b325d..0dba3fd26c487b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/mocks.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/mocks.ts @@ -11,6 +11,25 @@ import { NestedExceptionEntry, FormattedEntry, } from './types'; +import { ExceptionList } from '../../../lists_plugin_deps'; + +export const getExceptionListMock = (): ExceptionList => ({ + id: '5b543420', + created_at: '2020-04-23T00:19:13.289Z', + created_by: 'user_name', + list_id: 'test-exception', + tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', + updated_at: '2020-04-23T00:19:13.289Z', + updated_by: 'user_name', + namespace_type: 'single', + name: '', + description: 'This is a description', + _tags: ['os:windows'], + tags: [], + type: 'endpoint', + meta: {}, + totalItems: 0, +}); export const getExceptionItemEntryMock = (): ExceptionEntry => ({ field: 'actingProcess.file.signer', @@ -44,7 +63,7 @@ export const getExceptionItemMock = (): ExceptionListItemSchema => ({ namespace_type: 'single', name: '', description: 'This is a description', - comments: [ + comment: [ { user: 'user_name', timestamp: '2020-04-23T00:19:13.289Z', 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 704849430daf91..27dab7cf9db291 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 @@ -5,6 +5,17 @@ */ import { i18n } from '@kbn/i18n'; +export const DETECTION_LIST = i18n.translate( + 'xpack.securitySolution.exceptions.detectionListLabel', + { + defaultMessage: 'Detection list', + } +); + +export const ENDPOINT_LIST = i18n.translate('xpack.securitySolution.exceptions.endpointListLabel', { + defaultMessage: 'Endpoint list', +}); + export const EDIT = i18n.translate('xpack.securitySolution.exceptions.editButtonLabel', { defaultMessage: 'Edit', }); @@ -47,3 +58,82 @@ export const OPERATING_SYSTEM = i18n.translate( defaultMessage: 'OS', } ); + +export const SEARCH_DEFAULT = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder', + { + defaultMessage: 'Search field (ex: host.name)', + } +); + +export const ADD_EXCEPTION_LABEL = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.addExceptionLabel', + { + defaultMessage: 'Add new exception', + } +); + +export const ADD_TO_ENDPOINT_LIST = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.addToEndpointListLabel', + { + defaultMessage: 'Add to endpoint list', + } +); + +export const ADD_TO_DETECTIONS_LIST = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel', + { + defaultMessage: 'Add to detections list', + } +); + +export const EXCEPTION_EMPTY_PROMPT_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.emptyPromptTitle', + { + defaultMessage: 'You have no exceptions', + } +); + +export const EXCEPTION_EMPTY_PROMPT_BODY = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.emptyPromptBody', + { + defaultMessage: + 'You can add an exception to fine tune the rule so that it suppresses alerts that meet specified conditions. Exceptions leverage detection accuracy, which can help reduce the number of false positives.', + } +); + +export const FETCH_LIST_ERROR = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.fetchingListError', + { + defaultMessage: 'Error fetching exceptions', + } +); + +export const DELETE_EXCEPTION_ERROR = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.deleteExceptionError', + { + defaultMessage: 'Error deleting exception', + } +); + +export const ITEMS_PER_PAGE = (items: number) => + i18n.translate('xpack.securitySolution.exceptions.exceptionsPaginationLabel', { + values: { items }, + defaultMessage: 'Items per page: {items}', + }); + +export const NUMBER_OF_ITEMS = (items: number) => + i18n.translate('xpack.securitySolution.exceptions.paginationNumberOfItemsLabel', { + values: { items }, + defaultMessage: '{items} items', + }); + +export const REFRESH = i18n.translate('xpack.securitySolution.exceptions.utilityRefreshLabel', { + defaultMessage: 'Refresh', +}); + +export const SHOWING_EXCEPTIONS = (items: number) => + i18n.translate('xpack.securitySolution.exceptions.utilityNumberExceptionsLabel', { + values: { items }, + defaultMessage: 'Showing {items} {items, plural, =1 {exception} other {exceptions}}', + }); 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 e8393610e459d5..d60d1ef71e502e 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 @@ -5,6 +5,12 @@ */ import { ReactNode } from 'react'; +import { + NamespaceType, + ExceptionList, + ExceptionListItemSchema as ExceptionItem, +} from '../../../lists_plugin_deps'; + export interface OperatorOption { message: string; value: string; @@ -56,10 +62,51 @@ export interface Comment { comment: string; } +export enum ExceptionListType { + DETECTION_ENGINE = 'detection', + ENDPOINT = 'endpoint', +} + +export interface FilterOptions { + filter: string; + showDetectionsList: boolean; + showEndpointList: boolean; + tags: string[]; +} + +export interface Filter { + filter: Partial; + pagination: Partial; +} + +export interface SetExceptionsProps { + lists: ExceptionList[]; + exceptions: ExceptionItem[]; + pagination: Pagination; +} + +export interface ApiProps { + id: string; + namespaceType: NamespaceType; +} + +export interface Pagination { + page: number; + perPage: number; + total: number; +} + +export interface ExceptionsPagination { + pageIndex: number; + pageSize: number; + totalItemCount: number; + pageSizeOptions: number[]; +} + // TODO: Delete once types are updated export interface ExceptionListItemSchema { _tags: string[]; - comments: Comment[]; + comment: Comment[]; created_at: string; created_by: string; description?: string; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_details.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_details.tsx deleted file mode 100644 index 8745e80a215486..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_details.tsx +++ /dev/null @@ -1,85 +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 { EuiFlexItem, EuiFlexGroup, EuiDescriptionList, EuiButtonEmpty } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import styled, { css } from 'styled-components'; -import { transparentize } from 'polished'; - -import { ExceptionListItemSchema } from '../types'; -import { getDescriptionListContent } from '../helpers'; -import * as i18n from '../translations'; - -const StyledExceptionDetails = styled(EuiFlexItem)` - ${({ theme }) => css` - background-color: ${transparentize(0.95, theme.eui.euiColorPrimary)}; - padding: ${theme.eui.euiSize}; - - .euiDescriptionList__title.listTitle--width { - width: 40%; - } - - .euiDescriptionList__description.listDescription--width { - width: 60%; - } - `} -`; - -const ExceptionDetailsComponent = ({ - showComments, - onCommentsClick, - exceptionItem, -}: { - showComments: boolean; - exceptionItem: ExceptionListItemSchema; - onCommentsClick: () => void; -}): JSX.Element => { - const descriptionList = useMemo(() => getDescriptionListContent(exceptionItem), [exceptionItem]); - - const commentsSection = useMemo((): JSX.Element => { - const { comments } = exceptionItem; - if (comments.length > 0) { - return ( - - {!showComments - ? i18n.COMMENTS_SHOW(comments.length) - : i18n.COMMENTS_HIDE(comments.length)} - - ); - } else { - return <>; - } - }, [showComments, onCommentsClick, exceptionItem]); - - return ( - - - - - - {commentsSection} - - - ); -}; - -ExceptionDetailsComponent.displayName = 'ExceptionDetailsComponent'; - -export const ExceptionDetails = React.memo(ExceptionDetailsComponent); - -ExceptionDetails.displayName = 'ExceptionDetails'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_details.test.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx index 536d005c57b6e9..c5d2ffc7ac2bf0 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx @@ -11,7 +11,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import moment from 'moment-timezone'; import { ExceptionDetails } from './exception_details'; -import { getExceptionItemMock } from '../mocks'; +import { getExceptionItemMock } from '../../mocks'; describe('ExceptionDetails', () => { beforeEach(() => { @@ -24,7 +24,7 @@ describe('ExceptionDetails', () => { test('it renders no comments button if no comments exist', () => { const exceptionItem = getExceptionItemMock(); - exceptionItem.comments = []; + exceptionItem.comment = []; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> @@ -77,7 +77,7 @@ describe('ExceptionDetails', () => { test('it renders comments plural if more than one', () => { const exceptionItem = getExceptionItemMock(); - exceptionItem.comments = [ + exceptionItem.comment = [ { user: 'user_1', timestamp: '2020-04-23T00:19:13.289Z', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx new file mode 100644 index 00000000000000..ce7d1d499de6ea --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx @@ -0,0 +1,93 @@ +/* + * 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 { + EuiFlexItem, + EuiFlexGroup, + EuiDescriptionList, + EuiButtonEmpty, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; +import React, { useMemo, Fragment } from 'react'; +import styled, { css } from 'styled-components'; + +import { DescriptionListItem, ExceptionListItemSchema } from '../../types'; +import { getDescriptionListContent } from '../../helpers'; +import * as i18n from '../../translations'; + +const MyExceptionDetails = styled(EuiFlexItem)` + ${({ theme }) => css` + background-color: ${theme.eui.euiColorLightestShade}; + padding: ${theme.eui.euiSize}; + `} +`; + +const MyDescriptionListTitle = styled(EuiDescriptionListTitle)` + width: 40%; +`; + +const MyDescriptionListDescription = styled(EuiDescriptionListDescription)` + width: 60%; +`; + +const ExceptionDetailsComponent = ({ + showComments, + onCommentsClick, + exceptionItem, +}: { + showComments: boolean; + exceptionItem: ExceptionListItemSchema; + onCommentsClick: () => void; +}): JSX.Element => { + const descriptionListItems = useMemo( + (): DescriptionListItem[] => getDescriptionListContent(exceptionItem), + [exceptionItem] + ); + + const commentsSection = useMemo((): JSX.Element => { + // TODO: return back to exceptionItem.comments once updated + const { comment } = exceptionItem; + if (comment.length > 0) { + return ( + + {!showComments ? i18n.COMMENTS_SHOW(comment.length) : i18n.COMMENTS_HIDE(comment.length)} + + ); + } else { + return <>; + } + }, [showComments, onCommentsClick, exceptionItem]); + + return ( + + + + + {descriptionListItems.map((item) => ( + + {item.title} + {item.description} + + ))} + + + {commentsSection} + + + ); +}; + +ExceptionDetailsComponent.displayName = 'ExceptionDetailsComponent'; + +export const ExceptionDetails = React.memo(ExceptionDetailsComponent); + +ExceptionDetails.displayName = 'ExceptionDetails'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_entries.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx similarity index 67% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_entries.test.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx index e0c62f51d032ad..2d022591d99800 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_entries.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx @@ -10,17 +10,18 @@ import { mount } from 'enzyme'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { ExceptionEntries } from './exception_entries'; -import { getFormattedEntryMock } from '../mocks'; -import { getEmptyValue } from '../../empty_value'; +import { getFormattedEntryMock } from '../../mocks'; +import { getEmptyValue } from '../../../empty_value'; describe('ExceptionEntries', () => { test('it does NOT render the and badge if only one exception item entry exists', () => { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> ); @@ -32,9 +33,10 @@ describe('ExceptionEntries', () => { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> ); @@ -42,38 +44,73 @@ describe('ExceptionEntries', () => { expect(wrapper.find('[data-test-subj="exceptionsViewerAndBadge"]')).toHaveLength(1); }); - test('it invokes "handlEdit" when edit button clicked', () => { - const mockHandleEdit = jest.fn(); + test('it invokes "onEdit" when edit button clicked', () => { + const mockOnEdit = jest.fn(); const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> ); const editBtn = wrapper.find('[data-test-subj="exceptionsViewerEditBtn"] button').at(0); editBtn.simulate('click'); - expect(mockHandleEdit).toHaveBeenCalledTimes(1); + expect(mockOnEdit).toHaveBeenCalledTimes(1); }); - test('it invokes "handleDelete" when delete button clicked', () => { - const mockHandleDelete = jest.fn(); + test('it invokes "onDelete" when delete button clicked', () => { + const mockOnDelete = jest.fn(); const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> ); const deleteBtn = wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0); deleteBtn.simulate('click'); - expect(mockHandleDelete).toHaveBeenCalledTimes(1); + expect(mockOnDelete).toHaveBeenCalledTimes(1); + }); + + test('it renders edit button disabled if "disableDelete" is "true"', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + const editBtn = wrapper.find('[data-test-subj="exceptionsViewerEditBtn"] button').at(0); + + expect(editBtn.prop('disabled')).toBeTruthy(); + }); + + test('it renders delete button in loading state if "disableDelete" is "true"', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + const deleteBtn = wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0); + + expect(deleteBtn.prop('disabled')).toBeTruthy(); + expect(deleteBtn.find('.euiLoadingSpinner')).toBeTruthy(); }); test('it renders nested entry', () => { @@ -84,9 +121,10 @@ describe('ExceptionEntries', () => { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> ); @@ -125,9 +163,10 @@ describe('ExceptionEntries', () => { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_entries.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx similarity index 76% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_entries.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx index fa21d61b06ebe5..58667f1f78b0d3 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_entries.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx @@ -11,21 +11,22 @@ import { EuiFlexGroup, EuiButton, EuiTableFieldDataColumnType, + EuiHideFor, } from '@elastic/eui'; import React, { useMemo } from 'react'; import styled, { css } from 'styled-components'; import { transparentize } from 'polished'; -import { AndOrBadge } from '../../and_or_badge'; -import { getEmptyValue } from '../../empty_value'; -import * as i18n from '../translations'; -import { FormattedEntry } from '../types'; +import { AndOrBadge } from '../../../and_or_badge'; +import { getEmptyValue } from '../../../empty_value'; +import * as i18n from '../../translations'; +import { FormattedEntry } from '../../types'; -const EntriesDetails = styled(EuiFlexItem)` +const MyEntriesDetails = styled(EuiFlexItem)` padding: ${({ theme }) => theme.eui.euiSize}; `; -const StyledEditButton = styled(EuiButton)` +const MyEditButton = styled(EuiButton)` ${({ theme }) => css` background-color: ${transparentize(0.9, theme.eui.euiColorPrimary)}; border: none; @@ -33,7 +34,7 @@ const StyledEditButton = styled(EuiButton)` `} `; -const StyledRemoveButton = styled(EuiButton)` +const MyRemoveButton = styled(EuiButton)` ${({ theme }) => css` background-color: ${transparentize(0.9, theme.eui.euiColorDanger)}; border: none; @@ -41,20 +42,22 @@ const StyledRemoveButton = styled(EuiButton)` `} `; -const AndOrBadgeContainer = styled(EuiFlexItem)` +const MyAndOrBadgeContainer = styled(EuiFlexItem)` padding-top: ${({ theme }) => theme.eui.euiSizeXL}; `; interface ExceptionEntriesComponentProps { entries: FormattedEntry[]; - handleDelete: () => void; - handleEdit: () => void; + disableDelete: boolean; + onDelete: () => void; + onEdit: () => void; } const ExceptionEntriesComponent = ({ entries, - handleDelete, - handleEdit, + disableDelete, + onDelete, + onEdit, }: ExceptionEntriesComponentProps): JSX.Element => { const columns = useMemo( (): Array> => [ @@ -63,6 +66,7 @@ const ExceptionEntriesComponent = ({ name: 'Field', sortable: false, truncateText: true, + textOnly: true, 'data-test-subj': 'exceptionFieldNameCell', width: '30%', render: (value: string | null, data: FormattedEntry) => { @@ -114,14 +118,20 @@ const ExceptionEntriesComponent = ({ ); return ( - + {entries.length > 1 && ( - - - + + + + + )} - + - {i18n.EDIT} - + - {i18n.REMOVE} - + - + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx new file mode 100644 index 00000000000000..de214b098d4ea3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx @@ -0,0 +1,131 @@ +/* + * 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 { storiesOf, addDecorator } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { ExceptionItem } from './'; +import { Operator } from '../../types'; +import { getExceptionItemMock } from '../../mocks'; + +addDecorator((storyFn) => ( + ({ eui: euiLightVars, darkMode: false })}>{storyFn()} +)); + +storiesOf('Components|ExceptionItem', module) + .add('with os', () => { + const payload = getExceptionItemMock(); + payload.description = ''; + payload.comment = []; + payload.entries = [ + { + field: 'actingProcess.file.signer', + type: 'match', + operator: Operator.INCLUSION, + value: 'Elastic, N.V.', + }, + ]; + + return ( + + ); + }) + .add('with description', () => { + const payload = getExceptionItemMock(); + payload._tags = []; + payload.comment = []; + payload.entries = [ + { + field: 'actingProcess.file.signer', + type: 'match', + operator: Operator.INCLUSION, + value: 'Elastic, N.V.', + }, + ]; + + return ( + + ); + }) + .add('with comments', () => { + const payload = getExceptionItemMock(); + payload._tags = []; + payload.description = ''; + payload.entries = [ + { + field: 'actingProcess.file.signer', + type: 'match', + operator: Operator.INCLUSION, + value: 'Elastic, N.V.', + }, + ]; + + return ( + + ); + }) + .add('with nested entries', () => { + const payload = getExceptionItemMock(); + payload._tags = []; + payload.description = ''; + payload.comment = []; + + return ( + + ); + }) + .add('with everything', () => { + const payload = getExceptionItemMock(); + + return ( + + ); + }) + .add('with loadingItemIds', () => { + const { id, namespace_type, ...rest } = getExceptionItemMock(); + + return ( + + ); + }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx new file mode 100644 index 00000000000000..b4de3639944b1a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx @@ -0,0 +1,124 @@ +/* + * 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 React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { ExceptionItem } from './'; +import { getExceptionItemMock } from '../../mocks'; + +describe('ExceptionItem', () => { + it('it renders ExceptionDetails and ExceptionEntries', () => { + const exceptionItem = getExceptionItemMock(); + + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('ExceptionDetails')).toHaveLength(1); + expect(wrapper.find('ExceptionEntries')).toHaveLength(1); + }); + + it('it invokes "onEditException" when edit button clicked', () => { + const mockOnEditException = jest.fn(); + const exceptionItem = getExceptionItemMock(); + + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + const editBtn = wrapper.find('[data-test-subj="exceptionsViewerEditBtn"] button').at(0); + editBtn.simulate('click'); + + expect(mockOnEditException).toHaveBeenCalledWith(getExceptionItemMock()); + }); + + it('it invokes "onDeleteException" when delete button clicked', () => { + const mockOnDeleteException = jest.fn(); + const exceptionItem = getExceptionItemMock(); + + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + const editBtn = wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0); + editBtn.simulate('click'); + + expect(mockOnDeleteException).toHaveBeenCalledWith({ + id: 'uuid_here', + namespaceType: 'single', + }); + }); + + it('it renders comment accordion closed to begin with', () => { + const mockOnDeleteException = jest.fn(); + const exceptionItem = getExceptionItemMock(); + + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('.euiAccordion-isOpen')).toHaveLength(0); + }); + + it('it renders comment accordion open when showComments is true', () => { + const mockOnDeleteException = jest.fn(); + const exceptionItem = getExceptionItemMock(); + + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + const commentsBtn = wrapper + .find('.euiButtonEmpty[data-test-subj="exceptionsViewerItemCommentsBtn"]') + .at(0); + commentsBtn.simulate('click'); + + expect(wrapper.find('.euiAccordion-isOpen')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx new file mode 100644 index 00000000000000..ba6ffb109be042 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx @@ -0,0 +1,112 @@ +/* + * 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 { + EuiPanel, + EuiFlexGroup, + EuiCommentProps, + EuiCommentList, + EuiAccordion, + EuiFlexItem, +} from '@elastic/eui'; +import React, { useEffect, useState, useMemo, useCallback } from 'react'; +import styled from 'styled-components'; + +import { ExceptionDetails } from './exception_details'; +import { ExceptionEntries } from './exception_entries'; +import { getFormattedEntries, getFormattedComments } from '../../helpers'; +import { FormattedEntry, ExceptionListItemSchema, ApiProps } from '../../types'; + +const MyFlexItem = styled(EuiFlexItem)` + &.comments--show { + padding: ${({ theme }) => theme.eui.euiSize}; + border-top: ${({ theme }) => `${theme.eui.euiBorderThin}`}; + } +`; + +interface ExceptionItemProps { + loadingItemIds: ApiProps[]; + exceptionItem: ExceptionListItemSchema; + commentsAccordionId: string; + onDeleteException: (arg: ApiProps) => void; + onEditException: (item: ExceptionListItemSchema) => void; +} + +const ExceptionItemComponent = ({ + loadingItemIds, + exceptionItem, + commentsAccordionId, + onDeleteException, + onEditException, +}: ExceptionItemProps): JSX.Element => { + const [entryItems, setEntryItems] = useState([]); + const [showComments, setShowComments] = useState(false); + + useEffect((): void => { + const formattedEntries = getFormattedEntries(exceptionItem.entries); + setEntryItems(formattedEntries); + }, [exceptionItem.entries]); + + const handleDelete = useCallback((): void => { + onDeleteException({ id: exceptionItem.id, namespaceType: exceptionItem.namespace_type }); + }, [onDeleteException, exceptionItem]); + + const handleEdit = useCallback((): void => { + onEditException(exceptionItem); + }, [onEditException, exceptionItem]); + + const onCommentsClick = useCallback((): void => { + setShowComments(!showComments); + }, [setShowComments, showComments]); + + const formattedComments = useMemo((): EuiCommentProps[] => { + // TODO: return back to exceptionItem.comments once updated + return getFormattedComments(exceptionItem.comment); + }, [exceptionItem]); + + const disableDelete = useMemo((): boolean => { + const foundItems = loadingItemIds.filter((t) => t.id === exceptionItem.id); + return foundItems.length > 0; + }, [loadingItemIds, exceptionItem.id]); + + return ( + + + + + + + + + + + + + + + + ); +}; + +ExceptionItemComponent.displayName = 'ExceptionItemComponent'; + +export const ExceptionItem = React.memo(ExceptionItemComponent); + +ExceptionItem.displayName = 'ExceptionItem'; 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 new file mode 100644 index 00000000000000..dcc8611cd7298b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx @@ -0,0 +1,158 @@ +/* + * 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 React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { ExceptionsViewerPagination } from './exceptions_pagination'; + +describe('ExceptionsViewerPagination', () => { + it('it renders passed in "pageSize" as selected option', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsPerPageBtn"]').at(0).text()).toEqual( + 'Items per page: 50' + ); + }); + + it('it renders all passed in page size options when per page button clicked', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="exceptionsPerPageBtn"] button').simulate('click'); + + expect(wrapper.find('button[data-test-subj="exceptionsPerPageItem"]').at(0).text()).toEqual( + '20 items' + ); + expect(wrapper.find('button[data-test-subj="exceptionsPerPageItem"]').at(1).text()).toEqual( + '50 items' + ); + expect(wrapper.find('button[data-test-subj="exceptionsPerPageItem"]').at(2).text()).toEqual( + '100 items' + ); + }); + + it('it invokes "onPaginationChange" when per page item is clicked', () => { + const mockOnPaginationChange = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="exceptionsPerPageBtn"] button').simulate('click'); + wrapper.find('button[data-test-subj="exceptionsPerPageItem"]').at(0).simulate('click'); + + expect(mockOnPaginationChange).toHaveBeenCalledWith({ + filter: {}, + pagination: { pageIndex: 0, pageSize: 20, totalItemCount: 1 }, + }); + }); + + it('it renders correct total page count', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsPagination"]').at(0).prop('pageCount')).toEqual( + 4 + ); + expect( + wrapper.find('[data-test-subj="exceptionsPagination"]').at(0).prop('activePage') + ).toEqual(0); + }); + + it('it invokes "onPaginationChange" when next clicked', () => { + const mockOnPaginationChange = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="pagination-button-next"]').at(1).simulate('click'); + + expect(mockOnPaginationChange).toHaveBeenCalledWith({ + filter: {}, + pagination: { pageIndex: 2, pageSize: 50, totalItemCount: 160 }, + }); + }); + + it('it invokes "onPaginationChange" when page clicked', () => { + const mockOnPaginationChange = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('button[data-test-subj="pagination-button-3"]').simulate('click'); + + expect(mockOnPaginationChange).toHaveBeenCalledWith({ + filter: {}, + pagination: { pageIndex: 4, 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 new file mode 100644 index 00000000000000..2920f1a85eee71 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx @@ -0,0 +1,124 @@ +/* + * 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 React, { ReactElement, useCallback, useState, useMemo } from 'react'; +import { + EuiContextMenuItem, + EuiButtonEmpty, + EuiPagination, + EuiFlexItem, + EuiFlexGroup, + EuiPopover, + EuiContextMenuPanel, +} from '@elastic/eui'; + +import * as i18n from '../translations'; +import { ExceptionsPagination, Filter } from '../types'; + +interface ExceptionsViewerPaginationProps { + pagination: ExceptionsPagination; + onPaginationChange: (arg: Filter) => void; +} + +const ExceptionsViewerPaginationComponent = ({ + pagination, + onPaginationChange, +}: ExceptionsViewerPaginationProps): JSX.Element => { + const [isOpen, setIsOpen] = useState(false); + + const handleClosePerPageMenu = useCallback((): void => setIsOpen(false), [setIsOpen]); + + const handlePerPageMenuClick = useCallback( + (): void => setIsOpen((isPopoverOpen) => !isPopoverOpen), + [setIsOpen] + ); + + const handlePageClick = useCallback( + (pageIndex: number): void => { + onPaginationChange({ + filter: {}, + pagination: { + pageIndex: pageIndex + 1, + pageSize: pagination.pageSize, + totalItemCount: pagination.totalItemCount, + }, + }); + }, + [pagination, onPaginationChange] + ); + + const items = useMemo((): ReactElement[] => { + return pagination.pageSizeOptions.map((rows) => ( + { + onPaginationChange({ + filter: {}, + pagination: { + pageIndex: pagination.pageIndex, + pageSize: rows, + totalItemCount: pagination.totalItemCount, + }, + }); + handleClosePerPageMenu(); + }} + data-test-subj="exceptionsPerPageItem" + > + {i18n.NUMBER_OF_ITEMS(rows)} + + )); + }, [pagination, onPaginationChange, handleClosePerPageMenu]); + + const totalPages = useMemo((): number => { + if (pagination.totalItemCount > 0) { + return Math.ceil(pagination.totalItemCount / pagination.pageSize); + } else { + return 1; + } + }, [pagination]); + + return ( + + + + {i18n.ITEMS_PER_PAGE(pagination.pageSize)} + + } + isOpen={isOpen} + closePopover={handleClosePerPageMenu} + panelPaddingSize="none" + > + + + + + + + + + ); +}; + +ExceptionsViewerPaginationComponent.displayName = 'ExceptionsViewerPaginationComponent'; + +export const ExceptionsViewerPagination = React.memo(ExceptionsViewerPaginationComponent); + +ExceptionsViewerPagination.displayName = 'ExceptionsViewerPagination'; 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 new file mode 100644 index 00000000000000..d697023b2ced4e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx @@ -0,0 +1,174 @@ +/* + * 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 React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { ExceptionsViewerUtility } from './exceptions_utility'; + +describe('ExceptionsViewerUtility', () => { + it('it renders correct pluralized text when more than one exception exists', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsShowing"]').at(0).text()).toEqual( + 'Showing 2 exceptions' + ); + }); + + it('it renders correct singular text when less than two exceptions exists', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsShowing"]').at(0).text()).toEqual( + 'Showing 1 exception' + ); + }); + + it('it invokes "onRefreshClick" when refresh button clicked', () => { + const mockOnRefreshClick = jest.fn(); + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="exceptionsRefresh"] button').simulate('click'); + + expect(mockOnRefreshClick).toHaveBeenCalledTimes(1); + }); + + it('it does not render any messages when "showEndpointList" and "showDetectionsList" are "false"', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsEndpointMessage"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="exceptionsDetectionsMessage"]').exists()).toBeFalsy(); + }); + + it('it does render detections messages when "showDetectionsList" is "true"', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsEndpointMessage"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="exceptionsDetectionsMessage"]').exists()).toBeTruthy(); + }); + + it('it does render endpoint messages when "showEndpointList" is "true"', () => { + const wrapper = mountWithIntl( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsEndpointMessage"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="exceptionsDetectionsMessage"]').exists()).toBeFalsy(); + }); +}); 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 new file mode 100644 index 00000000000000..9ab4e170f4090f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx @@ -0,0 +1,111 @@ +/* + * 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 React from 'react'; +import { EuiText, EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from 'react-intl'; +import styled from 'styled-components'; + +import * as i18n from '../translations'; +import { ExceptionsPagination, FilterOptions } from '../types'; +import { + UtilityBar, + UtilityBarSection, + UtilityBarGroup, + UtilityBarText, + UtilityBarAction, +} from '../../utility_bar'; + +const StyledText = styled(EuiText)` + font-style: italic; +`; + +const MyUtilities = styled(EuiFlexGroup)` + height: 50px; +`; + +interface ExceptionsViewerUtilityProps { + pagination: ExceptionsPagination; + filterOptions: FilterOptions; + ruleSettingsUrl: string; + onRefreshClick: () => void; +} + +const ExceptionsViewerUtilityComponent: React.FC = ({ + pagination, + filterOptions, + ruleSettingsUrl, + onRefreshClick, +}): JSX.Element => ( + + + + + + + {i18n.SHOWING_EXCEPTIONS(pagination.totalItemCount ?? 0)} + + + + + + {i18n.REFRESH} + + + + + + + + {filterOptions.showEndpointList && ( + + + + ), + }} + /> + )} + {filterOptions.showDetectionsList && ( + + + + ), + }} + /> + )} + + + +); + +ExceptionsViewerUtilityComponent.displayName = 'ExceptionsViewerUtilityComponent'; + +export const ExceptionsViewerUtility = React.memo(ExceptionsViewerUtilityComponent); + +ExceptionsViewerUtility.displayName = 'ExceptionsViewerUtility'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx new file mode 100644 index 00000000000000..796af7cd760e25 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx @@ -0,0 +1,67 @@ +/* + * 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 { storiesOf, addDecorator } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { ExceptionsViewerHeader } from './exceptions_viewer_header'; +import { ExceptionListType } from '../types'; + +addDecorator((storyFn) => ( + ({ eui: euiLightVars, darkMode: false })}>{storyFn()} +)); + +storiesOf('Components|ExceptionsViewerHeader', module) + .add('loading', () => { + return ( + + ); + }) + .add('all lists', () => { + return ( + + ); + }) + .add('endpoint only', () => { + return ( + + ); + }) + .add('detections only', () => { + return ( + + ); + }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx new file mode 100644 index 00000000000000..c609a2296b83d8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx @@ -0,0 +1,295 @@ +/* + * 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 React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { ExceptionsViewerHeader } from './exceptions_viewer_header'; +import { ExceptionListType } from '../types'; + +describe('ExceptionsViewerHeader', () => { + it('it renders all disabled if "isInitLoading" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('input[data-test-subj="exceptionsHeaderSearch"]').at(0).prop('disabled') + ).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="exceptionsDetectionFilterBtn"] button').at(0).prop('disabled') + ).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="exceptionsEndpointFilterBtn"] button').at(0).prop('disabled') + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="exceptionsHeaderAddExceptionPopoverBtn"] button') + .at(0) + .prop('disabled') + ).toBeTruthy(); + }); + + it('it displays toggles and add exception popover when more than one list type available', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsFilterGroupBtns"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionPopoverBtn"]').exists() + ).toBeTruthy(); + }); + + it('it does not display toggles and add exception popover if only one list type is available', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsFilterGroupBtns"]')).toHaveLength(0); + expect(wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionPopoverBtn"]')).toHaveLength( + 0 + ); + }); + + it('it displays add exception button without popover if only one list type is available', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"]').exists() + ).toBeTruthy(); + }); + + it('it renders detections filter toggle selected when clicked', () => { + const mockOnFilterChange = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="exceptionsDetectionFilterBtn"] button').simulate('click'); + + expect( + wrapper + .find('EuiFilterButton[data-test-subj="exceptionsDetectionFilterBtn"]') + .at(0) + .prop('hasActiveFilters') + ).toBeTruthy(); + expect( + wrapper + .find('EuiFilterButton[data-test-subj="exceptionsEndpointFilterBtn"]') + .at(0) + .prop('hasActiveFilters') + ).toBeFalsy(); + expect(mockOnFilterChange).toHaveBeenCalledWith({ + filter: { + filter: '', + showDetectionsList: true, + showEndpointList: false, + tags: [], + }, + pagination: {}, + }); + }); + + it('it renders endpoint filter toggle selected and invokes "onFilterChange" when clicked', () => { + const mockOnFilterChange = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="exceptionsEndpointFilterBtn"] button').simulate('click'); + + expect( + wrapper + .find('EuiFilterButton[data-test-subj="exceptionsEndpointFilterBtn"]') + .at(0) + .prop('hasActiveFilters') + ).toBeTruthy(); + expect( + wrapper + .find('EuiFilterButton[data-test-subj="exceptionsDetectionFilterBtn"]') + .at(0) + .prop('hasActiveFilters') + ).toBeFalsy(); + expect(mockOnFilterChange).toHaveBeenCalledWith({ + filter: { + filter: '', + showDetectionsList: false, + showEndpointList: true, + tags: [], + }, + pagination: {}, + }); + }); + + it('it invokes "onAddExceptionClick" when user selects to add an exception item and only endpoint exception lists are available', () => { + const mockOnAddExceptionClick = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button').simulate('click'); + + expect(mockOnAddExceptionClick).toHaveBeenCalledTimes(1); + }); + + it('it invokes "onAddDetectionsExceptionClick" when user selects to add an exception item and only endpoint detections lists are available', () => { + const mockOnAddExceptionClick = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button').simulate('click'); + + expect(mockOnAddExceptionClick).toHaveBeenCalledTimes(1); + }); + + it('it invokes "onAddEndpointExceptionClick" when user selects to add an exception item to endpoint list from popover', () => { + const mockOnAddExceptionClick = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper + .find('[data-test-subj="exceptionsHeaderAddExceptionPopoverBtn"] button') + .simulate('click'); + wrapper.find('[data-test-subj="addEndpointExceptionBtn"] button').simulate('click'); + + expect(mockOnAddExceptionClick).toHaveBeenCalledTimes(1); + }); + + it('it invokes "onAddDetectionsExceptionClick" when user selects to add an exception item to endpoint list from popover', () => { + const mockOnAddExceptionClick = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper + .find('[data-test-subj="exceptionsHeaderAddExceptionPopoverBtn"] button') + .simulate('click'); + wrapper.find('[data-test-subj="addDetectionsExceptionBtn"] button').simulate('click'); + + expect(mockOnAddExceptionClick).toHaveBeenCalledTimes(1); + }); + + it('it invokes "onFilterChange" when search used and "Enter" pressed', () => { + const mockOnFilterChange = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('EuiFieldSearch').at(0).simulate('keyup', { + charCode: 13, + code: 'Enter', + key: 'Enter', + }); + + expect(mockOnFilterChange).toHaveBeenCalled(); + }); +}); 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 new file mode 100644 index 00000000000000..0a630414e32674 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx @@ -0,0 +1,203 @@ +/* + * 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 { + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiContextMenu, + EuiButton, + EuiFilterGroup, + EuiFilterButton, + EuiContextMenuPanelDescriptor, +} from '@elastic/eui'; +import React, { useEffect, useState, useCallback, useMemo } from 'react'; + +import * as i18n from '../translations'; +import { ExceptionListType, Filter } from '../types'; + +interface ExceptionsViewerHeaderProps { + isInitLoading: boolean; + supportedListTypes: ExceptionListType[]; + detectionsListItems: number; + endpointListItems: number; + onFilterChange: (arg: Filter) => void; + onAddExceptionClick: (type: ExceptionListType) => void; +} + +/** + * Collection of filters and toggles for filtering exception items. + */ +const ExceptionsViewerHeaderComponent = ({ + isInitLoading, + supportedListTypes, + detectionsListItems, + endpointListItems, + onFilterChange, + onAddExceptionClick, +}: ExceptionsViewerHeaderProps): JSX.Element => { + const [filter, setFilter] = useState(''); + const [tags, setTags] = useState([]); + const [showDetectionsList, setShowDetectionsList] = useState(false); + const [showEndpointList, setShowEndpointList] = useState(false); + const [isAddExceptionMenuOpen, setAddExceptionMenuOpen] = useState(false); + + useEffect((): void => { + onFilterChange({ + filter: { filter, showDetectionsList, showEndpointList, tags }, + pagination: {}, + }); + }, [filter, tags, showDetectionsList, showEndpointList, onFilterChange]); + + const onAddExceptionDropdownClick = useCallback( + (): void => setAddExceptionMenuOpen(!isAddExceptionMenuOpen), + [setAddExceptionMenuOpen, isAddExceptionMenuOpen] + ); + + const handleDetectionsListClick = useCallback((): void => { + setShowDetectionsList(!showDetectionsList); + setShowEndpointList(false); + }, [showDetectionsList, setShowDetectionsList, setShowEndpointList]); + + const handleEndpointListClick = useCallback((): void => { + setShowEndpointList(!showEndpointList); + setShowDetectionsList(false); + }, [showEndpointList, setShowEndpointList, setShowDetectionsList]); + + const handleOnSearch = useCallback( + (searchValue: string): void => { + const tagsRegex = /(tags:[^\s]*)/i; + const tagsMatch = searchValue.match(tagsRegex); + const foundTags: string = tagsMatch != null ? tagsMatch[0].split(':')[1] : ''; + const filterString = tagsMatch != null ? searchValue.replace(tagsRegex, '') : searchValue; + + if (foundTags.length > 0) { + setTags(foundTags.split(',')); + } + + setFilter(filterString.trim()); + }, + [setTags, setFilter] + ); + + const onAddException = useCallback( + (type: ExceptionListType): void => { + onAddExceptionClick(type); + setAddExceptionMenuOpen(false); + }, + [onAddExceptionClick, setAddExceptionMenuOpen] + ); + + const addExceptionButtonOptions = useMemo( + (): EuiContextMenuPanelDescriptor[] => [ + { + id: 0, + items: [ + { + name: i18n.ADD_TO_ENDPOINT_LIST, + onClick: () => onAddException(ExceptionListType.ENDPOINT), + 'data-test-subj': 'addEndpointExceptionBtn', + }, + { + name: i18n.ADD_TO_DETECTIONS_LIST, + onClick: () => onAddException(ExceptionListType.DETECTION_ENGINE), + 'data-test-subj': 'addDetectionsExceptionBtn', + }, + ], + }, + ], + [onAddException] + ); + + return ( + + + + + + {supportedListTypes.length < 2 && ( + + onAddException(supportedListTypes[0])} + isDisabled={isInitLoading} + fill + > + {i18n.ADD_EXCEPTION_LABEL} + + + )} + + {supportedListTypes.length > 1 && ( + + + + + + {i18n.DETECTION_LIST} + {detectionsListItems != null ? ` (${detectionsListItems})` : ''} + + + {i18n.ENDPOINT_LIST} + {endpointListItems != null ? ` (${endpointListItems})` : ''} + + + + + + + {i18n.ADD_EXCEPTION_LABEL} + + } + isOpen={isAddExceptionMenuOpen} + closePopover={onAddExceptionDropdownClick} + anchorPosition="downCenter" + panelPaddingSize="none" + repositionOnScroll + > + + + + + + )} + + ); +}; + +ExceptionsViewerHeaderComponent.displayName = 'ExceptionsViewerHeaderComponent'; + +export const ExceptionsViewerHeader = React.memo(ExceptionsViewerHeaderComponent); + +ExceptionsViewerHeader.displayName = 'ExceptionsViewerHeader'; 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_item.test.tsx new file mode 100644 index 00000000000000..dbcae20eb1385f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_item.test.tsx @@ -0,0 +1,147 @@ +/* + * 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 React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { getExceptionItemMock } from '../mocks'; +import { ExceptionsViewerItems } from './exceptions_viewer_items'; + +describe('ExceptionsViewerItems', () => { + it('it renders empty prompt if "showEmpty" 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(); + }); + + it('it renders exceptions if "showEmpty" and "isInitLoading" is "false", and exceptions exist', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeFalsy(); + }); + + it('it does not render exceptions if "isInitLoading" is "true"', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); + }); + + it('it does not render or badge for first exception displayed', () => { + const exception1 = getExceptionItemMock(); + const exception2 = getExceptionItemMock(); + exception2.id = 'newId'; + + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + const firstExceptionItem = wrapper.find('[data-test-subj="exceptionItemContainer"]').at(0); + + expect(firstExceptionItem.find('[data-test-subj="exceptionItemOrBadge"]').exists()).toBeFalsy(); + }); + + it('it does render or badge with exception displayed', () => { + const exception1 = getExceptionItemMock(); + const exception2 = getExceptionItemMock(); + exception2.id = 'newId'; + + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + const notFirstExceptionItem = wrapper.find('[data-test-subj="exceptionItemContainer"]').at(1); + + expect( + notFirstExceptionItem.find('[data-test-subj="exceptionItemOrBadge"]').exists() + ).toBeFalsy(); + }); + + it('it invokes "onDeleteException" when delete button is clicked', () => { + const mockOnDeleteException = jest.fn(); + + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0).simulate('click'); + + expect(mockOnDeleteException).toHaveBeenCalledWith({ + id: 'uuid_here', + namespaceType: 'single', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx new file mode 100644 index 00000000000000..e1ef3c10188b31 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx @@ -0,0 +1,100 @@ +/* + * 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 React from 'react'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import styled from 'styled-components'; + +import * as i18n from '../translations'; +import { ExceptionListItemSchema, ApiProps } from '../types'; +import { ExceptionItem } from './exception_item'; +import { AndOrBadge } from '../../and_or_badge'; + +const MyFlexItem = styled(EuiFlexItem)` + margin: ${({ theme }) => `${theme.eui.euiSize} 0`}; + + &:first-child { + margin: ${({ theme }) => `${theme.eui.euiSizeXS} 0 ${theme.eui.euiSize}`}; + } +`; + +const MyExceptionsContainer = styled(EuiFlexGroup)` + height: 600px; + overflow: hidden; +`; + +const MyExceptionItemContainer = styled(EuiFlexGroup)` + margin: ${({ theme }) => `0 ${theme.eui.euiSize} ${theme.eui.euiSize} 0`}; +`; + +interface ExceptionsViewerItemsProps { + showEmpty: boolean; + isInitLoading: boolean; + exceptions: ExceptionListItemSchema[]; + loadingItemIds: ApiProps[]; + commentsAccordionId: string; + onDeleteException: (arg: ApiProps) => void; + onEditExceptionItem: (item: ExceptionListItemSchema) => void; +} + +const ExceptionsViewerItemsComponent: React.FC = ({ + showEmpty, + isInitLoading, + exceptions, + loadingItemIds, + commentsAccordionId, + onDeleteException, + onEditExceptionItem, +}): JSX.Element => ( + + {showEmpty || isInitLoading ? ( + + {i18n.EXCEPTION_EMPTY_PROMPT_TITLE}} + body={

    {i18n.EXCEPTION_EMPTY_PROMPT_BODY}

    } + data-test-subj="exceptionsEmptyPrompt" + /> +
    + ) : ( + + + {!isInitLoading && + exceptions.length > 0 && + exceptions.map((exception, index) => ( + + {index !== 0 ? ( + <> + + + + ) : ( + + )} + + + ))} + + + )} +
    +); + +ExceptionsViewerItemsComponent.displayName = 'ExceptionsViewerItemsComponent'; + +export const ExceptionsViewerItems = React.memo(ExceptionsViewerItemsComponent); + +ExceptionsViewerItems.displayName = 'ExceptionsViewerItems'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx index 7d3b7195def80e..b77b8380c39f1a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx @@ -9,108 +9,120 @@ import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { ExceptionItem } from './'; -import { getExceptionItemMock } from '../mocks'; - -describe('ExceptionItem', () => { - it('it renders ExceptionDetails and ExceptionEntries', () => { - const exceptionItem = getExceptionItemMock(); - - const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - ); - - expect(wrapper.find('ExceptionDetails')).toHaveLength(1); - expect(wrapper.find('ExceptionEntries')).toHaveLength(1); - }); - - it('it invokes "handleEdit" when edit button clicked', () => { - const mockHandleEdit = jest.fn(); - const exceptionItem = getExceptionItemMock(); - - const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - ); - - const editBtn = wrapper.find('[data-test-subj="exceptionsViewerEditBtn"] button').at(0); - editBtn.simulate('click'); - - expect(mockHandleEdit).toHaveBeenCalledTimes(1); +import { ExceptionsViewer } from './'; +import { ExceptionListType } from '../types'; +import { useKibana } from '../../../../common/lib/kibana'; +import { useExceptionList, useApi } from '../../../../../public/lists_plugin_deps'; +import { getExceptionListMock } from '../mocks'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../../public/lists_plugin_deps'); + +describe('ExceptionsViewer', () => { + beforeEach(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + http: {}, + application: { + getUrlForApp: () => 'some/url', + }, + }, + }); + + (useApi as jest.Mock).mockReturnValue({ + deleteExceptionItem: jest.fn().mockResolvedValue(true), + }); + + (useExceptionList as jest.Mock).mockReturnValue([ + false, + [], + [], + { + page: 1, + perPage: 20, + total: 0, + }, + jest.fn(), + ]); }); - it('it invokes "handleDelete" when delete button clicked', () => { - const mockHandleDelete = jest.fn(); - const exceptionItem = getExceptionItemMock(); - + it('it renders loader if "loadingList" is true', () => { + (useExceptionList as jest.Mock).mockReturnValue([ + true, + [], + [], + { + page: 1, + perPage: 20, + total: 0, + }, + jest.fn(), + ]); const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> - ); - const editBtn = wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0); - editBtn.simulate('click'); - - expect(mockHandleDelete).toHaveBeenCalledTimes(1); + expect(wrapper.find('[data-test-subj="loadingPanelAllRulesTable"]').exists()).toBeTruthy(); }); - it('it renders comment accordion closed to begin with', () => { - const mockHandleDelete = jest.fn(); - const exceptionItem = getExceptionItemMock(); - + it('it renders empty prompt if no "exceptionListMeta" passed in', () => { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> - ); - expect(wrapper.find('.euiAccordion-isOpen')).toHaveLength(0); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); }); - it('it renders comment accordion open when showComments is true', () => { - const mockHandleDelete = jest.fn(); - const exceptionItem = getExceptionItemMock(); + it('it renders empty prompt if no exception items exist', () => { + (useExceptionList as jest.Mock).mockReturnValue([ + false, + [getExceptionListMock()], + [], + { + page: 1, + perPage: 20, + total: 0, + }, + jest.fn(), + ]); const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> - ); - const commentsBtn = wrapper - .find('.euiButtonEmpty[data-test-subj="exceptionsViewerItemCommentsBtn"]') - .at(0); - commentsBtn.simulate('click'); - - expect(wrapper.find('.euiAccordion-isOpen')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); }); }); 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 f4cdce62f56b38..10519628522193 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,96 +4,315 @@ * you may not use this file except in compliance with the Elastic License. */ +import React, { useCallback, useMemo, useEffect, useReducer } from 'react'; +import { EuiOverlayMask, EuiModal, EuiModalBody, EuiCodeBlock, EuiSpacer } from '@elastic/eui'; +import uuid from 'uuid'; + +import * as i18n from '../translations'; +import { useStateToaster } from '../../toasters'; +import { useKibana } from '../../../../common/lib/kibana'; +import { Panel } from '../../../../common/components/panel'; +import { Loader } from '../../../../common/components/loader'; +import { ExceptionsViewerHeader } from './exceptions_viewer_header'; +import { + ExceptionListType, + ExceptionListItemSchema, + ApiProps, + Filter, + SetExceptionsProps, +} from '../types'; +import { allExceptionItemsReducer, State } from './reducer'; import { - EuiPanel, - EuiFlexGroup, - EuiCommentProps, - EuiCommentList, - EuiAccordion, - EuiFlexItem, -} from '@elastic/eui'; -import React, { useEffect, useState, useMemo, useCallback } from 'react'; -import styled from 'styled-components'; - -import { ExceptionDetails } from './exception_details'; -import { ExceptionEntries } from './exception_entries'; -import { getFormattedEntries, getFormattedComments } from '../helpers'; -import { FormattedEntry, ExceptionListItemSchema } from '../types'; - -const MyFlexItem = styled(EuiFlexItem)` - &.comments--show { - padding: ${({ theme }) => theme.eui.euiSize}; - border-top: ${({ theme }) => `${theme.eui.euiBorderThin}`} - -`; - -interface ExceptionItemProps { - exceptionItem: ExceptionListItemSchema; + useExceptionList, + ExceptionIdentifiers, + useApi, +} from '../../../../../public/lists_plugin_deps'; +import { ExceptionsViewerPagination } from './exceptions_pagination'; +import { ExceptionsViewerUtility } from './exceptions_utility'; +import { ExceptionsViewerItems } from './exceptions_viewer_items'; + +const initialState: State = { + filterOptions: { filter: '', showEndpointList: false, showDetectionsList: false, 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, + isModalOpen: false, +}; + +enum ModalAction { + CREATE = 'CREATE', + EDIT = 'EDIT', +} + +interface ExceptionsViewerProps { + ruleId: string; + exceptionListsMeta: ExceptionIdentifiers[]; + availableListTypes: ExceptionListType[]; commentsAccordionId: string; - handleDelete: ({ id }: { id: string }) => void; - handleEdit: (item: ExceptionListItemSchema) => void; + onAssociateList?: (listId: string) => void; } -const ExceptionItemComponent = ({ - exceptionItem, +const ExceptionsViewerComponent = ({ + ruleId, + exceptionListsMeta, + availableListTypes, + onAssociateList, commentsAccordionId, - handleDelete, - handleEdit, -}: ExceptionItemProps): JSX.Element => { - const [entryItems, setEntryItems] = useState([]); - const [showComments, setShowComments] = useState(false); +}: ExceptionsViewerProps): JSX.Element => { + const { services } = useKibana(); + const [, dispatchToaster] = useStateToaster(); + const onDispatchToaster = useCallback( + ({ title, color, iconType }) => (): void => { + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title, + color, + iconType, + }, + }); + }, + [dispatchToaster] + ); + const [ + { + endpointList, + detectionsList, + exceptions, + filterOptions, + pagination, + loadingLists, + loadingItemIds, + isInitLoading, + isModalOpen, + }, + dispatch, + ] = useReducer(allExceptionItemsReducer(), { ...initialState, loadingLists: exceptionListsMeta }); + const { deleteExceptionItem } = useApi(services.http); - useEffect((): void => { - const formattedEntries = getFormattedEntries(exceptionItem.entries); - setEntryItems(formattedEntries); - }, [exceptionItem.entries]); + const setExceptions = useCallback( + ({ + lists: newLists, + exceptions: newExceptions, + pagination: newPagination, + }: SetExceptionsProps) => { + dispatch({ + type: 'setExceptions', + lists: newLists, + exceptions: (newExceptions as unknown) as ExceptionListItemSchema[], + pagination: newPagination, + }); + }, + [dispatch] + ); + const [loadingList, , , , fetchList] = useExceptionList({ + http: services.http, + lists: loadingLists, + filterOptions, + pagination: { + page: pagination.pageIndex + 1, + perPage: pagination.pageSize, + total: pagination.totalItemCount, + }, + onSuccess: setExceptions, + onError: onDispatchToaster({ + color: 'danger', + title: i18n.FETCH_LIST_ERROR, + iconType: 'alert', + }), + }); + + const setIsModalOpen = useCallback( + (isOpen: boolean): void => { + dispatch({ + type: 'updateModalOpen', + isOpen, + }); + }, + [dispatch] + ); + + const handleFetchList = useCallback((): void => { + if (fetchList != null) { + fetchList(); + } + }, [fetchList]); + + const handleFilterChange = useCallback( + ({ filter, pagination: pag }: Filter): void => { + dispatch({ + type: 'updateFilterOptions', + filterOptions: filter, + pagination: pag, + allLists: exceptionListsMeta, + }); + }, + [dispatch, exceptionListsMeta] + ); + + const handleAddException = useCallback( + (type: ExceptionListType): void => { + setIsModalOpen(true); + }, + [setIsModalOpen] + ); + + const handleEditException = useCallback( + (exception: ExceptionListItemSchema): void => { + // TODO: Added this just for testing. Update + // modal state logic as needed once ready + dispatch({ + type: 'updateExceptionToEdit', + exception, + }); - const onDelete = useCallback((): void => { - handleDelete({ id: exceptionItem.id }); - }, [handleDelete, exceptionItem]); + setIsModalOpen(true); + }, + [setIsModalOpen] + ); + + const onCloseExceptionModal = useCallback( + ({ actionType, listId }): void => { + setIsModalOpen(false); + + // TODO: This callback along with fetchList can probably get + // passed to the modal for it to call itself maybe + if (actionType === ModalAction.CREATE && listId != null && onAssociateList != null) { + onAssociateList(listId); + } + + handleFetchList(); + }, + [setIsModalOpen, handleFetchList, onAssociateList] + ); + + const setLoadingItemIds = useCallback( + (items: ApiProps[]): void => { + dispatch({ + type: 'updateLoadingItemIds', + items, + }); + }, + [dispatch] + ); + + const handleDeleteException = useCallback( + ({ id, namespaceType }: ApiProps) => { + setLoadingItemIds([{ id, namespaceType }]); + + deleteExceptionItem({ + id, + namespaceType, + onSuccess: () => { + setLoadingItemIds(loadingItemIds.filter((t) => t.id !== id)); + handleFetchList(); + }, + onError: () => { + const dispatchToasterError = onDispatchToaster({ + color: 'danger', + title: i18n.DELETE_EXCEPTION_ERROR, + iconType: 'alert', + }); - const onEdit = useCallback((): void => { - handleEdit(exceptionItem); - }, [handleEdit, exceptionItem]); + dispatchToasterError(); + setLoadingItemIds(loadingItemIds.filter((t) => t.id !== id)); + }, + }); + }, + [setLoadingItemIds, deleteExceptionItem, loadingItemIds, handleFetchList, onDispatchToaster] + ); + + // Logic for initial render + useEffect((): void => { + if (isInitLoading && !loadingList && (exceptions.length === 0 || exceptions != null)) { + dispatch({ + type: 'updateIsInitLoading', + loading: false, + }); + } + }, [isInitLoading, exceptions, loadingList, dispatch]); - const onCommentsClick = useCallback((): void => { - setShowComments(!showComments); - }, [setShowComments, showComments]); + // 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 formattedComments = useMemo((): EuiCommentProps[] => { - return getFormattedComments(exceptionItem.comments); - }, [exceptionItem]); + const showEmpty = useMemo((): boolean => { + return !isInitLoading && !loadingList && exceptions.length === 0; + }, [isInitLoading, exceptions.length, loadingList]); return ( - - - - - - - - - - - - - - - + <> + {isModalOpen && ( + + + + + {`Modal goes here`} + + + + + )} + + + {(isInitLoading || loadingList) && ( + + )} + + + + + + + + + + + + ); }; -ExceptionItemComponent.displayName = 'ExceptionItemComponent'; +ExceptionsViewerComponent.displayName = 'ExceptionsViewerComponent'; -export const ExceptionItem = React.memo(ExceptionItemComponent); +export const ExceptionsViewer = React.memo(ExceptionsViewerComponent); -ExceptionItem.displayName = 'ExceptionItem'; +ExceptionsViewer.displayName = 'ExceptionsViewer'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts new file mode 100644 index 00000000000000..538207458f0edb --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts @@ -0,0 +1,134 @@ +/* + * 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 { + ApiProps, + FilterOptions, + ExceptionsPagination, + ExceptionListItemSchema, + Pagination, +} from '../types'; +import { ExceptionList, ExceptionIdentifiers } from '../../../../../public/lists_plugin_deps'; + +export interface State { + filterOptions: FilterOptions; + pagination: ExceptionsPagination; + endpointList: ExceptionList | null; + detectionsList: ExceptionList | null; + allExceptions: ExceptionListItemSchema[]; + exceptions: ExceptionListItemSchema[]; + exceptionToEdit: ExceptionListItemSchema | null; + loadingLists: ExceptionIdentifiers[]; + loadingItemIds: ApiProps[]; + isInitLoading: boolean; + isModalOpen: boolean; +} + +export type Action = + | { + type: 'setExceptions'; + lists: ExceptionList[]; + exceptions: ExceptionListItemSchema[]; + pagination: Pagination; + } + | { + type: 'updateFilterOptions'; + filterOptions: Partial; + pagination: Partial; + allLists: ExceptionIdentifiers[]; + } + | { type: 'updateIsInitLoading'; loading: boolean } + | { type: 'updateModalOpen'; isOpen: boolean } + | { type: 'updateExceptionToEdit'; exception: ExceptionListItemSchema } + | { type: 'updateLoadingItemIds'; items: ApiProps[] }; + +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'); + + 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, + }, + allExceptions: action.exceptions, + exceptions: action.exceptions, + }; + } + case 'updateFilterOptions': { + const returnState = { + ...state, + filterOptions: { + ...state.filterOptions, + ...action.filterOptions, + }, + pagination: { + ...state.pagination, + ...action.pagination, + }, + }; + + 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 { + ...state, + isInitLoading: action.loading, + }; + } + case 'updateLoadingItemIds': { + return { + ...state, + loadingItemIds: [...state.loadingItemIds, ...action.items], + }; + } + case 'updateExceptionToEdit': { + return { + ...state, + exceptionToEdit: action.exception, + }; + } + case 'updateModalOpen': { + return { + ...state, + isModalOpen: action.isOpen, + }; + } + default: + return state; + } +}; diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 628d56ebb647cc..dc4a8ae78bf46d 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -4340,6 +4340,8 @@ export namespace GetAllTimeline { pinnedEventIds: Maybe; + status: Maybe; + title: Maybe; timelineType: Maybe; diff --git a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts index 350b53ef52f4eb..113bfaa860f000 100644 --- a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts +++ b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts @@ -5,10 +5,18 @@ */ export { + useApi, useExceptionList, usePersistExceptionItem, usePersistExceptionList, + ExceptionIdentifiers, + ExceptionList, mockNewExceptionItem, mockNewExceptionList, } from '../../lists/public'; -export { ExceptionListItemSchema, Entries } from '../../lists/common/schemas'; +export { + ExceptionListSchema, + ExceptionListItemSchema, + Entries, + NamespaceType, +} from '../../lists/common/schemas'; 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 4fbbcfc8d948aa..8f7e0e06fb7a16 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 @@ -35,8 +35,7 @@ export const ManagementPageView = memo>((options) => href: getManagementUrl({ name: 'policyList' }), }, ]; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tabName]); + }, [options.viewType, tabName]); return ; }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx index 6c447df6187919..9b0ca73cf021f5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx @@ -137,15 +137,14 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { description: details.agent.version, }, ]; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - details.agent.version, details.endpoint.policy.id, - details.host.hostname, details.host.ip, - policyResponseUri.search, - policyStatusClickHandler, + details.host.hostname, + details.agent.version, policyStatus, + policyResponseUri, + policyStatusClickHandler, ]); return ( diff --git a/x-pack/plugins/security_solution/public/management/store/reducer.ts b/x-pack/plugins/security_solution/public/management/store/reducer.ts index e00b3ec9e0f5ba..2ed3dfe86d2f8c 100644 --- a/x-pack/plugins/security_solution/public/management/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/store/reducer.ts @@ -40,6 +40,5 @@ export const mockManagementState: Immutable = { export const managementReducer = immutableCombineReducers({ [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: policyListReducer, [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer, - // @ts-ignore [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: hostListReducer, }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx index 8856805e6b6609..44d4ff3ec1980e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx @@ -61,7 +61,7 @@ type ActionManageTimeline = | { type: 'SET_TIMELINE_FILTER_MANAGER'; id: string; - payload: FilterManager; + payload: { filterManager: FilterManager }; }; export const timelineDefaults = { @@ -161,7 +161,7 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT dispatch({ type: 'SET_TIMELINE_FILTER_MANAGER', id, - payload: filterManager, + payload: { filterManager }, }); }, [] diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index f521ffa9b6b5d0..c8a47798f169ce 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -221,7 +221,7 @@ export const queryTimelineById = ({ variables: { id: timelineId }, }) // eslint-disable-next-line - .then(result => { + .then((result) => { const timelineToOpen: TimelineResult = omitTypenameInTimeline( getOr({}, 'data.getOneTimeline', result) ); diff --git a/x-pack/plugins/security_solution/scripts/storybook.js b/x-pack/plugins/security_solution/scripts/storybook.js index 5f06f2a4ebb12b..cd4d16d89c48d0 100644 --- a/x-pack/plugins/security_solution/scripts/storybook.js +++ b/x-pack/plugins/security_solution/scripts/storybook.js @@ -9,5 +9,5 @@ import { join } from 'path'; // eslint-disable-next-line require('@kbn/storybook').runStorybookCli({ name: 'siem', - storyGlobs: [join(__dirname, '..', 'public', '**', 'components', '**', '*.stories.tsx')], + storyGlobs: [join(__dirname, '..', 'public', '**', '*.stories.tsx')], }); diff --git a/x-pack/plugins/security_solution/server/endpoint/alerts/handlers/lib/index.ts b/x-pack/plugins/security_solution/server/endpoint/alerts/handlers/lib/index.ts index 4f0a7ba2450dce..e398e70f1d7b7b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/alerts/handlers/lib/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/alerts/handlers/lib/index.ts @@ -6,7 +6,7 @@ import { SearchResponse } from 'elasticsearch'; import { IScopedClusterClient } from 'kibana/server'; import { AlertEvent } from '../../../../../common/endpoint/types'; -import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; import { esQuery } from '../../../../../../../../src/plugins/data/server'; import { AlertAPIOrdering, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts index 440b578bde413a..debb455ac728ff 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts @@ -7,7 +7,7 @@ import { SearchResponse } from 'elasticsearch'; import { IScopedClusterClient } from 'kibana/server'; import { ResolverEvent } from '../../../../../common/endpoint/types'; -import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; import { legacyEventIndexPattern } from './legacy_event_index_pattern'; import { MSearchQuery } from './multi_searcher'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts index e4b2559a1780c1..f357ac1a43d729 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts @@ -7,7 +7,7 @@ import { SearchResponse } from 'elasticsearch'; import { ResolverEvent } from '../../../../../common/endpoint/types'; import { ResolverQuery } from './base'; import { PaginationBuilder, PaginatedResults } from '../utils/pagination'; -import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; /** * Builds a query for retrieving descendants of a node. diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts index e14b222500d7c8..04202cfd007f9d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts @@ -7,7 +7,7 @@ import { SearchResponse } from 'elasticsearch'; import { ResolverEvent } from '../../../../../common/endpoint/types'; import { ResolverQuery } from './base'; import { PaginationBuilder, PaginatedResults } from '../utils/pagination'; -import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; /** * Builds a query for retrieving related events for a node. diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts index 74fe44f39615c4..93910293b00aff 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts @@ -5,7 +5,7 @@ */ import { SearchResponse } from 'elasticsearch'; import { ResolverQuery } from './base'; -import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; import { ResolverEvent } from '../../../../../common/endpoint/types'; /** diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts index 7f55fafeafb599..4c0e1a5126e7c6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts @@ -7,7 +7,7 @@ import { IScopedClusterClient } from 'kibana/server'; import { MSearchResponse } from 'elasticsearch'; import { ResolverEvent } from '../../../../../common/endpoint/types'; -import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; /** * Contract for queries to be compatible with ES multi search api diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts index 359445f514b77e..a728054bef2197 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts @@ -6,7 +6,7 @@ import { SearchResponse } from 'elasticsearch'; import { ResolverQuery } from './base'; import { ResolverEvent, EventStats } from '../../../../../common/endpoint/types'; -import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; import { AggBucket } from '../utils/pagination'; export interface StatsResult { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts index 9a852d47e0e85a..61cb5bdb8f1465 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts @@ -6,7 +6,7 @@ import { ResolverEvent } from '../../../../../common/endpoint/types'; import { eventId } from '../../../../../common/endpoint/models/event'; -import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; /** * Represents a single result bucket of an aggregation diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index 5a2a950f21bcf2..cf5a6aa01f29e8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -64,7 +64,8 @@ export const rulesNotificationAlertType = ({ from: fromInMs, to: toInMs, id: ruleAlertSavedObject.id, - kibanaSiemAppUrl: ruleAlertParams.meta?.kibana_siem_app_url, + kibanaSiemAppUrl: (ruleAlertParams.meta as { kibana_siem_app_url?: string }) + .kibana_siem_app_url, }); logger.info( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 23d5d7eb8d385f..fe66496f70dcdf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -22,34 +22,11 @@ import { IRuleSavedAttributesSavedObjectAttributes, HapiReadableStream, } from '../../rules/types'; -import { RuleAlertParamsRest } from '../../types'; import { requestMock } from './request'; import { RuleNotificationAlertType } from '../../notifications/types'; import { QuerySignalsSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema'; import { SetSignalsStatusSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema'; - -export const typicalPayload = (): Partial => ({ - rule_id: 'rule-1', - description: 'Detecting root and admin users', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - risk_score: 50, - type: 'query', - from: 'now-6m', - to: 'now', - severity: 'high', - query: 'user.name: root or user.name: admin', - language: 'kuery', - threat: [ - { - framework: 'fake', - tactic: { id: 'fakeId', name: 'fakeName', reference: 'fakeRef' }, - technique: [{ id: 'techniqueId', name: 'techniqueName', reference: 'techniqueRef' }], - }, - ], -}); +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ signal_ids: ['somefakeid1', 'somefakeid2'], @@ -77,14 +54,14 @@ export const getUpdateRequest = () => requestMock.create({ method: 'put', path: DETECTION_ENGINE_RULES_URL, - body: typicalPayload(), + body: getCreateRulesSchemaMock(), }); export const getPatchRequest = () => requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_URL, - body: typicalPayload(), + body: getCreateRulesSchemaMock(), }); export const getReadRequest = () => @@ -104,21 +81,21 @@ export const getReadBulkRequest = () => requestMock.create({ method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - body: [typicalPayload()], + body: [getCreateRulesSchemaMock()], }); export const getUpdateBulkRequest = () => requestMock.create({ method: 'put', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, - body: [typicalPayload()], + body: [getCreateRulesSchemaMock()], }); export const getPatchBulkRequest = () => requestMock.create({ method: 'patch', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, - body: [typicalPayload()], + body: [getCreateRulesSchemaMock()], }); export const getDeleteBulkRequest = () => @@ -254,11 +231,12 @@ export const getCreateRequest = () => requestMock.create({ method: 'post', path: DETECTION_ENGINE_RULES_URL, - body: typicalPayload(), + body: getCreateRulesSchemaMock(), }); +// TODO: Replace this with the mocks version from the mocks file export const typicalMlRulePayload = () => { - const { query, language, index, ...mlParams } = typicalPayload(); + const { query, language, index, ...mlParams } = getCreateRulesSchemaMock(); return { ...mlParams, @@ -284,8 +262,9 @@ export const createBulkMlRuleRequest = () => { }); }; +// TODO: Replace this with a mocks file version export const createRuleWithActionsRequest = () => { - const payload = typicalPayload(); + const payload = getCreateRulesSchemaMock(); return requestMock.create({ method: 'post', @@ -369,6 +348,7 @@ export const getResult = (): RuleAlertType => ({ falsePositives: [], from: 'now-6m', immutable: false, + savedId: undefined, query: 'user.name: root or user.name: admin', language: 'kuery', machineLearningJobId: undefined, @@ -560,9 +540,9 @@ export const getFindResultStatus = (): SavedObjectsFindResponse< alertId: '1ea5a820-4da1-4e82-92a1-2b43a7bece08', statusDate: '2020-02-18T15:26:49.783Z', status: 'succeeded', - lastFailureAt: null, + lastFailureAt: undefined, lastSuccessAt: '2020-02-18T15:26:49.783Z', - lastFailureMessage: null, + lastFailureMessage: undefined, lastSuccessMessage: 'succeeded', lastLookBackDate: new Date('2020-02-18T15:14:58.806Z').toISOString(), gap: '500.32', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index 41a28bfe8967da..063c9dffd66dd6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -6,70 +6,8 @@ import { Readable } from 'stream'; -import { OutputRuleAlertRest } from '../../types'; import { HapiReadableStream } from '../../rules/types'; - -/** - * This is a typical simple rule for testing that is easy for most basic testing - * @param ruleId - */ -export const getSimpleRule = (ruleId = 'rule-1'): Partial => ({ - name: 'Simple Rule Query', - description: 'Simple Rule Query', - risk_score: 1, - rule_id: ruleId, - severity: 'high', - type: 'query', - query: 'user.name: root or user.name: admin', -}); - -/** - * This is a typical ML rule for testing - * @param ruleId - */ -export const getSimpleMlRule = (ruleId = 'rule-1'): Partial => ({ - name: 'Simple Rule Query', - description: 'Simple Rule Query', - risk_score: 1, - rule_id: ruleId, - severity: 'high', - type: 'machine_learning', - anomaly_threshold: 44, - machine_learning_job_id: 'some_job_id', -}); - -/** - * This is a typical simple rule for testing that is easy for most basic testing - * @param ruleId - */ -export const getSimpleRuleWithId = (id = 'rule-1'): Partial => ({ - name: 'Simple Rule Query', - description: 'Simple Rule Query', - risk_score: 1, - id, - severity: 'high', - type: 'query', - query: 'user.name: root or user.name: admin', -}); - -/** - * Given an array of rules, builds an NDJSON string of rules - * as we might import/export - * @param rules Array of rule objects with which to generate rule JSON - */ -export const rulesToNdJsonString = (rules: Array>) => { - return rules.map((rule) => JSON.stringify(rule)).join('\r\n'); -}; - -/** - * Given an array of rule IDs, builds an NDJSON string of rules - * as we might import/export - * @param ruleIds Array of ruleIds with which to generate rule JSON - */ -export const ruleIdsToNdJsonString = (ruleIds: string[]) => { - const rules = ruleIds.map((ruleId) => getSimpleRule(ruleId)); - return rulesToNdJsonString(rules); -}; +import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; /** * Given a string, builds a hapi stream as our @@ -94,7 +32,7 @@ export const buildHapiStream = (string: string, filename = 'file.ndjson'): HapiR }; export const getOutputRuleAlertForRest = (): Omit< - OutputRuleAlertRest, + RulesSchema, 'machine_learning_job_id' | 'anomaly_threshold' > => ({ actions: [], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 1df6070bc33a6e..11a4543e2fa12f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -8,7 +8,6 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../machine_learning/mocks'; import { buildMlAuthz } from '../../../machine_learning/authz'; import { - typicalPayload, getReadBulkRequest, getEmptyIndex, getNonEmptyIndex, @@ -20,6 +19,7 @@ import { import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesBulkRoute } from './create_rules_bulk_route'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -98,8 +98,7 @@ describe('create_rules_bulk', () => { expect(response.body).toEqual([ { error: { - message: - 'To create a rule, the index must exist first. Index .siem-signals does not exist', + message: 'To create a rule, the index must exist first. Index undefined does not exist', status_code: 400, }, rule_id: 'rule-1', @@ -143,7 +142,7 @@ describe('create_rules_bulk', () => { const request = requestMock.create({ method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - body: [typicalPayload(), typicalPayload()], + body: [getCreateRulesSchemaMock(), getCreateRulesSchemaMock()], }); const response = await server.inject(request, context); @@ -164,7 +163,7 @@ describe('create_rules_bulk', () => { const request = requestMock.create({ method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - body: [{ ...typicalPayload(), type: 'query' }], + body: [{ ...getCreateRulesSchemaMock(), type: 'query' }], }); const result = server.validate(request); @@ -175,7 +174,7 @@ describe('create_rules_bulk', () => { const request = requestMock.create({ method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - body: [{ ...typicalPayload(), type: 'unexpected_type' }], + body: [{ ...getCreateRulesSchemaMock(), type: 'unexpected_type' }], }); const result = server.validate(request); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 5abd7b1e76a761..37500572d13866 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -9,7 +9,6 @@ import { getEmptyFindResult, getResult, getCreateRequest, - typicalPayload, getFindResultStatus, getNonEmptyIndex, getEmptyIndex, @@ -22,6 +21,7 @@ import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesRoute } from './create_rules_route'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; jest.mock('../../rules/update_rules_notifications'); jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -114,7 +114,7 @@ describe('create_rules', () => { expect(response.status).toEqual(400); expect(response.body).toEqual({ - message: 'To create a rule, the index must exist first. Index .siem-signals does not exist', + message: 'To create a rule, the index must exist first. Index undefined does not exist', status_code: 400, }); }); @@ -149,7 +149,7 @@ describe('create_rules', () => { method: 'post', path: DETECTION_ENGINE_RULES_URL, body: { - ...typicalPayload(), + ...getCreateRulesSchemaMock(), type: 'query', }, }); @@ -163,7 +163,7 @@ describe('create_rules', () => { method: 'post', path: DETECTION_ENGINE_RULES_URL, body: { - ...typicalPayload(), + ...getCreateRulesSchemaMock(), type: 'unexpected_type', }, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index 0762fbc7dd6e36..84148231431a16 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -4,12 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - buildHapiStream, - ruleIdsToNdJsonString, - rulesToNdJsonString, - getSimpleRuleWithId, -} from '../__mocks__/utils'; +import { buildHapiStream } from '../__mocks__/utils'; import { getImportRulesRequest, getImportRulesRequestOverwriteTrue, @@ -25,6 +20,11 @@ import { buildMlAuthz } from '../../../machine_learning/authz'; import { importRulesRoute } from './import_rules_route'; import * as createRulesStreamFromNdJson from '../../rules/create_rules_stream_from_ndjson'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +import { + getImportRulesWithIdSchemaMock, + ruleIdsToNdJsonString, + rulesToNdJsonString, +} from '../../../../../common/detection_engine/schemas/request/import_rules_schema.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -239,7 +239,11 @@ describe('import_rules_route', () => { }); test('returns 200 with errors if all rules are missing rule_ids and import fails on validation', async () => { - const rulesWithoutRuleIds = ['rule-1', 'rule-2'].map((ruleId) => getSimpleRuleWithId(ruleId)); + const rulesWithoutRuleIds = ['rule-1', 'rule-2'].map((ruleId) => + getImportRulesWithIdSchemaMock(ruleId) + ); + delete rulesWithoutRuleIds[0].rule_id; + delete rulesWithoutRuleIds[1].rule_id; const badPayload = buildHapiStream(rulesToNdJsonString(rulesWithoutRuleIds)); const badRequest = getImportRulesRequest(badPayload); @@ -251,7 +255,7 @@ describe('import_rules_route', () => { { error: { // TODO: Change the formatter to do better than output [object Object] - message: '[object Object],[object Object]', + message: '[object Object]', status_code: 400, }, rule_id: '(unknown id)', @@ -259,7 +263,7 @@ describe('import_rules_route', () => { { error: { // TODO: Change the formatter to do better than output [object Object] - message: '[object Object],[object Object]', + message: '[object Object]', status_code: 400, }, rule_id: '(unknown id)', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index d5145e1dceae0e..24fd5e151e485d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -9,7 +9,6 @@ import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../mach import { buildMlAuthz } from '../../../machine_learning/authz'; import { getEmptyFindResult, - typicalPayload, getFindResultWithSingleHit, getPatchBulkRequest, getResult, @@ -18,6 +17,7 @@ import { import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { patchRulesBulkRoute } from './patch_rules_bulk_route'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -153,7 +153,7 @@ describe('patch_rules_bulk', () => { const request = requestMock.create({ method: 'patch', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, - body: [{ ...typicalPayload(), rule_id: undefined }], + body: [{ ...getCreateRulesSchemaMock(), rule_id: undefined }], }); const response = await server.inject(request, context); @@ -173,7 +173,7 @@ describe('patch_rules_bulk', () => { const request = requestMock.create({ method: 'patch', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, - body: [{ ...typicalPayload(), type: 'query' }], + body: [{ ...getCreateRulesSchemaMock(), type: 'query' }], }); const result = server.validate(request); @@ -184,7 +184,7 @@ describe('patch_rules_bulk', () => { const request = requestMock.create({ method: 'patch', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, - body: [{ ...typicalPayload(), type: 'unknown_type' }], + body: [{ ...getCreateRulesSchemaMock(), type: 'unknown_type' }], }); const result = server.validate(request); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index 09230d45ee7857..1b225f6dd93c36 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -12,7 +12,6 @@ import { getFindResultStatus, getResult, getPatchRequest, - typicalPayload, getFindResultWithSingleHit, nonRuleFindResult, typicalMlRulePayload, @@ -20,6 +19,7 @@ import { import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { patchRulesRoute } from './patch_rules_route'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -165,7 +165,7 @@ describe('patch_rules', () => { const request = requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_URL, - body: { ...typicalPayload(), rule_id: undefined }, + body: { ...getCreateRulesSchemaMock(), rule_id: undefined }, }); const response = await server.inject(request, context); expect(response.body).toEqual({ @@ -178,7 +178,7 @@ describe('patch_rules', () => { const request = requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_URL, - body: { ...typicalPayload(), type: 'query' }, + body: { ...getCreateRulesSchemaMock(), type: 'query' }, }); const result = server.validate(request); @@ -189,7 +189,7 @@ describe('patch_rules', () => { const request = requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_URL, - body: { ...typicalPayload(), type: 'unknown_type' }, + body: { ...getCreateRulesSchemaMock(), type: 'unknown_type' }, }); const result = server.validate(request); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 2f331938e3ca88..582bf1a9aa22b9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -10,7 +10,6 @@ import { buildMlAuthz } from '../../../machine_learning/authz'; import { getEmptyFindResult, getResult, - typicalPayload, getFindResultWithSingleHit, getUpdateBulkRequest, getFindResultStatus, @@ -20,6 +19,7 @@ import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { updateRulesBulkRoute } from './update_rules_bulk_route'; import { BulkError } from '../utils'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -129,7 +129,7 @@ describe('update_rules_bulk', () => { const noIdRequest = requestMock.create({ method: 'put', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, - body: [{ ...typicalPayload(), rule_id: undefined }], + body: [{ ...getCreateRulesSchemaMock(), rule_id: undefined }], }); const response = await server.inject(noIdRequest, context); expect(response.body).toEqual([ @@ -144,7 +144,7 @@ describe('update_rules_bulk', () => { const request = requestMock.create({ method: 'put', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, - body: [{ ...typicalPayload(), type: 'query' }], + body: [{ ...getCreateRulesSchemaMock(), type: 'query' }], }); const result = server.validate(request); @@ -155,7 +155,7 @@ describe('update_rules_bulk', () => { const request = requestMock.create({ method: 'put', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, - body: [{ ...typicalPayload(), type: 'unknown_type' }], + body: [{ ...getCreateRulesSchemaMock(), type: 'unknown_type' }], }); const result = server.validate(request); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index f8b7636080b1bb..bbe65ff36a3e51 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -10,7 +10,6 @@ import { getEmptyFindResult, getResult, getUpdateRequest, - typicalPayload, getFindResultWithSingleHit, getFindResultStatusEmpty, nonRuleFindResult, @@ -21,6 +20,7 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { updateRulesRoute } from './update_rules_route'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); jest.mock('../../rules/update_rules_notifications'); @@ -139,7 +139,7 @@ describe('update_rules', () => { method: 'put', path: DETECTION_ENGINE_RULES_URL, body: { - ...typicalPayload(), + ...getCreateRulesSchemaMock(), rule_id: undefined, }, }); @@ -154,7 +154,7 @@ describe('update_rules', () => { const request = requestMock.create({ method: 'put', path: DETECTION_ENGINE_RULES_URL, - body: { ...typicalPayload(), type: 'query' }, + body: { ...getCreateRulesSchemaMock(), type: 'query' }, }); const result = await server.validate(request); @@ -165,7 +165,7 @@ describe('update_rules', () => { const request = requestMock.create({ method: 'put', path: DETECTION_ENGINE_RULES_URL, - body: { ...typicalPayload(), type: 'unknown type' }, + body: { ...getCreateRulesSchemaMock(), type: 'unknown type' }, }); const result = await server.validate(request); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts index 3b2750bbbf6649..3b514b92e1479e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts @@ -21,7 +21,7 @@ import { getResult } from '../__mocks__/request_responses'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { RuleTypeParams } from '../../types'; import { BulkError, ImportSuccessError } from '../utils'; -import { getSimpleRule, getOutputRuleAlertForRest } from '../__mocks__/utils'; +import { getOutputRuleAlertForRest } from '../__mocks__/utils'; import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; import { PartialAlert } from '../../../../../../alerts/server'; import { SanitizedAlert } from '../../../../../../alerts/server/types'; @@ -30,6 +30,7 @@ import { RuleAlertType } from '../../rules/types'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; import { CreateRulesBulkSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; type PromiseFromStreams = ImportRulesSchemaDecoded | Error; @@ -62,14 +63,6 @@ describe('utils', () => { expect(rule).toEqual(expectedWithoutFromWithoutLanguage); }); - test('should omit query if query is null', () => { - const fullRule = getResult(); - fullRule.params.query = null; - const rule = transformAlertToRule(fullRule); - const { query, ...expectedWithoutQuery } = getOutputRuleAlertForRest(); - expect(rule).toEqual(expectedWithoutQuery); - }); - test('should omit query if query is undefined', () => { const fullRule = getResult(); fullRule.params.query = undefined; @@ -81,7 +74,7 @@ describe('utils', () => { test('should omit a mix of undefined, null, and missing fields', () => { const fullRule = getResult(); fullRule.params.query = undefined; - fullRule.params.language = null; + fullRule.params.language = undefined; const { from, ...omitParams } = fullRule.params; fullRule.params = omitParams as RuleTypeParams; const { enabled, ...omitEnabled } = fullRule; @@ -532,8 +525,8 @@ describe('utils', () => { }); test('returns tuple of duplicate conflict error and single rule when rules with matching rule-ids passed in and `overwrite` is false', async () => { - const rule = getSimpleRule('rule-1'); - const rule2 = getSimpleRule('rule-1'); + const rule = getCreateRulesSchemaMock('rule-1'); + const rule2 = getCreateRulesSchemaMock('rule-1'); const ndJsonStream = new Readable({ read() { this.push(`${JSON.stringify(rule)}\n`); @@ -561,9 +554,9 @@ describe('utils', () => { }); test('returns tuple of duplicate conflict error and single rule when rules with matching ids passed in and `overwrite` is false', async () => { - const rule = getSimpleRule('rule-1'); + const rule = getCreateRulesSchemaMock('rule-1'); delete rule.rule_id; - const rule2 = getSimpleRule('rule-1'); + const rule2 = getCreateRulesSchemaMock('rule-1'); delete rule2.rule_id; const ndJsonStream = new Readable({ read() { @@ -585,8 +578,8 @@ describe('utils', () => { }); test('returns tuple of empty duplicate errors array and single rule when rules with matching rule-ids passed in and `overwrite` is true', async () => { - const rule = getSimpleRule('rule-1'); - const rule2 = getSimpleRule('rule-1'); + const rule = getCreateRulesSchemaMock('rule-1'); + const rule2 = getCreateRulesSchemaMock('rule-1'); const ndJsonStream = new Readable({ read() { this.push(`${JSON.stringify(rule)}\n`); @@ -606,7 +599,7 @@ describe('utils', () => { }); test('returns tuple of empty duplicate errors array and single rule when rules without a rule-id is passed in', async () => { - const simpleRule = getSimpleRule(); + const simpleRule = getCreateRulesSchemaMock(); delete simpleRule.rule_id; const multipartPayload = `${JSON.stringify(simpleRule)}\n`; const ndJsonStream = new Readable({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index 3ed45bd8367fc8..9320eba26df0bc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -8,6 +8,7 @@ import { pickBy, countBy } from 'lodash/fp'; import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; import uuid from 'uuid'; +import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; import { CreateRulesBulkSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import { PartialAlert, FindResult } from '../../../../../../alerts/server'; @@ -21,7 +22,6 @@ import { isRuleStatusFindTypes, isRuleStatusSavedObjectType, } from '../../rules/types'; -import { OutputRuleAlertRest } from '../../types'; import { createBulkErrorObject, BulkError, @@ -30,7 +30,6 @@ import { createImportErrorObject, OutputError, } from '../utils'; -import { hasListsFeature } from '../../feature_flags'; import { RuleActions } from '../../rule_actions/types'; type PromiseFromStreams = ImportRulesSchemaDecoded | Error; @@ -104,12 +103,12 @@ export const transformAlertToRule = ( alert: RuleAlertType, ruleActions?: RuleActions | null, ruleStatus?: SavedObject -): Partial => { - return pickBy((value: unknown) => value != null, { +): Partial => { + return pickBy((value: unknown) => value != null, { actions: ruleActions?.actions ?? [], created_at: alert.createdAt.toISOString(), updated_at: alert.updatedAt.toISOString(), - created_by: alert.createdBy, + created_by: alert.createdBy ?? 'elastic', description: alert.params.description, enabled: alert.enabled, anomaly_threshold: alert.params.anomalyThreshold, @@ -134,28 +133,25 @@ export const transformAlertToRule = ( timeline_title: alert.params.timelineTitle, meta: alert.params.meta, severity: alert.params.severity, - updated_by: alert.updatedBy, + updated_by: alert.updatedBy ?? 'elastic', tags: transformTags(alert.tags), to: alert.params.to, type: alert.params.type, - threat: alert.params.threat, + threat: alert.params.threat ?? [], throttle: ruleActions?.ruleThrottle || 'no_actions', note: alert.params.note, version: alert.params.version, - status: ruleStatus?.attributes.status, + status: ruleStatus?.attributes.status ?? undefined, status_date: ruleStatus?.attributes.statusDate, - last_failure_at: ruleStatus?.attributes.lastFailureAt, - last_success_at: ruleStatus?.attributes.lastSuccessAt, - last_failure_message: ruleStatus?.attributes.lastFailureMessage, - last_success_message: ruleStatus?.attributes.lastSuccessMessage, - // TODO: (LIST-FEATURE) Remove hasListsFeature() check once we have lists available for a release - exceptions_list: hasListsFeature() ? alert.params.exceptionsList : null, + last_failure_at: ruleStatus?.attributes.lastFailureAt ?? undefined, + last_success_at: ruleStatus?.attributes.lastSuccessAt ?? undefined, + last_failure_message: ruleStatus?.attributes.lastFailureMessage ?? undefined, + last_success_message: ruleStatus?.attributes.lastSuccessMessage ?? undefined, + exceptions_list: alert.params.exceptionsList ?? [], }); }; -export const transformAlertsToRules = ( - alerts: RuleAlertType[] -): Array> => { +export const transformAlertsToRules = (alerts: RuleAlertType[]): Array> => { return alerts.map((alert) => transformAlertToRule(alert)); }; @@ -167,7 +163,7 @@ export const transformFindAlerts = ( page: number; perPage: number; total: number; - data: Array>; + data: Array>; } | null => { if (!ruleStatuses && isAlertTypes(findResults.data)) { return { @@ -194,7 +190,7 @@ export const transform = ( alert: PartialAlert, ruleActions?: RuleActions | null, ruleStatus?: SavedObject -): Partial | null => { +): Partial | null => { if (isAlertType(alert)) { return transformAlertToRule( alert, @@ -211,7 +207,7 @@ export const transformOrBulkError = ( alert: PartialAlert, ruleActions: RuleActions, ruleStatus?: unknown -): Partial | BulkError => { +): Partial | BulkError => { if (isAlertType(alert)) { if (isRuleStatusFindType(ruleStatus) && ruleStatus?.saved_objects.length > 0) { return transformAlertToRule(alert, ruleActions, ruleStatus?.saved_objects[0] ?? ruleStatus); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts index 5fc239ed48263b..7b0bf5997d12f9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts @@ -22,7 +22,6 @@ import { IRuleSavedAttributesSavedObjectAttributes, isRuleStatusFindType, } from '../../rules/types'; -import { OutputRuleAlertRest } from '../../types'; import { createBulkErrorObject, BulkError } from '../utils'; import { transformFindAlerts, transform, transformAlertToRule } from './utils'; import { RuleActions } from '../../rule_actions/types'; @@ -36,7 +35,7 @@ export const transformValidateFindAlerts = ( page: number; perPage: number; total: number; - data: Array>; + data: Array>; } | null, string | null ] => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts index 1a965842348acf..747cf825104604 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { AlertsClient } from '../../../../../alerts/server'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; import { isAlertType } from '../rules/types'; import { readRules } from './read_rules'; import { transformAlertToRule } from '../routes/rules/utils'; -import { OutputRuleAlertRest } from '../types'; import { transformDataToNdjson } from '../../../utils/read_stream/create_stream_from_ndjson'; -interface ExportSuccesRule { +interface ExportSuccessRule { statusCode: 200; - rule: Partial; + rule: Partial; } interface ExportFailedRule { @@ -22,12 +22,12 @@ interface ExportFailedRule { missingRuleId: { rule_id: string }; } -type ExportRules = ExportSuccesRule | ExportFailedRule; +type ExportRules = ExportSuccessRule | ExportFailedRule; export interface RulesErrors { exportedCount: number; missingRules: Array<{ rule_id: string }>; - rules: Array>; + rules: Array>; } export const getExportByObjectIds = async ( @@ -80,7 +80,7 @@ export const getRulesFromObjects = async ( ) as ExportFailedRule[]; const exportedRules = alertsAndErrors.filter( (resp) => resp.statusCode === 200 - ) as ExportSuccesRule[]; + ) as ExportSuccessRule[]; return { exportedCount: exportedRules.length, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts index 431b3776fd9e2a..848abc3febf353 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts @@ -4,18 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sampleRule } from '../signals/__mocks__/es_results'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; +import { getRulesSchemaMock } from '../../../../common/detection_engine/schemas/response/rules_schema.mocks'; describe('getExportDetailsNdjson', () => { test('it ends with a new line character', () => { - const rule = sampleRule(); + const rule = getRulesSchemaMock(); const details = getExportDetailsNdjson([rule]); expect(details.endsWith('\n')).toEqual(true); }); test('it exports a correct count given a single rule and no missing rules', () => { - const rule = sampleRule(); + const rule = getRulesSchemaMock(); const details = getExportDetailsNdjson([rule]); const reParsed = JSON.parse(details); expect(reParsed).toEqual({ @@ -37,8 +37,8 @@ describe('getExportDetailsNdjson', () => { }); test('it exports a correct count given multiple rules and multiple missing rules', () => { - const rule1 = sampleRule(); - const rule2 = sampleRule(); + const rule1 = getRulesSchemaMock(); + const rule2 = getRulesSchemaMock(); rule2.rule_id = 'some other id'; rule2.id = 'some other id'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts index a39541d044bc36..0a685cab9b4503 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { OutputRuleAlertRest } from '../types'; +import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; export const getExportDetailsNdjson = ( - rules: Array>, + rules: Array>, missingRules: Array<{ rule_id: string }> = [] ): string => { const stringified = JSON.stringify({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index 3796df353abb89..0c103b7176db4d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -11,7 +11,6 @@ import { PatchRulesOptions } from './types'; import { addTags } from './add_tags'; import { calculateVersion, calculateName, calculateInterval } from './utils'; import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; -import { Meta } from '../types'; export const patchRules = async ({ alertsClient, @@ -60,7 +59,7 @@ export const patchRules = async ({ savedId, timelineId, timelineTitle, - meta: meta as Meta | undefined, // TODO: Remove this cast once we fix the types for calculate version and patch, + meta, filters, from, index, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 8bdb1287c6df2c..4b84057f6d7952 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -69,11 +69,17 @@ import { QueryFilterOrUndefined, FieldsOrUndefined, SortOrderOrUndefined, + JobStatus, + LastSuccessAt, + StatusDate, + LastSuccessMessage, + LastFailureAt, + LastFailureMessage, } from '../../../../common/detection_engine/schemas/common/schemas'; import { AlertsClient, PartialAlert } from '../../../../../alerts/server'; import { Alert, SanitizedAlert } from '../../../../../alerts/common'; import { SIGNALS_ID } from '../../../../common/constants'; -import { RuleAlertParams, RuleTypeParams, PartialFilter } from '../types'; +import { RuleTypeParams, PartialFilter } from '../types'; export interface RuleAlertType extends Alert { params: RuleTypeParams; @@ -82,12 +88,12 @@ export interface RuleAlertType extends Alert { // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface IRuleStatusAttributes extends Record { alertId: string; // created alert id. - statusDate: string; - lastFailureAt: string | null | undefined; - lastFailureMessage: string | null | undefined; - lastSuccessAt: string | null | undefined; - lastSuccessMessage: string | null | undefined; - status: RuleStatusString | null | undefined; + statusDate: StatusDate; + lastFailureAt: LastFailureAt | null | undefined; + lastFailureMessage: LastFailureMessage | null | undefined; + lastSuccessAt: LastSuccessAt | null | undefined; + lastSuccessMessage: LastSuccessMessage | null | undefined; + status: JobStatus | null | undefined; lastLookBackDate: string | null | undefined; gap: string | null | undefined; bulkCreateTimeDurations: string[] | null | undefined; @@ -121,8 +127,6 @@ export interface IRuleStatusFindType { saved_objects: IRuleStatusSavedObject[]; } -export type RuleStatusString = 'succeeded' | 'failed' | 'going to run'; - export interface HapiReadableStream extends Readable { hapi: { filename: string; @@ -133,12 +137,6 @@ export interface Clients { alertsClient: AlertsClient; } -// TODO: Try and remove this patch -export type PatchRuleParams = Partial> & { - rule: SanitizedAlert | null; - savedObjectsClient: SavedObjectsClientContract; -} & Clients; - export const isAlertTypes = (partialAlert: PartialAlert[]): partialAlert is RuleAlertType[] => { return partialAlert.every((rule) => isAlertType(rule)); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts index 0236c357988d56..b3f327857dbb3d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts @@ -12,7 +12,6 @@ import { addTags } from './add_tags'; import { calculateVersion } from './utils'; import { hasListsFeature } from '../feature_flags'; import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; -import { Meta } from '../types'; export const updateRules = async ({ alertsClient, @@ -63,7 +62,7 @@ export const updateRules = async ({ savedId, timelineId, timelineTitle, - meta: meta as Meta, // TODO: Remove this cast once we fix the types for calculate version and patch + meta, filters, from, index, @@ -81,6 +80,7 @@ export const updateRules = async ({ note, anomalyThreshold, machineLearningJobId, + exceptionsList, }); // TODO: Remove this and use regular exceptions_list once the feature is stable for a release diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts index b7c36b20f44bef..0f65b2a78ec4ce 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts @@ -5,7 +5,6 @@ */ import { calculateInterval, calculateVersion, calculateName } from './utils'; -import { PatchRuleParams } from './types'; describe('utils', () => { describe('#calculateInterval', () => { @@ -27,24 +26,104 @@ describe('utils', () => { describe('#calculateVersion', () => { test('returning the same version number if given an immutable but no updated version number', () => { - expect(calculateVersion(true, 1, { description: 'some description change' })).toEqual(1); + expect( + calculateVersion(true, 1, { + description: 'some description change', + falsePositives: undefined, + query: undefined, + language: undefined, + outputIndex: undefined, + savedId: undefined, + timelineId: undefined, + timelineTitle: undefined, + meta: undefined, + filters: [], + from: undefined, + index: undefined, + interval: undefined, + maxSignals: undefined, + riskScore: undefined, + name: undefined, + severity: undefined, + tags: undefined, + threat: undefined, + to: undefined, + type: undefined, + references: undefined, + version: undefined, + note: undefined, + anomalyThreshold: undefined, + machineLearningJobId: undefined, + exceptionsList: [], + }) + ).toEqual(1); }); test('returning an updated version number if given an immutable and an updated version number', () => { - expect(calculateVersion(true, 2, { description: 'some description change' })).toEqual(2); + expect( + calculateVersion(true, 2, { + description: 'some description change', + falsePositives: undefined, + query: undefined, + language: undefined, + outputIndex: undefined, + savedId: undefined, + timelineId: undefined, + timelineTitle: undefined, + meta: undefined, + filters: [], + from: undefined, + index: undefined, + interval: undefined, + maxSignals: undefined, + riskScore: undefined, + name: undefined, + severity: undefined, + tags: undefined, + threat: undefined, + to: undefined, + type: undefined, + references: undefined, + version: undefined, + note: undefined, + anomalyThreshold: undefined, + machineLearningJobId: undefined, + exceptionsList: [], + }) + ).toEqual(2); }); test('returning an updated version number if not given an immutable but but an updated description', () => { - expect(calculateVersion(false, 1, { description: 'some description change' })).toEqual(2); - }); - - test('returning the same version number but a undefined description', () => { - expect(calculateVersion(false, 1, { description: undefined })).toEqual(1); - }); - - test('returning an updated version number if not given an immutable but an updated falsy value', () => { expect( - calculateVersion(false, 1, ({ description: false } as unknown) as PatchRuleParams) + calculateVersion(false, 1, { + description: 'some description change', + falsePositives: undefined, + query: undefined, + language: undefined, + outputIndex: undefined, + savedId: undefined, + timelineId: undefined, + timelineTitle: undefined, + meta: undefined, + filters: [], + from: undefined, + index: undefined, + interval: undefined, + maxSignals: undefined, + riskScore: undefined, + name: undefined, + severity: undefined, + tags: undefined, + threat: undefined, + to: undefined, + type: undefined, + references: undefined, + version: undefined, + note: undefined, + anomalyThreshold: undefined, + machineLearningJobId: undefined, + exceptionsList: [], + }) ).toEqual(2); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts index 7d6091f6b97faa..d40cb5d96669bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts @@ -5,7 +5,35 @@ */ import { pickBy, isEmpty } from 'lodash/fp'; -import { PatchRuleParams } from './types'; +import { + DescriptionOrUndefined, + AnomalyThresholdOrUndefined, + QueryOrUndefined, + LanguageOrUndefined, + SavedIdOrUndefined, + TimelineIdOrUndefined, + TimelineTitleOrUndefined, + MachineLearningJobIdOrUndefined, + IndexOrUndefined, + NoteOrUndefined, + MetaOrUndefined, + VersionOrUndefined, + FalsePositivesOrUndefined, + FromOrUndefined, + OutputIndexOrUndefined, + IntervalOrUndefined, + MaxSignalsOrUndefined, + RiskScoreOrUndefined, + NameOrUndefined, + SeverityOrUndefined, + TagsOrUndefined, + ToOrUndefined, + ThreatOrUndefined, + TypeOrUndefined, + ReferencesOrUndefined, + ListAndOrUndefined, +} from '../../../../common/detection_engine/schemas/common/schemas'; +import { PartialFilter } from '../types'; export const calculateInterval = ( interval: string | undefined, @@ -20,10 +48,40 @@ export const calculateInterval = ( } }; +export interface UpdateProperties { + description: DescriptionOrUndefined; + falsePositives: FalsePositivesOrUndefined; + from: FromOrUndefined; + query: QueryOrUndefined; + language: LanguageOrUndefined; + savedId: SavedIdOrUndefined; + timelineId: TimelineIdOrUndefined; + timelineTitle: TimelineTitleOrUndefined; + meta: MetaOrUndefined; + machineLearningJobId: MachineLearningJobIdOrUndefined; + filters: PartialFilter[]; + index: IndexOrUndefined; + interval: IntervalOrUndefined; + maxSignals: MaxSignalsOrUndefined; + riskScore: RiskScoreOrUndefined; + outputIndex: OutputIndexOrUndefined; + name: NameOrUndefined; + severity: SeverityOrUndefined; + tags: TagsOrUndefined; + threat: ThreatOrUndefined; + to: ToOrUndefined; + type: TypeOrUndefined; + references: ReferencesOrUndefined; + note: NoteOrUndefined; + version: VersionOrUndefined; + exceptionsList: ListAndOrUndefined; + anomalyThreshold: AnomalyThresholdOrUndefined; +} + export const calculateVersion = ( immutable: boolean, currentVersion: number, - updateProperties: Partial> + updateProperties: UpdateProperties ): number => { // early return if we are pre-packaged/immutable rule to be safe. We are never responsible // for changing the version number of an immutable. Immutables are only responsible for changing @@ -44,7 +102,7 @@ export const calculateVersion = ( // the version number if only the enabled/disabled flag is being set. Likewise if we get other // properties we are not expecting such as updatedAt we do not to cause a version number bump // on that either. - const removedNullValues = pickBy( + const removedNullValues = pickBy( (value: unknown) => value != null, updateProperties ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 46a16e7dca1536..6056e692854afe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -11,7 +11,7 @@ import { SavedObjectsFindResponse, } from '../../../../../../../../src/core/server'; import { loggingServiceMock } from '../../../../../../../../src/core/server/mocks'; -import { RuleTypeParams, OutputRuleAlertRest } from '../../types'; +import { RuleTypeParams } from '../../types'; import { IRuleStatusAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; @@ -364,34 +364,6 @@ export const sampleDocSearchResultsWithSortId = ( export const sampleRuleGuid = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; export const sampleIdGuid = 'e1e08ddc-5e37-49ff-a258-5393aa44435a'; -export const sampleRule = (): Partial => { - return { - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: ['some fake tag 1', 'some fake tag 2'], - to: 'now', - type: 'query', - note: '', - }; -}; - export const exampleRuleStatus: () => SavedObject = () => ({ type: ruleStatusSavedObjectType, id: '042e6d90-7069-11ea-af8b-0f8ae4fa817e', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts index df9d282b71e5e1..80c2441193a0ca 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -18,7 +18,7 @@ describe('buildBulkBody', () => { jest.clearAllMocks(); }); - test('if bulk body builds well-defined body', () => { + test('bulk body builds well-defined body', () => { const sampleParams = sampleRuleAlertParams(); const fakeSignalSourceHit = buildBulkBody({ doc: sampleDocNoSortId(), @@ -80,6 +80,7 @@ describe('buildBulkBody', () => { references: ['http://google.com'], severity: 'high', tags: ['some fake tag 1', 'some fake tag 2'], + threat: [], throttle: 'no_actions', type: 'query', to: 'now', @@ -128,7 +129,7 @@ describe('buildBulkBody', () => { expect(fakeSignalSourceHit).toEqual(expected); }); - test('if bulk body builds original_event if it exists on the event to begin with', () => { + test('bulk body builds original_event if it exists on the event to begin with', () => { const sampleParams = sampleRuleAlertParams(); const doc = sampleDocNoSortId(); doc._source.event = { @@ -216,6 +217,7 @@ describe('buildBulkBody', () => { created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, throttle: 'no_actions', + threat: [], exceptions_list: [ { field: 'source.ip', @@ -254,7 +256,7 @@ describe('buildBulkBody', () => { expect(fakeSignalSourceHit).toEqual(expected); }); - test('if bulk body builds original_event if it exists on the event to begin with but no kind information', () => { + test('bulk body builds original_event if it exists on the event to begin with but no kind information', () => { const sampleParams = sampleRuleAlertParams(); const doc = sampleDocNoSortId(); doc._source.event = { @@ -329,6 +331,7 @@ describe('buildBulkBody', () => { query: 'user.name: root or user.name: admin', references: ['http://google.com'], severity: 'high', + threat: [], tags: ['some fake tag 1', 'some fake tag 2'], type: 'query', to: 'now', @@ -378,7 +381,7 @@ describe('buildBulkBody', () => { expect(fakeSignalSourceHit).toEqual(expected); }); - test('if bulk body builds original_event if it exists on the event to begin with with only kind information', () => { + test('bulk body builds original_event if it exists on the event to begin with with only kind information', () => { const sampleParams = sampleRuleAlertParams(); const doc = sampleDocNoSortId(); doc._source.event = { @@ -447,6 +450,7 @@ describe('buildBulkBody', () => { references: ['http://google.com'], severity: 'high', tags: ['some fake tag 1', 'some fake tag 2'], + threat: [], type: 'query', to: 'now', note: '', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.test.ts index 772ebd932698b8..07adfde71c1a99 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.test.ts @@ -1147,17 +1147,6 @@ describe('build_exceptions_query', () => { }); describe('buildQueryExceptions', () => { - test('it returns original query if no lists exist', () => { - const query = buildQueryExceptions({ - query: 'host.name: *', - language: 'kuery', - lists: undefined, - }); - const expectedQuery = 'host.name: *'; - - expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); - }); - test('it returns original query if lists is empty array', () => { const query = buildQueryExceptions({ query: 'host.name: *', language: 'kuery', lists: [] }); const expectedQuery = 'host.name: *'; @@ -1165,24 +1154,6 @@ describe('build_exceptions_query', () => { expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); }); - test('it returns original query if lists is null', () => { - const query = buildQueryExceptions({ query: 'host.name: *', language: 'kuery', lists: null }); - const expectedQuery = 'host.name: *'; - - expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); - }); - - test('it returns original query if lists is undefined', () => { - const query = buildQueryExceptions({ - query: 'host.name: *', - language: 'kuery', - lists: undefined, - }); - const expectedQuery = 'host.name: *'; - - expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); - }); - test('it returns expected query when lists exist and language is "kuery"', () => { // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) // https://www.dcode.fr/boolean-expressions-calculator diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts index d4efd9a2e8a1ad..233b20792299b1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts @@ -3,13 +3,17 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { + ListAndOrUndefined, + Language, + Query, +} from '../../../../common/detection_engine/schemas/common/schemas'; import { ListOperator, ListValues, List, } from '../../../../common/detection_engine/schemas/types/lists_default_array'; -import { Query } from '../../../../../../../src/plugins/data/server'; -import { RuleAlertParams, Language } from '../types'; +import { Query as DataQuery } from '../../../../../../../src/plugins/data/server'; type Operators = 'and' | 'or' | 'not'; type LuceneOperators = 'AND' | 'OR' | 'NOT'; @@ -187,10 +191,10 @@ export const buildQueryExceptions = ({ language, lists, }: { - query: string; + query: Query; language: Language; - lists: RuleAlertParams['exceptionsList']; -}): Query[] => { + lists: ListAndOrUndefined; +}): DataQuery[] => { if (lists && lists !== null) { const exceptions = buildExceptions({ lists, language, query }); const formattedQuery = formatQuery({ exceptions, language, query }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts index b3586c884d0c7f..eb87976a6fbab9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts @@ -6,7 +6,7 @@ import { buildRule } from './build_rule'; import { sampleRuleAlertParams, sampleRuleGuid } from './__mocks__/es_results'; -import { OutputRuleAlertRest } from '../types'; +import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; describe('buildRule', () => { beforeEach(() => { @@ -40,7 +40,7 @@ describe('buildRule', () => { tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); - const expected: Partial = { + const expected: Partial = { actions: [], created_by: 'elastic', description: 'Detecting root and admin users', @@ -61,6 +61,7 @@ describe('buildRule', () => { rule_id: 'rule-1', severity: 'high', tags: ['some fake tag 1', 'some fake tag 2'], + threat: [], to: 'now', type: 'query', note: '', @@ -133,7 +134,7 @@ describe('buildRule', () => { tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); - const expected: Partial = { + const expected: Partial = { actions: [], created_by: 'elastic', description: 'Detecting root and admin users', @@ -154,6 +155,7 @@ describe('buildRule', () => { rule_id: 'rule-1', severity: 'high', tags: ['some fake tag 1', 'some fake tag 2'], + threat: [], to: 'now', type: 'query', note: '', @@ -215,7 +217,7 @@ describe('buildRule', () => { tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); - const expected: Partial = { + const expected: Partial = { actions: [], created_by: 'elastic', description: 'Detecting root and admin users', @@ -237,6 +239,7 @@ describe('buildRule', () => { rule_id: 'rule-1', severity: 'high', tags: ['some fake tag 1', 'some fake tag 2'], + threat: [], to: 'now', type: 'query', updated_by: 'elastic', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts index de8de1bc513e30..bde9c970b0c8c3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts @@ -5,8 +5,9 @@ */ import { pickBy } from 'lodash/fp'; +import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleTypeParams, OutputRuleAlertRest } from '../types'; +import { RuleTypeParams } from '../types'; interface BuildRuleParams { ruleParams: RuleTypeParams; @@ -36,10 +37,10 @@ export const buildRule = ({ interval, tags, throttle, -}: BuildRuleParams): Partial => { - return pickBy((value: unknown) => value != null, { +}: BuildRuleParams): Partial => { + return pickBy((value: unknown) => value != null, { id, - rule_id: ruleParams.ruleId, + rule_id: ruleParams.ruleId ?? '(unknown rule_id)', actions, false_positives: ruleParams.falsePositives, saved_id: ruleParams.savedId, @@ -67,12 +68,12 @@ export const buildRule = ({ filters: ruleParams.filters, created_by: createdBy, updated_by: updatedBy, - threat: ruleParams.threat, + threat: ruleParams.threat ?? [], throttle, version: ruleParams.version, created_at: createdAt, updated_at: updatedAt, - exceptions_list: ruleParams.exceptionsList, + exceptions_list: ruleParams.exceptionsList ?? [], machine_learning_job_id: ruleParams.machineLearningJobId, anomaly_threshold: ruleParams.anomalyThreshold, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts index f3f4ab60e4db6c..6aebf8815659a6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sampleDocNoSortId, sampleRule } from './__mocks__/es_results'; +import { sampleDocNoSortId } from './__mocks__/es_results'; import { buildSignal, buildAncestor, @@ -13,6 +13,7 @@ import { } from './build_signal'; import { Signal, Ancestor } from './types'; import { INTERNAL_RULE_ID_KEY, INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants'; +import { getPartialRulesSchemaMock } from '../../../../common/detection_engine/schemas/response/rules_schema.mocks'; describe('buildSignal', () => { beforeEach(() => { @@ -22,7 +23,7 @@ describe('buildSignal', () => { test('it builds a signal as expected without original_event if event does not exist', () => { const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); delete doc._source.event; - const rule = sampleRule(); + const rule = getPartialRulesSchemaMock(); const signal = buildSignal(doc, rule); const expected: Signal = { parent: { @@ -82,7 +83,7 @@ describe('buildSignal', () => { kind: 'event', module: 'system', }; - const rule = sampleRule(); + const rule = getPartialRulesSchemaMock(); const signal = buildSignal(doc, rule); const expected: Signal = { parent: { @@ -148,7 +149,7 @@ describe('buildSignal', () => { kind: 'event', module: 'system', }; - const rule = sampleRule(); + const rule = getPartialRulesSchemaMock(); rule.tags = [ 'some fake tag 1', 'some fake tag 2', @@ -220,7 +221,7 @@ describe('buildSignal', () => { kind: 'event', module: 'system', }; - const rule = sampleRule(); + const rule = getPartialRulesSchemaMock(); const signal = buildAncestor(doc, rule); const expected: Ancestor = { rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', @@ -258,7 +259,7 @@ describe('buildSignal', () => { }, ], }; - const rule = sampleRule(); + const rule = getPartialRulesSchemaMock(); const signal = buildAncestor(doc, rule); const expected: Ancestor = { rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', @@ -296,7 +297,7 @@ describe('buildSignal', () => { }, ], }; - const rule = sampleRule(); + const rule = getPartialRulesSchemaMock(); rule.tags = [ 'some fake tag 1', 'some fake tag 2', @@ -323,7 +324,7 @@ describe('buildSignal', () => { kind: 'event', module: 'system', }; - const rule = sampleRule(); + const rule = getPartialRulesSchemaMock(); const signal = buildAncestorsSignal(doc, rule); const expected: Ancestor[] = [ { @@ -363,7 +364,7 @@ describe('buildSignal', () => { }, ], }; - const rule = sampleRule(); + const rule = getPartialRulesSchemaMock(); const signal = buildAncestorsSignal(doc, rule); const expected: Ancestor[] = [ { @@ -385,7 +386,7 @@ describe('buildSignal', () => { }); test('it removes internal tags from a typical rule', () => { - const rule = sampleRule(); + const rule = getPartialRulesSchemaMock(); rule.tags = [ 'some fake tag 1', 'some fake tag 2', @@ -393,29 +394,29 @@ describe('buildSignal', () => { `${INTERNAL_IMMUTABLE_KEY}:true`, ]; const noInternals = removeInternalTagsFromRule(rule); - expect(noInternals).toEqual(sampleRule()); + expect(noInternals).toEqual(getPartialRulesSchemaMock()); }); test('it works with an empty array', () => { - const rule = sampleRule(); + const rule = getPartialRulesSchemaMock(); rule.tags = []; const noInternals = removeInternalTagsFromRule(rule); - const expected = sampleRule(); + const expected = getPartialRulesSchemaMock(); expected.tags = []; expect(noInternals).toEqual(expected); }); test('it works if tags does not exist', () => { - const rule = sampleRule(); + const rule = getPartialRulesSchemaMock(); delete rule.tags; const noInternals = removeInternalTagsFromRule(rule); - const expected = sampleRule(); + const expected = getPartialRulesSchemaMock(); delete expected.tags; expect(noInternals).toEqual(expected); }); test('it works if tags contains normal values and no internal values', () => { - const rule = sampleRule(); + const rule = getPartialRulesSchemaMock(); const noInternals = removeInternalTagsFromRule(rule); expect(noInternals).toEqual(rule); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts index a0551d75d27503..77a63c63ff97ad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; import { SignalSourceHit, Signal, Ancestor } from './types'; -import { OutputRuleAlertRest } from '../types'; -export const buildAncestor = ( - doc: SignalSourceHit, - rule: Partial -): Ancestor => { +export const buildAncestor = (doc: SignalSourceHit, rule: Partial): Ancestor => { const existingSignal = doc._source.signal?.parent; if (existingSignal != null) { return { @@ -34,7 +31,7 @@ export const buildAncestor = ( export const buildAncestorsSignal = ( doc: SignalSourceHit, - rule: Partial + rule: Partial ): Signal['ancestors'] => { const newAncestor = buildAncestor(doc, rule); const existingAncestors = doc._source.signal?.ancestors; @@ -45,7 +42,7 @@ export const buildAncestorsSignal = ( } }; -export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { +export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { const ruleWithoutInternalTags = removeInternalTagsFromRule(rule); const parent = buildAncestor(doc, rule); const ancestors = buildAncestorsSignal(doc, rule); @@ -62,13 +59,11 @@ export const buildSignal = (doc: SignalSourceHit, rule: Partial -): Partial => { +export const removeInternalTagsFromRule = (rule: Partial): Partial => { if (rule.tags == null) { return rule; } else { - const ruleWithoutInternalTags: Partial = { + const ruleWithoutInternalTags: Partial = { ...rule, tags: rule.tags.filter((tag) => !tag.startsWith(INTERNAL_IDENTIFIER)), }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts index d56e167f59e4cd..4e9eb8587484fa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts @@ -21,11 +21,11 @@ describe('filterEventsAgainstList', () => { listClient.getListItemByValues = jest.fn().mockResolvedValue([]); }); - it('should respond with eventSearchResult if exceptionList is empty', async () => { + it('should respond with eventSearchResult if exceptionList is empty array', async () => { const res = await filterEventsAgainstList({ logger: mockLogger, listClient, - exceptionsList: undefined, + exceptionsList: [], eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ '1.1.1.1', '2.2.2.2', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts index 29b8b54d162dfa..48b120d1b5806c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts @@ -6,15 +6,15 @@ import { get } from 'lodash/fp'; import { Logger } from 'src/core/server'; +import { ListAndOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; import { List } from '../../../../common/detection_engine/schemas/types/lists_default_array'; import { type } from '../../../../../lists/common/schemas/common'; import { ListClient } from '../../../../../lists/server'; import { SignalSearchResponse, SearchTypes } from './types'; -import { RuleAlertParams } from '../types'; interface FilterEventsAgainstList { listClient: ListClient; - exceptionsList: RuleAlertParams['exceptionsList']; + exceptionsList: ListAndOrUndefined; logger: Logger; eventSearchResult: SignalSearchResponse; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts index 0930fbdb534f5c..b1500e47db8436 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts @@ -449,20 +449,6 @@ describe('get_filter', () => { }); }); - test('it should work when lists has value null', () => { - const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], null); - expect(esQuery).toEqual({ - bool: { - filter: [ - { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } }, - ], - must: [], - must_not: [], - should: [], - }, - }); - }); - test('it should work when lists has value undefined', () => { const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], undefined); expect(esQuery).toEqual({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts index c464238715afdd..3e9f79c67d8ca8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts @@ -4,32 +4,43 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + LanguageOrUndefined, + QueryOrUndefined, + Type, + SavedIdOrUndefined, + IndexOrUndefined, + ListAndOrUndefined, + Language, + Index, + Query, +} from '../../../../common/detection_engine/schemas/common/schemas'; import { AlertServices } from '../../../../../alerts/server'; import { assertUnreachable } from '../../../utils/build_query'; import { Filter, - Query, + Query as DataQuery, esQuery, esFilters, IIndexPattern, } from '../../../../../../../src/plugins/data/server'; -import { PartialFilter, RuleAlertParams, Language } from '../types'; +import { PartialFilter } from '../types'; import { BadRequestError } from '../errors/bad_request_error'; import { buildQueryExceptions } from './build_exceptions_query'; export const getQueryFilter = ( - query: string, + query: Query, language: Language, filters: PartialFilter[], - index: string[], - lists: RuleAlertParams['exceptionsList'] + index: Index, + lists: ListAndOrUndefined ) => { const indexPattern = { fields: [], title: index.join(), } as IIndexPattern; - const queries: Query[] = buildQueryExceptions({ query, language, lists }); + const queries: DataQuery[] = buildQueryExceptions({ query, language, lists }); const config = { allowLeadingWildcards: true, @@ -46,14 +57,14 @@ export const getQueryFilter = ( }; interface GetFilterArgs { - type: RuleAlertParams['type']; - filters: PartialFilter[] | undefined | null; - language: Language | undefined | null; - query: string | undefined | null; - savedId: string | undefined | null; + type: Type; + filters: PartialFilter[] | undefined; + language: LanguageOrUndefined; + query: QueryOrUndefined; + savedId: SavedIdOrUndefined; services: AlertServices; - index: string[] | undefined | null; - lists: RuleAlertParams['exceptionsList']; + index: IndexOrUndefined; + lists: ListAndOrUndefined; } interface QueryAttributes { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts index 9430dfe7bfe23f..0f4b8d1472b3fa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { JobStatus } from '../../../../common/detection_engine/schemas/common/schemas'; import { assertUnreachable } from '../../../utils/build_query'; -import { IRuleStatusAttributes, RuleStatusString } from '../rules/types'; +import { IRuleStatusAttributes } from '../rules/types'; import { getOrCreateRuleStatuses } from './get_or_create_rule_statuses'; import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; @@ -26,7 +27,7 @@ export interface RuleStatusService { } export const buildRuleStatusAttributes: ( - status: RuleStatusString, + status: JobStatus, message?: string, attributes?: Attributes ) => Partial = (status, message, attributes = {}) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index b7bea906475db9..42893a199a8f1f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ListAndOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; import { AlertServices } from '../../../../../alerts/server'; import { ListClient } from '../../../../../lists/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleTypeParams, RefreshTypes, RuleAlertParams } from '../types'; +import { RuleTypeParams, RefreshTypes } from '../types'; import { Logger } from '../../../../../../../src/core/server'; import { singleSearchAfter } from './single_search_after'; import { singleBulkCreate } from './single_bulk_create'; @@ -18,7 +19,7 @@ interface SearchAfterAndBulkCreateParams { ruleParams: RuleTypeParams; services: AlertServices; listClient: ListClient | undefined; // TODO: undefined is for temporary development, remove before merged - exceptionsList: RuleAlertParams['exceptionsList']; + exceptionsList: ListAndOrUndefined; logger: Logger; id: string; inputIndexPattern: string[]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts index d42ba8fe57005e..461b2589babcc8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts @@ -4,40 +4,43 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; +const signalSchema = schema.object({ + anomalyThreshold: schema.maybe(schema.number()), + description: schema.string(), + note: schema.nullable(schema.string()), + falsePositives: schema.arrayOf(schema.string(), { defaultValue: [] }), + from: schema.string(), + ruleId: schema.string(), + immutable: schema.boolean({ defaultValue: false }), + index: schema.nullable(schema.arrayOf(schema.string())), + language: schema.nullable(schema.string()), + outputIndex: schema.nullable(schema.string()), + savedId: schema.nullable(schema.string()), + timelineId: schema.nullable(schema.string()), + timelineTitle: schema.nullable(schema.string()), + meta: schema.nullable(schema.object({}, { unknowns: 'allow' })), + machineLearningJobId: schema.maybe(schema.string()), + query: schema.nullable(schema.string()), + filters: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }), + riskScore: schema.number(), + severity: schema.string(), + threat: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + to: schema.string(), + type: schema.string(), + references: schema.arrayOf(schema.string(), { defaultValue: [] }), + version: schema.number({ defaultValue: 1 }), + exceptionsList: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), +}); + /** * This is the schema for the Alert Rule that represents the SIEM alert for signals * that index into the .siem-signals-${space-id} */ -export const signalParamsSchema = () => - schema.object({ - anomalyThreshold: schema.maybe(schema.number()), - description: schema.string(), - note: schema.nullable(schema.string()), - falsePositives: schema.arrayOf(schema.string(), { defaultValue: [] }), - from: schema.string(), - ruleId: schema.string(), - immutable: schema.boolean({ defaultValue: false }), - index: schema.nullable(schema.arrayOf(schema.string())), - language: schema.nullable(schema.string()), - outputIndex: schema.nullable(schema.string()), - savedId: schema.nullable(schema.string()), - timelineId: schema.nullable(schema.string()), - timelineTitle: schema.nullable(schema.string()), - meta: schema.nullable(schema.object({}, { unknowns: 'allow' })), - machineLearningJobId: schema.maybe(schema.string()), - query: schema.nullable(schema.string()), - filters: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), - maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }), - riskScore: schema.number(), - severity: schema.string(), - threat: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), - to: schema.string(), - type: schema.string(), - references: schema.arrayOf(schema.string(), { defaultValue: [] }), - version: schema.number({ defaultValue: 1 }), - exceptionsList: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), - }); +export const signalParamsSchema = () => signalSchema; + +export type SignalParamsSchema = TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index f94eb7006829e5..a2dc33ba1c2bf3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -39,8 +39,6 @@ const getPayload = (ruleAlert: RuleAlertType, services: AlertServicesMock) => ({ name: ruleAlert.name, tags: ruleAlert.tags, throttle: ruleAlert.throttle, - scrollSize: 10, - scrollLock: '0', }, state: {}, spaceId: '', @@ -105,7 +103,7 @@ describe('rules_notification_alert_type', () => { attributes: ruleAlert, }); - payload = getPayload(ruleAlert, alertServices); + payload = getPayload(ruleAlert, alertServices) as jest.Mocked; alert = signalRulesAlertType({ logger, @@ -196,7 +194,7 @@ describe('rules_notification_alert_type', () => { describe('ML rule', () => { it('should throw an error if ML plugin was not available', async () => { const ruleAlert = getMlResult(); - payload = getPayload(ruleAlert, alertServices); + payload = getPayload(ruleAlert, alertServices) as jest.Mocked; alert = signalRulesAlertType({ logger, version, @@ -213,7 +211,7 @@ describe('rules_notification_alert_type', () => { it('should throw an error if machineLearningJobId or anomalyThreshold was not null', async () => { const ruleAlert = getMlResult(); ruleAlert.params.anomalyThreshold = undefined; - payload = getPayload(ruleAlert, alertServices); + payload = getPayload(ruleAlert, alertServices) as jest.Mocked; await alert.executor(payload); expect(logger.error).toHaveBeenCalled(); expect(logger.error.mock.calls[0][0]).toContain( @@ -223,7 +221,7 @@ describe('rules_notification_alert_type', () => { it('should throw an error if Machine learning job summary was null', async () => { const ruleAlert = getMlResult(); - payload = getPayload(ruleAlert, alertServices); + payload = getPayload(ruleAlert, alertServices) as jest.Mocked; jobsSummaryMock.mockResolvedValue([]); await alert.executor(payload); expect(logger.warn).toHaveBeenCalled(); @@ -236,7 +234,7 @@ describe('rules_notification_alert_type', () => { it('should log an error if Machine learning job was not started', async () => { const ruleAlert = getMlResult(); - payload = getPayload(ruleAlert, alertServices); + payload = getPayload(ruleAlert, alertServices) as jest.Mocked; jobsSummaryMock.mockResolvedValue([ { id: 'some_job_id', @@ -260,7 +258,7 @@ describe('rules_notification_alert_type', () => { it('should not call ruleStatusService.success if no anomalies were found', async () => { const ruleAlert = getMlResult(); - payload = getPayload(ruleAlert, alertServices); + payload = getPayload(ruleAlert, alertServices) as jest.Mocked; jobsSummaryMock.mockResolvedValue([]); (findMlSignals as jest.Mock).mockResolvedValue({ hits: { @@ -278,7 +276,7 @@ describe('rules_notification_alert_type', () => { it('should call ruleStatusService.success if signals were created', async () => { const ruleAlert = getMlResult(); - payload = getPayload(ruleAlert, alertServices); + payload = getPayload(ruleAlert, alertServices) as jest.Mocked; jobsSummaryMock.mockResolvedValue([ { id: 'some_job_id', @@ -313,7 +311,7 @@ describe('rules_notification_alert_type', () => { id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', }, ]; - payload = getPayload(ruleAlert, alertServices); + payload = getPayload(ruleAlert, alertServices) as jest.Mocked; alertServices.savedObjectsClient.get.mockResolvedValue({ id: 'id', type: 'type', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 567274be6a9f8b..091ee54e3174e1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -275,7 +275,7 @@ export const signalRulesAlertType = ({ from: fromInMs, to: toInMs, id: savedObject.id, - kibanaSiemAppUrl: meta?.kibana_siem_app_url, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string }).kibana_siem_app_url, }); logger.info( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 869c81f640561c..082211df28320d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -4,21 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; +import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { AlertType, State, AlertExecutorOptions } from '../../../../../alerts/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleAlertParams, OutputRuleAlertRest } from '../types'; +import { RuleTypeParams } from '../types'; import { SearchResponse } from '../../types'; export interface SignalsParams { signalIds: string[] | undefined | null; query: object | undefined | null; - status: 'open' | 'closed'; + status: Status; } export interface SignalsStatusParams { signalIds: string[] | undefined | null; query: object | undefined | null; - status: 'open' | 'closed'; + status: Status; } export type SearchTypes = @@ -91,10 +93,7 @@ export type SignalSearchResponse = SearchResponse; export type SignalSourceHit = SignalSearchResponse['hits']['hits'][number]; export type RuleExecutorOptions = Omit & { - params: RuleAlertParams & { - scrollSize: number; - scrollLock: string; - }; + params: RuleTypeParams; }; // This returns true because by default a RuleAlertTypeDefinition is an AlertType @@ -116,12 +115,12 @@ export interface Ancestor { } export interface Signal { - rule: Partial; + rule: Partial; parent: Ancestor; ancestors: Ancestor[]; original_time: string; original_event?: SearchTypes; - status: 'open' | 'closed'; + status: Status; } export interface SignalHit { @@ -145,12 +144,7 @@ export interface AlertAttributes { } export interface RuleAlertAttributes extends AlertAttributes { - params: Omit< - RuleAlertParams, - 'ruleId' | 'name' | 'enabled' | 'interval' | 'tags' | 'actions' | 'throttle' - > & { - ruleId: string; - }; + params: RuleTypeParams; } export type BulkResponseErrorAggregation = Record; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts index 9062de49fa6ce6..6e284908e3358c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts @@ -4,131 +4,67 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ListsDefaultArraySchema } from '../../../common/detection_engine/schemas/types/lists_default_array'; +import { + AnomalyThresholdOrUndefined, + Description, + NoteOrUndefined, + ThreatOrUndefined, + FalsePositives, + From, + Immutable, + IndexOrUndefined, + LanguageOrUndefined, + MaxSignals, + MachineLearningJobIdOrUndefined, + RiskScore, + OutputIndex, + QueryOrUndefined, + References, + SavedIdOrUndefined, + Severity, + To, + TimelineIdOrUndefined, + TimelineTitleOrUndefined, + Version, + MetaOrUndefined, + RuleId, + ListAndOrUndefined, +} from '../../../common/detection_engine/schemas/common/schemas'; import { CallAPIOptions } from '../../../../../../src/core/server'; import { Filter } from '../../../../../../src/plugins/data/server'; -import { IRuleStatusAttributes } from './rules/types'; -import { RuleAlertAction, RuleType } from '../../../common/detection_engine/types'; +import { RuleType } from '../../../common/detection_engine/types'; export type PartialFilter = Partial; -export interface IMitreAttack { - id: string; - name: string; - reference: string; -} - -export interface ThreatParams { - framework: string; - tactic: IMitreAttack; - technique: IMitreAttack[]; -} - -// Notice below we are using lists: ListsAndArraySchema[]; which is coming directly from the response output section. -// TODO: Eventually this whole RuleAlertParams will be replaced with io-ts. For now we can slowly strangle it out and reduce duplicate types -// We don't have the input types defined through io-ts just yet but as we being introducing types from there we will more and more remove -// types and share them between input and output schema but have an input Rule Schema and an output Rule Schema. - -export interface Meta { - [key: string]: {} | string | undefined | null; - kibana_siem_app_url?: string | undefined; -} - -export type Language = 'kuery' | 'lucene'; - -export interface RuleAlertParams { - actions: RuleAlertAction[]; - anomalyThreshold: number | undefined; - description: string; - note: string | undefined | null; - enabled: boolean; - falsePositives: string[]; - filters: PartialFilter[] | undefined | null; - from: string; - immutable: boolean; - index: string[] | undefined | null; - interval: string; - ruleId: string | undefined | null; - language: Language | undefined | null; - maxSignals: number; - machineLearningJobId: string | undefined; - riskScore: number; - outputIndex: string; - name: string; - query: string | undefined | null; - references: string[]; - savedId?: string | undefined | null; - meta: Meta | undefined | null; - severity: string; - tags: string[]; - to: string; - timelineId: string | undefined | null; - timelineTitle: string | undefined | null; - threat: ThreatParams[] | undefined | null; +export interface RuleTypeParams { + anomalyThreshold: AnomalyThresholdOrUndefined; + description: Description; + note: NoteOrUndefined; + falsePositives: FalsePositives; + from: From; + ruleId: RuleId; + immutable: Immutable; + index: IndexOrUndefined; + language: LanguageOrUndefined; + outputIndex: OutputIndex; + savedId: SavedIdOrUndefined; + timelineId: TimelineIdOrUndefined; + timelineTitle: TimelineTitleOrUndefined; + meta: MetaOrUndefined; + machineLearningJobId: MachineLearningJobIdOrUndefined; + query: QueryOrUndefined; + filters: PartialFilter[] | undefined; + maxSignals: MaxSignals; + riskScore: RiskScore; + severity: Severity; + threat: ThreatOrUndefined; + to: To; type: RuleType; - version: number; - throttle: string | undefined | null; - exceptionsList: ListsDefaultArraySchema | null | undefined; + references: References; + version: Version; + exceptionsList: ListAndOrUndefined; } -export type RuleTypeParams = Omit< - RuleAlertParams, - 'name' | 'enabled' | 'interval' | 'tags' | 'actions' | 'throttle' ->; - -export type RuleAlertParamsRest = Omit< - RuleAlertParams, - | 'anomalyThreshold' - | 'ruleId' - | 'falsePositives' - | 'immutable' - | 'maxSignals' - | 'exceptionsList' - | 'machineLearningJobId' - | 'savedId' - | 'riskScore' - | 'timelineId' - | 'timelineTitle' - | 'outputIndex' -> & - Omit< - IRuleStatusAttributes, - | 'status' - | 'alertId' - | 'statusDate' - | 'lastFailureAt' - | 'lastSuccessAt' - | 'lastSuccessMessage' - | 'lastFailureMessage' - > & { - anomaly_threshold: RuleAlertParams['anomalyThreshold']; - exceptions_list: RuleAlertParams['exceptionsList']; - rule_id: RuleAlertParams['ruleId']; - false_positives: RuleAlertParams['falsePositives']; - saved_id?: RuleAlertParams['savedId']; - timeline_id: RuleAlertParams['timelineId']; - timeline_title: RuleAlertParams['timelineTitle']; - max_signals: RuleAlertParams['maxSignals']; - machine_learning_job_id: RuleAlertParams['machineLearningJobId']; - risk_score: RuleAlertParams['riskScore']; - output_index: RuleAlertParams['outputIndex']; - created_at: string; - updated_at: string; - status?: IRuleStatusAttributes['status'] | undefined; - status_date?: IRuleStatusAttributes['statusDate'] | undefined; - last_failure_at?: IRuleStatusAttributes['lastFailureAt'] | undefined; - last_success_at?: IRuleStatusAttributes['lastSuccessAt'] | undefined; - last_failure_message?: IRuleStatusAttributes['lastFailureMessage'] | undefined; - last_success_message?: IRuleStatusAttributes['lastSuccessMessage'] | undefined; - }; - -export type OutputRuleAlertRest = RuleAlertParamsRest & { - id: string; - created_by: string | undefined | null; - updated_by: string | undefined | null; - immutable: boolean; -}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any export type CallWithRequest, V> = ( endpoint: string, diff --git a/x-pack/plugins/security_solution/server/lib/types.ts b/x-pack/plugins/security_solution/server/lib/types.ts index 2a897806dc6287..ff89512124b66a 100644 --- a/x-pack/plugins/security_solution/server/lib/types.ts +++ b/x-pack/plugins/security_solution/server/lib/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuthenticatedUser } from '../../../security/public'; +import { AuthenticatedUser } from '../../../security/common/model'; import { RequestHandlerContext } from '../../../../../src/core/server'; export { ConfigType as Configuration } from '../config'; diff --git a/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.test.ts b/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.test.ts index ca2f881d6abd80..a1902202d983d3 100644 --- a/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.test.ts +++ b/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { transformDataToNdjson } from './create_stream_from_ndjson'; -import { sampleRule } from '../../lib/detection_engine/signals/__mocks__/es_results'; import { ImportRulesSchemaDecoded } from '../../../common/detection_engine/schemas/request/import_rules_schema'; +import { getRulesSchemaMock } from '../../../common/detection_engine/schemas/response/rules_schema.mocks'; export const getOutputSample = (): Partial => ({ rule_id: 'rule-1', @@ -33,14 +33,14 @@ describe('create_rules_stream_from_ndjson', () => { }); test('single rule will transform with new line ending character for ndjson', () => { - const rule = sampleRule(); + const rule = getRulesSchemaMock(); const ruleNdjson = transformDataToNdjson([rule]); expect(ruleNdjson.endsWith('\n')).toBe(true); }); test('multiple rules will transform with two new line ending characters for ndjson', () => { - const result1 = sampleRule(); - const result2 = sampleRule(); + const result1 = getRulesSchemaMock(); + const result2 = getRulesSchemaMock(); result2.id = 'some other id'; result2.rule_id = 'some other id'; result2.name = 'Some other rule'; @@ -52,8 +52,8 @@ describe('create_rules_stream_from_ndjson', () => { }); test('you can parse two rules back out without errors', () => { - const result1 = sampleRule(); - const result2 = sampleRule(); + const result1 = getRulesSchemaMock(); + const result2 = getRulesSchemaMock(); result2.id = 'some other id'; result2.rule_id = 'some other id'; result2.name = 'Some other rule'; diff --git a/x-pack/plugins/security_solution/server/utils/serialized_query.ts b/x-pack/plugins/security_solution/server/utils/serialized_query.ts index 09b227d8c5a325..357aec1db480b1 100644 --- a/x-pack/plugins/security_solution/server/utils/serialized_query.ts +++ b/x-pack/plugins/security_solution/server/utils/serialized_query.ts @@ -7,7 +7,7 @@ import { UserInputError } from 'apollo-server-errors'; import { isEmpty, isPlainObject, isString } from 'lodash/fp'; -import { JsonObject } from '../../../../../src/plugins/kibana_utils/public'; +import { JsonObject } from '../../../../../src/plugins/kibana_utils/common'; export const parseFilterQuery = (filterQuery: string): JsonObject => { try { diff --git a/x-pack/plugins/snapshot_restore/public/shared_imports.ts b/x-pack/plugins/snapshot_restore/public/shared_imports.ts index e0024ea8e0c126..cad8ce147bd252 100644 --- a/x-pack/plugins/snapshot_restore/public/shared_imports.ts +++ b/x-pack/plugins/snapshot_restore/public/shared_imports.ts @@ -5,17 +5,17 @@ */ export { - SendRequestConfig, - SendRequestResponse, - UseRequestConfig, - sendRequest, - useRequest, + AuthorizationProvider, CronEditor, DAY, - SectionError, Error, - WithPrivileges, - useAuthorizationContext, NotAuthorizedSection, - AuthorizationProvider, + SectionError, + sendRequest, + SendRequestConfig, + SendRequestResponse, + useAuthorizationContext, + useRequest, + UseRequestConfig, + WithPrivileges, } from '../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/app.ts b/x-pack/plugins/snapshot_restore/server/routes/api/app.ts index f9714bcc02e47e..e978fae0af5bc1 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/app.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/app.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 { Privileges } from '../../../../../../src/plugins/es_ui_shared/public'; +import { Privileges } from '../../../../../../src/plugins/es_ui_shared/common'; import { APP_REQUIRED_CLUSTER_PRIVILEGES, diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts index e82ebd9a5a4ad5..0d6e9743f0f4b0 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts @@ -13,9 +13,7 @@ import { mount } from 'enzyme'; import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers'; import { CustomTimeRangeAction } from './custom_time_range_action'; /* eslint-disable */ -import { - HelloWorldContainer, -} from '../../../../src/plugins/embeddable/public/lib/test_samples'; +import { HelloWorldContainer } from '../../../../src/plugins/embeddable/public/lib/test_samples'; /* eslint-enable */ import { diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index c120e1f780761f..37b22a687741ed 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -6,7 +6,7 @@ const alwaysImportedTests = [ require.resolve('../test/functional/config.js'), - require.resolve('../test/functional_endpoint/config.ts'), + require.resolve('../test/security_solution_endpoint/config.ts'), require.resolve('../test/functional_with_es_ssl/config.ts'), require.resolve('../test/functional/config_security_basic.ts'), require.resolve('../test/functional/config_security_trial.ts'), diff --git a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts index 64bf03a043b559..10ab35714b1ce9 100644 --- a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts +++ b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts @@ -16,11 +16,19 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('legacyEs'); - const { createComponentTemplate, deleteComponentTemplate } = initElasticsearchHelpers(es); + const { + createComponentTemplate, + cleanUpComponentTemplates, + deleteComponentTemplate, + } = initElasticsearchHelpers(es); describe('Component templates', function () { + after(async () => { + await cleanUpComponentTemplates(); + }); + describe('Get', () => { - const COMPONENT_NAME = 'test_component_template'; + const COMPONENT_NAME = 'test_get_component_template'; const COMPONENT = { template: { settings: { @@ -45,8 +53,16 @@ export default function ({ getService }: FtrProviderContext) { }, }; - before(() => createComponentTemplate({ body: COMPONENT, name: COMPONENT_NAME })); - after(() => deleteComponentTemplate(COMPONENT_NAME)); + // Create component template to verify GET requests + before(async () => { + try { + await createComponentTemplate({ body: COMPONENT, name: COMPONENT_NAME }, true); + } catch (err) { + // eslint-disable-next-line no-console + console.log('[Setup error] Error creating component template'); + throw err; + } + }); describe('all component templates', () => { it('should return an array of component templates', async () => { @@ -90,9 +106,15 @@ export default function ({ getService }: FtrProviderContext) { const COMPONENT_NAME = 'test_create_component_template'; const REQUIRED_FIELDS_COMPONENT_NAME = 'test_create_required_fields_component_template'; - after(() => { - deleteComponentTemplate(COMPONENT_NAME); - deleteComponentTemplate(REQUIRED_FIELDS_COMPONENT_NAME); + after(async () => { + // Clean up any component templates created in test cases + await Promise.all( + [COMPONENT_NAME, REQUIRED_FIELDS_COMPONENT_NAME].map(deleteComponentTemplate) + ).catch((err) => { + // eslint-disable-next-line no-console + console.log(`[Cleanup error] Error deleting component templates: ${err.message}`); + throw err; + }); }); it('should create a component template', async () => { @@ -167,7 +189,7 @@ export default function ({ getService }: FtrProviderContext) { }); describe('Update', () => { - const COMPONENT_NAME = 'test_component_template'; + const COMPONENT_NAME = 'test_update_component_template'; const COMPONENT = { template: { settings: { @@ -192,8 +214,16 @@ export default function ({ getService }: FtrProviderContext) { }, }; - before(() => createComponentTemplate({ body: COMPONENT, name: COMPONENT_NAME })); - after(() => deleteComponentTemplate(COMPONENT_NAME)); + before(async () => { + // Create component template that can be used to test PUT request + try { + await createComponentTemplate({ body: COMPONENT, name: COMPONENT_NAME }, true); + } catch (err) { + // eslint-disable-next-line no-console + console.log('[Setup error] Error creating component template'); + throw err; + } + }); it('should allow an existing component template to be updated', async () => { const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_NAME}`; @@ -244,29 +274,44 @@ export default function ({ getService }: FtrProviderContext) { }, }; - it('should delete a component template', async () => { - // Create component template to be deleted - const COMPONENT_NAME = 'test_delete_component_template'; - createComponentTemplate({ body: COMPONENT, name: COMPONENT_NAME }); + const componentTemplateA = { body: COMPONENT, name: 'test_delete_component_template_a' }; + const componentTemplateB = { body: COMPONENT, name: 'test_delete_component_template_b' }; + const componentTemplateC = { body: COMPONENT, name: 'test_delete_component_template_c' }; + const componentTemplateD = { body: COMPONENT, name: 'test_delete_component_template_d' }; + + before(async () => { + // Create several component templates that can be used to test deletion + await Promise.all( + [ + componentTemplateA, + componentTemplateB, + componentTemplateC, + componentTemplateD, + ].map((template) => createComponentTemplate(template, false)) + ).catch((err) => { + // eslint-disable-next-line no-console + console.log(`[Setup error] Error creating component templates: ${err.message}`); + throw err; + }); + }); - const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_NAME}`; + it('should delete a component template', async () => { + const { name } = componentTemplateA; + const uri = `${API_BASE_PATH}/component_templates/${name}`; const { body } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); expect(body).to.eql({ - itemsDeleted: [COMPONENT_NAME], + itemsDeleted: [name], errors: [], }); }); it('should delete multiple component templates', async () => { - // Create component templates to be deleted - const COMPONENT_ONE_NAME = 'test_delete_component_1'; - const COMPONENT_TWO_NAME = 'test_delete_component_2'; - createComponentTemplate({ body: COMPONENT, name: COMPONENT_ONE_NAME }); - createComponentTemplate({ body: COMPONENT, name: COMPONENT_TWO_NAME }); + const { name: componentTemplate1Name } = componentTemplateB; + const { name: componentTemplate2Name } = componentTemplateC; - const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_ONE_NAME},${COMPONENT_TWO_NAME}`; + const uri = `${API_BASE_PATH}/component_templates/${componentTemplate1Name},${componentTemplate2Name}`; const { body: { itemsDeleted, errors }, @@ -275,23 +320,20 @@ export default function ({ getService }: FtrProviderContext) { expect(errors).to.eql([]); // The itemsDeleted array order isn't guaranteed, so we assert against each name instead - [COMPONENT_ONE_NAME, COMPONENT_TWO_NAME].forEach((componentName) => { + [componentTemplate1Name, componentTemplate2Name].forEach((componentName) => { expect(itemsDeleted.includes(componentName)).to.be(true); }); }); it('should return an error for any component templates not sucessfully deleted', async () => { const COMPONENT_DOES_NOT_EXIST = 'component_does_not_exist'; + const { name: componentTemplateName } = componentTemplateD; - // Create component template to be deleted - const COMPONENT_ONE_NAME = 'test_delete_component_1'; - createComponentTemplate({ body: COMPONENT, name: COMPONENT_ONE_NAME }); - - const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_ONE_NAME},${COMPONENT_DOES_NOT_EXIST}`; + const uri = `${API_BASE_PATH}/component_templates/${componentTemplateName},${COMPONENT_DOES_NOT_EXIST}`; const { body } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); - expect(body.itemsDeleted).to.eql([COMPONENT_ONE_NAME]); + expect(body.itemsDeleted).to.eql([componentTemplateName]); expect(body.errors[0].name).to.eql(COMPONENT_DOES_NOT_EXIST); expect(body.errors[0].error.msg).to.contain('index_template_missing_exception'); }); diff --git a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts new file mode 100644 index 00000000000000..9a8511b4331ea2 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts @@ -0,0 +1,89 @@ +/* + * 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. + */ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +// @ts-ignore +import { API_BASE_PATH } from './constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + const createDataStream = (name: string) => { + // A data stream requires an index template before it can be created. + return es.dataManagement + .saveComposableIndexTemplate({ + name, + body: { + index_patterns: ['*'], + template: { + settings: {}, + }, + data_stream: { + timestamp_field: '@timestamp', + }, + }, + }) + .then(() => + es.dataManagement.createDataStream({ + name, + }) + ); + }; + + const deleteDataStream = (name: string) => { + return es.dataManagement + .deleteComposableIndexTemplate({ + name, + }) + .then(() => + es.dataManagement.deleteDataStream({ + name, + }) + ); + }; + + describe('Data streams', function () { + const testDataStreamName = 'test-data-stream'; + + describe('Get', () => { + before(async () => await createDataStream(testDataStreamName)); + after(async () => await deleteDataStream(testDataStreamName)); + + describe('all data streams', () => { + it('returns an array of data streams', async () => { + const { body: dataStreams } = await supertest + .get(`${API_BASE_PATH}/data_streams`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + // ES determines these values so we'll just echo them back. + const { name: indexName, uuid } = dataStreams[0].indices[0]; + expect(dataStreams).to.eql([ + { + name: testDataStreamName, + timeStampField: '@timestamp', + indices: [ + { + name: indexName, + uuid, + }, + ], + generation: 1, + }, + ]); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/management/index_management/index.js b/x-pack/test/api_integration/apis/management/index_management/index.js index fdee325938ff41..93e8a4a8d6130d 100644 --- a/x-pack/test/api_integration/apis/management/index_management/index.js +++ b/x-pack/test/api_integration/apis/management/index_management/index.js @@ -10,6 +10,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./mapping')); loadTestFile(require.resolve('./settings')); loadTestFile(require.resolve('./stats')); + loadTestFile(require.resolve('./data_streams')); loadTestFile(require.resolve('./templates')); loadTestFile(require.resolve('./component_templates')); }); diff --git a/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js b/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js index b950a56a913db0..6dcfbecb6bd0dd 100644 --- a/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js +++ b/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { getRandomString } from './random'; /** @@ -12,6 +13,7 @@ import { getRandomString } from './random'; */ export const initElasticsearchHelpers = (es) => { let indicesCreated = []; + let componentTemplatesCreated = []; const createIndex = (index = getRandomString(), body) => { indicesCreated.push(index); @@ -34,7 +36,11 @@ export const initElasticsearchHelpers = (es) => { const catTemplate = (name) => es.cat.templates({ name, format: 'json' }); - const createComponentTemplate = (componentTemplate) => { + const createComponentTemplate = (componentTemplate, shouldCacheTemplate) => { + if (shouldCacheTemplate) { + componentTemplatesCreated.push(componentTemplate.name); + } + return es.dataManagement.saveComponentTemplate(componentTemplate); }; @@ -42,6 +48,16 @@ export const initElasticsearchHelpers = (es) => { return es.dataManagement.deleteComponentTemplate({ name: componentTemplateName }); }; + const cleanUpComponentTemplates = () => + Promise.all(componentTemplatesCreated.map(deleteComponentTemplate)) + .then(() => { + componentTemplatesCreated = []; + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.log(`[Cleanup error] Error deleting ES resources: ${err.message}`); + }); + return { createIndex, deleteIndex, @@ -52,5 +68,6 @@ export const initElasticsearchHelpers = (es) => { catTemplate, createComponentTemplate, deleteComponentTemplate, + cleanUpComponentTemplates, }; }; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts index 127f688dfbc288..9cda6271d427f1 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts @@ -12,12 +12,12 @@ import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex, - getSimpleRule, getSimpleRuleOutput, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, getSimpleRuleOutputWithoutRuleId, - getSimpleMlRule, + getSimpleRuleUpdate, + getSimpleMlRuleUpdate, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -41,11 +41,11 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update a simple rule's name - const updatedRule = getSimpleRule('rule-1'); + const updatedRule = getSimpleRuleUpdate('rule-1'); updatedRule.rule_id = 'rule-1'; updatedRule.name = 'some other name'; delete updatedRule.id; @@ -68,11 +68,11 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update a simple rule's type to try to be a machine learning job type - const updatedRule = getSimpleMlRule('rule-1'); + const updatedRule = getSimpleMlRuleUpdate('rule-1'); updatedRule.rule_id = 'rule-1'; updatedRule.name = 'some other name'; delete updatedRule.id; @@ -90,7 +90,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using an auto-generated rule_id', async () => { - const rule = getSimpleRule('rule-1'); + const rule = getSimpleRuleUpdate('rule-1'); delete rule.rule_id; // create a simple rule const { body: createRuleBody } = await supertest @@ -100,7 +100,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); // update a simple rule's name - const updatedRule = getSimpleRule('rule-1'); + const updatedRule = getSimpleRuleUpdate('rule-1'); updatedRule.rule_id = createRuleBody.rule_id; updatedRule.name = 'some other name'; delete updatedRule.id; @@ -123,11 +123,11 @@ export default ({ getService }: FtrProviderContext) => { const { body: createdBody } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update a simple rule's name - const updatedRule = getSimpleRule('rule-1'); + const updatedRule = getSimpleRuleUpdate('rule-1'); updatedRule.name = 'some other name'; updatedRule.id = createdBody.id; delete updatedRule.rule_id; @@ -150,11 +150,11 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update a simple rule's enabled to false and another property - const updatedRule = getSimpleRule('rule-1'); + const updatedRule = getSimpleRuleUpdate('rule-1'); updatedRule.severity = 'low'; updatedRule.enabled = false; @@ -178,10 +178,10 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); - const ruleUpdate = getSimpleRule('rule-1'); + const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.timeline_title = 'some title'; ruleUpdate.timeline_id = 'some id'; @@ -192,7 +192,7 @@ export default ({ getService }: FtrProviderContext) => { .send(ruleUpdate) .expect(200); - const ruleUpdate2 = getSimpleRule('rule-1'); + const ruleUpdate2 = getSimpleRuleUpdate('rule-1'); ruleUpdate2.name = 'some other name'; // update a simple rule's name @@ -211,7 +211,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should give a 404 if it is given a fake id', async () => { - const simpleRule = getSimpleRule(); + const simpleRule = getSimpleRuleUpdate(); simpleRule.id = '5096dec6-b6b9-4d8d-8f93-6c2602079d9d'; delete simpleRule.rule_id; @@ -228,7 +228,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should give a 404 if it is given a fake rule_id', async () => { - const simpleRule = getSimpleRule(); + const simpleRule = getSimpleRuleUpdate(); simpleRule.rule_id = 'fake_id'; delete simpleRule.id; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts index 54f29939fb6b4e..24eee8deaf3d48 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts @@ -12,11 +12,11 @@ import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex, - getSimpleRule, getSimpleRuleOutput, removeServerGeneratedProperties, getSimpleRuleOutputWithoutRuleId, removeServerGeneratedPropertiesIncludingRuleId, + getSimpleRuleUpdate, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -40,10 +40,10 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); - const updatedRule = getSimpleRule('rule-1'); + const updatedRule = getSimpleRuleUpdate('rule-1'); updatedRule.name = 'some other name'; // update a simple rule's name @@ -65,20 +65,20 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // create a second simple rule await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-2')) + .send(getSimpleRuleUpdate('rule-2')) .expect(200); - const updatedRule1 = getSimpleRule('rule-1'); + const updatedRule1 = getSimpleRuleUpdate('rule-1'); updatedRule1.name = 'some other name'; - const updatedRule2 = getSimpleRule('rule-2'); + const updatedRule2 = getSimpleRuleUpdate('rule-2'); updatedRule2.name = 'some other name'; // update both rule names @@ -107,11 +107,11 @@ export default ({ getService }: FtrProviderContext) => { const { body: createRuleBody } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update a simple rule's name - const updatedRule1 = getSimpleRule('rule-1'); + const updatedRule1 = getSimpleRuleUpdate('rule-1'); updatedRule1.id = createRuleBody.id; updatedRule1.name = 'some other name'; delete updatedRule1.rule_id; @@ -134,23 +134,23 @@ export default ({ getService }: FtrProviderContext) => { const { body: createRule1 } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // create a second simple rule const { body: createRule2 } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-2')) + .send(getSimpleRuleUpdate('rule-2')) .expect(200); // update both rule names - const updatedRule1 = getSimpleRule('rule-1'); + const updatedRule1 = getSimpleRuleUpdate('rule-1'); updatedRule1.id = createRule1.id; updatedRule1.name = 'some other name'; delete updatedRule1.rule_id; - const updatedRule2 = getSimpleRule('rule-1'); + const updatedRule2 = getSimpleRuleUpdate('rule-1'); updatedRule2.id = createRule2.id; updatedRule2.name = 'some other name'; delete updatedRule2.rule_id; @@ -180,11 +180,11 @@ export default ({ getService }: FtrProviderContext) => { const { body: createdBody } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update a simple rule's name - const updatedRule1 = getSimpleRule('rule-1'); + const updatedRule1 = getSimpleRuleUpdate('rule-1'); updatedRule1.id = createdBody.id; updatedRule1.name = 'some other name'; delete updatedRule1.rule_id; @@ -207,11 +207,11 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update a simple rule's enabled to false and another property - const updatedRule1 = getSimpleRule('rule-1'); + const updatedRule1 = getSimpleRuleUpdate('rule-1'); updatedRule1.severity = 'low'; updatedRule1.enabled = false; @@ -235,11 +235,11 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update a simple rule's timeline_title - const ruleUpdate = getSimpleRule('rule-1'); + const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.timeline_title = 'some title'; ruleUpdate.timeline_id = 'some id'; @@ -250,7 +250,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); // update a simple rule's name - const ruleUpdate2 = getSimpleRule('rule-1'); + const ruleUpdate2 = getSimpleRuleUpdate('rule-1'); ruleUpdate2.name = 'some other name'; const { body } = await supertest @@ -268,7 +268,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return a 200 but give a 404 in the message if it is given a fake id', async () => { - const ruleUpdate = getSimpleRule('rule-1'); + const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.id = '1fd52120-d3a9-4e7a-b23c-96c0e1a74ae5'; delete ruleUpdate.rule_id; @@ -290,7 +290,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return a 200 but give a 404 in the message if it is given a fake rule_id', async () => { - const ruleUpdate = getSimpleRule('rule-1'); + const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.rule_id = 'fake_id'; delete ruleUpdate.id; @@ -313,14 +313,14 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); - const ruleUpdate = getSimpleRule('rule-1'); + const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.name = 'some other name'; delete ruleUpdate.id; - const ruleUpdate2 = getSimpleRule('fake_id'); + const ruleUpdate2 = getSimpleRuleUpdate('fake_id'); ruleUpdate2.name = 'some other name'; delete ruleUpdate.id; @@ -353,16 +353,16 @@ export default ({ getService }: FtrProviderContext) => { const { body: createdBody } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update one rule name and give a fake id for the second - const rule1 = getSimpleRule(); + const rule1 = getSimpleRuleUpdate(); delete rule1.rule_id; rule1.id = createdBody.id; rule1.name = 'some other name'; - const rule2 = getSimpleRule(); + const rule2 = getSimpleRuleUpdate(); delete rule2.rule_id; rule2.id = 'b3aa019a-656c-4311-b13b-4d9852e24347'; rule2.name = 'some other name'; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts index 0b1b49e379d17a..279158f09f2733 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts @@ -12,13 +12,14 @@ import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex, - getSimpleRule, getSimpleRuleOutput, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, getSimpleRuleOutputWithoutRuleId, getSimpleMlRule, getSimpleMlRuleOutput, + getSimpleRuleUpdate, + getSimpleMlRuleUpdate, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -42,11 +43,11 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update a simple rule's name - const updatedRule = getSimpleRule('rule-1'); + const updatedRule = getSimpleRuleUpdate('rule-1'); updatedRule.rule_id = 'rule-1'; updatedRule.name = 'some other name'; delete updatedRule.id; @@ -73,7 +74,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); // update a simple rule's name - const updatedRule = getSimpleMlRule('rule-1'); + const updatedRule = getSimpleMlRuleUpdate('rule-1'); updatedRule.rule_id = 'rule-1'; updatedRule.name = 'some other name'; delete updatedRule.id; @@ -92,7 +93,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using an auto-generated rule_id', async () => { - const rule = getSimpleRule('rule-1'); + const rule = getSimpleRuleUpdate('rule-1'); delete rule.rule_id; // create a simple rule const { body: createRuleBody } = await supertest @@ -102,7 +103,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); // update a simple rule's name - const updatedRule = getSimpleRule('rule-1'); + const updatedRule = getSimpleRuleUpdate('rule-1'); updatedRule.rule_id = createRuleBody.rule_id; updatedRule.name = 'some other name'; delete updatedRule.id; @@ -125,11 +126,11 @@ export default ({ getService }: FtrProviderContext) => { const { body: createdBody } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update a simple rule's name - const updatedRule = getSimpleRule('rule-1'); + const updatedRule = getSimpleRuleUpdate('rule-1'); updatedRule.name = 'some other name'; updatedRule.id = createdBody.id; delete updatedRule.rule_id; @@ -152,11 +153,11 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update a simple rule's enabled to false and another property - const updatedRule = getSimpleRule('rule-1'); + const updatedRule = getSimpleRuleUpdate('rule-1'); updatedRule.severity = 'low'; updatedRule.enabled = false; @@ -180,10 +181,10 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); - const ruleUpdate = getSimpleRule('rule-1'); + const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.timeline_title = 'some title'; ruleUpdate.timeline_id = 'some id'; @@ -194,7 +195,7 @@ export default ({ getService }: FtrProviderContext) => { .send(ruleUpdate) .expect(200); - const ruleUpdate2 = getSimpleRule('rule-1'); + const ruleUpdate2 = getSimpleRuleUpdate('rule-1'); ruleUpdate2.name = 'some other name'; // update a simple rule's name @@ -213,7 +214,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should give a 404 if it is given a fake id', async () => { - const simpleRule = getSimpleRule(); + const simpleRule = getSimpleRuleUpdate(); simpleRule.id = '5096dec6-b6b9-4d8d-8f93-6c2602079d9d'; delete simpleRule.rule_id; @@ -230,7 +231,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should give a 404 if it is given a fake rule_id', async () => { - const simpleRule = getSimpleRule(); + const simpleRule = getSimpleRuleUpdate(); simpleRule.rule_id = 'fake_id'; delete simpleRule.id; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts index 54f29939fb6b4e..24eee8deaf3d48 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts @@ -12,11 +12,11 @@ import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex, - getSimpleRule, getSimpleRuleOutput, removeServerGeneratedProperties, getSimpleRuleOutputWithoutRuleId, removeServerGeneratedPropertiesIncludingRuleId, + getSimpleRuleUpdate, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -40,10 +40,10 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); - const updatedRule = getSimpleRule('rule-1'); + const updatedRule = getSimpleRuleUpdate('rule-1'); updatedRule.name = 'some other name'; // update a simple rule's name @@ -65,20 +65,20 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // create a second simple rule await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-2')) + .send(getSimpleRuleUpdate('rule-2')) .expect(200); - const updatedRule1 = getSimpleRule('rule-1'); + const updatedRule1 = getSimpleRuleUpdate('rule-1'); updatedRule1.name = 'some other name'; - const updatedRule2 = getSimpleRule('rule-2'); + const updatedRule2 = getSimpleRuleUpdate('rule-2'); updatedRule2.name = 'some other name'; // update both rule names @@ -107,11 +107,11 @@ export default ({ getService }: FtrProviderContext) => { const { body: createRuleBody } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update a simple rule's name - const updatedRule1 = getSimpleRule('rule-1'); + const updatedRule1 = getSimpleRuleUpdate('rule-1'); updatedRule1.id = createRuleBody.id; updatedRule1.name = 'some other name'; delete updatedRule1.rule_id; @@ -134,23 +134,23 @@ export default ({ getService }: FtrProviderContext) => { const { body: createRule1 } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // create a second simple rule const { body: createRule2 } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-2')) + .send(getSimpleRuleUpdate('rule-2')) .expect(200); // update both rule names - const updatedRule1 = getSimpleRule('rule-1'); + const updatedRule1 = getSimpleRuleUpdate('rule-1'); updatedRule1.id = createRule1.id; updatedRule1.name = 'some other name'; delete updatedRule1.rule_id; - const updatedRule2 = getSimpleRule('rule-1'); + const updatedRule2 = getSimpleRuleUpdate('rule-1'); updatedRule2.id = createRule2.id; updatedRule2.name = 'some other name'; delete updatedRule2.rule_id; @@ -180,11 +180,11 @@ export default ({ getService }: FtrProviderContext) => { const { body: createdBody } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update a simple rule's name - const updatedRule1 = getSimpleRule('rule-1'); + const updatedRule1 = getSimpleRuleUpdate('rule-1'); updatedRule1.id = createdBody.id; updatedRule1.name = 'some other name'; delete updatedRule1.rule_id; @@ -207,11 +207,11 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update a simple rule's enabled to false and another property - const updatedRule1 = getSimpleRule('rule-1'); + const updatedRule1 = getSimpleRuleUpdate('rule-1'); updatedRule1.severity = 'low'; updatedRule1.enabled = false; @@ -235,11 +235,11 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update a simple rule's timeline_title - const ruleUpdate = getSimpleRule('rule-1'); + const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.timeline_title = 'some title'; ruleUpdate.timeline_id = 'some id'; @@ -250,7 +250,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); // update a simple rule's name - const ruleUpdate2 = getSimpleRule('rule-1'); + const ruleUpdate2 = getSimpleRuleUpdate('rule-1'); ruleUpdate2.name = 'some other name'; const { body } = await supertest @@ -268,7 +268,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return a 200 but give a 404 in the message if it is given a fake id', async () => { - const ruleUpdate = getSimpleRule('rule-1'); + const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.id = '1fd52120-d3a9-4e7a-b23c-96c0e1a74ae5'; delete ruleUpdate.rule_id; @@ -290,7 +290,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return a 200 but give a 404 in the message if it is given a fake rule_id', async () => { - const ruleUpdate = getSimpleRule('rule-1'); + const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.rule_id = 'fake_id'; delete ruleUpdate.id; @@ -313,14 +313,14 @@ export default ({ getService }: FtrProviderContext) => { await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); - const ruleUpdate = getSimpleRule('rule-1'); + const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.name = 'some other name'; delete ruleUpdate.id; - const ruleUpdate2 = getSimpleRule('fake_id'); + const ruleUpdate2 = getSimpleRuleUpdate('fake_id'); ruleUpdate2.name = 'some other name'; delete ruleUpdate.id; @@ -353,16 +353,16 @@ export default ({ getService }: FtrProviderContext) => { const { body: createdBody } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getSimpleRule('rule-1')) + .send(getSimpleRuleUpdate('rule-1')) .expect(200); // update one rule name and give a fake id for the second - const rule1 = getSimpleRule(); + const rule1 = getSimpleRuleUpdate(); delete rule1.rule_id; rule1.id = createdBody.id; rule1.name = 'some other name'; - const rule2 = getSimpleRule(); + const rule2 = getSimpleRuleUpdate(); delete rule2.rule_id; rule2.id = 'b3aa019a-656c-4311-b13b-4d9852e24347'; rule2.name = 'some other name'; diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index b4330ce5d3912a..5f0a8cd9384081 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -7,7 +7,9 @@ import { Client } from '@elastic/elasticsearch'; import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; -import { OutputRuleAlertRest } from '../../plugins/security_solution/server/lib/detection_engine/types'; +import { CreateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema'; +import { UpdateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema'; +import { RulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema'; import { DETECTION_ENGINE_INDEX_URL } from '../../plugins/security_solution/common/constants'; /** @@ -15,8 +17,8 @@ import { DETECTION_ENGINE_INDEX_URL } from '../../plugins/security_solution/comm * @param rule Rule to pass in to remove typical server generated properties */ export const removeServerGeneratedProperties = ( - rule: Partial -): Partial => { + rule: Partial +): Partial => { const { created_at, updated_at, @@ -37,8 +39,8 @@ export const removeServerGeneratedProperties = ( * @param rule Rule to pass in to remove typical server generated properties */ export const removeServerGeneratedPropertiesIncludingRuleId = ( - rule: Partial -): Partial => { + rule: Partial +): Partial => { const ruleWithRemovedProperties = removeServerGeneratedProperties(rule); const { rule_id, ...additionalRuledIdRemoved } = ruleWithRemovedProperties; return additionalRuledIdRemoved; @@ -48,7 +50,22 @@ export const removeServerGeneratedPropertiesIncludingRuleId = ( * This is a typical simple rule for testing that is easy for most basic testing * @param ruleId */ -export const getSimpleRule = (ruleId = 'rule-1'): Partial => ({ +export const getSimpleRule = (ruleId = 'rule-1'): CreateRulesSchema => ({ + name: 'Simple Rule Query', + description: 'Simple Rule Query', + risk_score: 1, + rule_id: ruleId, + severity: 'high', + index: ['auditbeat-*'], + type: 'query', + query: 'user.name: root or user.name: admin', +}); + +/** + * This is a typical simple rule for testing that is easy for most basic testing + * @param ruleId + */ +export const getSimpleRuleUpdate = (ruleId = 'rule-1'): UpdateRulesSchema => ({ name: 'Simple Rule Query', description: 'Simple Rule Query', risk_score: 1, @@ -63,7 +80,18 @@ export const getSimpleRule = (ruleId = 'rule-1'): Partial = * This is a representative ML rule payload as expected by the server * @param ruleId */ -export const getSimpleMlRule = (ruleId = 'rule-1'): Partial => ({ +export const getSimpleMlRule = (ruleId = 'rule-1'): CreateRulesSchema => ({ + name: 'Simple ML Rule', + description: 'Simple Machine Learning Rule', + anomaly_threshold: 44, + risk_score: 1, + rule_id: ruleId, + severity: 'high', + machine_learning_job_id: 'some_job_id', + type: 'machine_learning', +}); + +export const getSimpleMlRuleUpdate = (ruleId = 'rule-1'): UpdateRulesSchema => ({ name: 'Simple ML Rule', description: 'Simple Machine Learning Rule', anomaly_threshold: 44, @@ -107,7 +135,7 @@ export const getSignalStatusEmptyResponse = () => ({ /** * This is a typical simple rule for testing that is easy for most basic testing */ -export const getSimpleRuleWithoutRuleId = (): Partial => { +export const getSimpleRuleWithoutRuleId = (): CreateRulesSchema => { const simpleRule = getSimpleRule(); const { rule_id, ...ruleWithoutId } = simpleRule; return ruleWithoutId; @@ -130,9 +158,10 @@ export const binaryToString = (res: any, callback: any): void => { }; /** - * This is the typical output of a simple rule that Kibana will output with all the defaults. + * This is the typical output of a simple rule that Kibana will output with all the defaults + * except for the server generated properties. Useful for testing end to end tests. */ -export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial => ({ +export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial => ({ actions: [], created_by: 'elastic', description: 'Simple Rule Query', @@ -162,17 +191,16 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial => { +export const getSimpleRuleOutputWithoutRuleId = (ruleId = 'rule-1'): Partial => { const rule = getSimpleRuleOutput(ruleId); const { rule_id, ...ruleWithoutRuleId } = rule; return ruleWithoutRuleId; }; -export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial => { +export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial => { const rule = getSimpleRuleOutput(ruleId); const { query, language, index, ...rest } = rule; @@ -252,7 +280,7 @@ export const getSimpleRuleAsNdjson = (ruleIds: string[]): Buffer => { * testing upload features. * @param rule The rule to convert to ndjson */ -export const ruleToNdjson = (rule: Partial): Buffer => { +export const ruleToNdjson = (rule: Partial): Buffer => { const stringified = JSON.stringify(rule); return Buffer.from(`${stringified}\n`); }; @@ -261,7 +289,7 @@ export const ruleToNdjson = (rule: Partial): Buffer => { * This will return a complex rule with all the outputs possible * @param ruleId The ruleId to set which is optional and defaults to rule-1 */ -export const getComplexRule = (ruleId = 'rule-1'): Partial => ({ +export const getComplexRule = (ruleId = 'rule-1'): Partial => ({ actions: [], name: 'Complex Rule Query', description: 'Complex Rule Query', @@ -345,7 +373,7 @@ export const getComplexRule = (ruleId = 'rule-1'): Partial * This will return a complex rule with all the outputs possible * @param ruleId The ruleId to set which is optional and defaults to rule-1 */ -export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial => ({ +export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial => ({ actions: [], created_by: 'elastic', name: 'Complex Rule Query', diff --git a/x-pack/test/functional/apps/index_management/home_page.ts b/x-pack/test/functional/apps/index_management/home_page.ts index cca0a5d1ad1bd7..90bc3603c16133 100644 --- a/x-pack/test/functional/apps/index_management/home_page.ts +++ b/x-pack/test/functional/apps/index_management/home_page.ts @@ -18,12 +18,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.common.navigateToApp('indexManagement'); }); - it('Loads the app', async () => { + it('Loads the app and renders the indices tab by default', async () => { await log.debug('Checking for section heading to say Index Management.'); const headingText = await pageObjects.indexManagement.sectionHeadingText(); expect(headingText).to.be('Index Management'); + // Verify url + const url = await browser.getCurrentUrl(); + expect(url).to.contain(`/indices`); + + // Verify content const indicesList = await testSubjects.exists('indicesList'); expect(indicesList).to.be(true); @@ -31,6 +36,23 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(await reloadIndicesButton.isDisplayed()).to.be(true); }); + describe('Data streams', () => { + it('renders the data streams tab', async () => { + // Navigate to the data streams tab + await pageObjects.indexManagement.changeTabs('data_streamsTab'); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify url + const url = await browser.getCurrentUrl(); + expect(url).to.contain(`/data_streams`); + + // Verify content + const dataStreamList = await testSubjects.exists('dataStreamList'); + expect(dataStreamList).to.be(true); + }); + }); + describe('Index templates', () => { it('renders the index templates tab', async () => { // Navigate to the index templates tab diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index ff40cde0704167..9e04f6e9df22b5 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -29,6 +29,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { this.tags(['ciGroup4', 'skipFirefox']); loadTestFile(require.resolve('./smokescreen')); + loadTestFile(require.resolve('./persistent_context')); loadTestFile(require.resolve('./lens_reporting')); }); }); diff --git a/x-pack/test/functional/apps/lens/persistent_context.ts b/x-pack/test/functional/apps/lens/persistent_context.ts new file mode 100644 index 00000000000000..00d9208772798d --- /dev/null +++ b/x-pack/test/functional/apps/lens/persistent_context.ts @@ -0,0 +1,60 @@ +/* + * 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 _ from 'lodash'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'header', 'timePicker']); + const browser = getService('browser'); + const filterBar = getService('filterBar'); + const appsMenu = getService('appsMenu'); + + describe('lens query context', () => { + it('should carry over time range and pinned filters to discover', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.timePicker.setAbsoluteRange( + 'Sep 06, 2015 @ 06:31:44.000', + 'Sep 18, 2025 @ 06:31:44.000' + ); + await filterBar.addFilter('ip', 'is', '97.220.3.248'); + await filterBar.toggleFilterPinned('ip'); + await PageObjects.header.clickDiscover(); + const timeRange = await PageObjects.timePicker.getTimeConfig(); + expect(timeRange.start).to.equal('Sep 6, 2015 @ 06:31:44.000'); + expect(timeRange.end).to.equal('Sep 18, 2025 @ 06:31:44.000'); + await filterBar.hasFilter('ip', '97.220.3.248', true, true); + }); + + it('should remember time range and pinned filters from discover', async () => { + await PageObjects.timePicker.setAbsoluteRange( + 'Sep 07, 2015 @ 06:31:44.000', + 'Sep 19, 2025 @ 06:31:44.000' + ); + await filterBar.toggleFilterEnabled('ip'); + await appsMenu.clickLink('Visualize', { category: 'kibana' }); + await PageObjects.visualize.clickNewVisualization(); + await PageObjects.visualize.waitForVisualizationSelectPage(); + await PageObjects.visualize.clickVisType('lens'); + const timeRange = await PageObjects.timePicker.getTimeConfig(); + expect(timeRange.start).to.equal('Sep 7, 2015 @ 06:31:44.000'); + expect(timeRange.end).to.equal('Sep 19, 2025 @ 06:31:44.000'); + await filterBar.hasFilter('ip', '97.220.3.248', false, true); + }); + + it('keep time range and pinned filters after refresh', async () => { + await browser.refresh(); + await PageObjects.header.waitUntilLoadingHasFinished(); + const timeRange = await PageObjects.timePicker.getTimeConfig(); + expect(timeRange.start).to.equal('Sep 7, 2015 @ 06:31:44.000'); + expect(timeRange.end).to.equal('Sep 19, 2025 @ 06:31:44.000'); + await filterBar.hasFilter('ip', '97.220.3.248', false, true); + }); + }); +} diff --git a/x-pack/test/functional/page_objects/index_management_page.ts b/x-pack/test/functional/page_objects/index_management_page.ts index 9bfcd79671b4d4..5e5d0e7583450e 100644 --- a/x-pack/test/functional/page_objects/index_management_page.ts +++ b/x-pack/test/functional/page_objects/index_management_page.ts @@ -44,7 +44,10 @@ export function IndexManagementPageProvider({ getService }: FtrProviderContext) }; }); }, - async changeTabs(tab: 'indicesTab' | 'templatesTab' | 'component_templatesTab') { + + async changeTabs( + tab: 'indicesTab' | 'data_streamsTab' | 'templatesTab' | 'component_templatesTab' + ) { await testSubjects.click(tab); }, }; diff --git a/x-pack/test/functional_endpoint/page_objects/page_utils.ts b/x-pack/test/functional_endpoint/page_objects/page_utils.ts deleted file mode 100644 index daf66464f7e1ed..00000000000000 --- a/x-pack/test/functional_endpoint/page_objects/page_utils.ts +++ /dev/null @@ -1,30 +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 { FtrProviderContext } from '../ftr_provider_context'; - -export function EndpointPageUtils({ getService }: FtrProviderContext) { - const find = getService('find'); - - return { - /** - * Finds a given EuiCheckbox by test subject and clicks on it - * - * @param euiCheckBoxTestId - */ - async clickOnEuiCheckbox(euiCheckBoxTestId: string) { - // This utility is needed because EuiCheckbox forwards the test subject on to - // the actual `` which is not actually visible/accessible on the page. - // In order to actually cause the state of the checkbox to change, the `