diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 516de74378cbc..0993876f98a6a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -262,8 +262,31 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Enterprise Search # Shared -/x-pack/plugins/enterprise_search/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/* @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/common/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/public/* @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/public/applications/* @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/public/applications/enterprise_search/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/public/applications/shared/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/public/applications/__mocks__/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/server/* @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/server/lib/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/server/__mocks__/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/server/collectors/lib/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/server/routes/enterprise_search/ @elastic/enterprise-search-frontend +/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/ @elastic/enterprise-search-frontend /x-pack/test/functional_enterprise_search/ @elastic/enterprise-search-frontend +# App Search +/x-pack/plugins/enterprise_search/public/applications/app_search/ @elastic/app-search-frontend +/x-pack/plugins/enterprise_search/server/routes/app_search/ @elastic/app-search-frontend +/x-pack/plugins/enterprise_search/server/collectors/app_search/ @elastic/app-search-frontend +/x-pack/plugins/enterprise_search/server/saved_objects/app_search/ @elastic/app-search-frontend +# Workplace Search +/x-pack/plugins/enterprise_search/public/applications/workplace_search/ @elastic/workplace-search-frontend +/x-pack/plugins/enterprise_search/server/routes/workplace_search/ @elastic/workplace-search-frontend +/x-pack/plugins/enterprise_search/server/collectors/workplace_search/ @elastic/workplace-search-frontend +/x-pack/plugins/enterprise_search/server/saved_objects/workplace_search/ @elastic/workplace-search-frontend # Elasticsearch UI /src/plugins/dev_tools/ @elastic/es-ui diff --git a/docs/api/logstash-configuration-management/create-logstash.asciidoc b/docs/api/logstash-configuration-management/create-logstash.asciidoc index b608f4ee698f7..9bd5a9028ee9a 100644 --- a/docs/api/logstash-configuration-management/create-logstash.asciidoc +++ b/docs/api/logstash-configuration-management/create-logstash.asciidoc @@ -20,9 +20,6 @@ experimental[] Create a centrally-managed Logstash pipeline, or update an existi [[logstash-configuration-management-api-create-request-body]] ==== Request body -`id`:: - (Required, string) The pipeline ID. - `description`:: (Optional, string) The pipeline description. diff --git a/docs/api/saved-objects/create.asciidoc b/docs/api/saved-objects/create.asciidoc index 50809a1bd5d4e..d7a368034ef07 100644 --- a/docs/api/saved-objects/create.asciidoc +++ b/docs/api/saved-objects/create.asciidoc @@ -9,11 +9,13 @@ experimental[] Create {kib} saved objects. [[saved-objects-api-create-request]] ==== Request -`POST :/api/saved_objects/` + +`POST :/api/saved_objects/` `POST :/api/saved_objects//` -`POST :/s//saved_objects/` +`POST :/s//api/saved_objects/` + +`POST :/s//api/saved_objects//` [[saved-objects-api-create-path-params]] ==== Path parameters diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md new file mode 100644 index 0000000000000..61091306d0dbc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) > [initialize](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md) + +## SavedObjectsIncrementCounterOptions.initialize property + +(default=false) If true, sets all the counter fields to 0 if they don't already exist. Existing fields will be left as-is and won't be incremented. + +Signature: + +```typescript +initialize?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md index 6077945ddd376..68e9bb09456cd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.md @@ -15,6 +15,7 @@ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOpt | Property | Type | Description | | --- | --- | --- | -| [migrationVersion](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md) | SavedObjectsMigrationVersion | | -| [refresh](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | +| [initialize](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.initialize.md) | boolean | (default=false) If true, sets all the counter fields to 0 if they don't already exist. Existing fields will be left as-is and won't be incremented. | +| [migrationVersion](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md) | SavedObjectsMigrationVersion | [SavedObjectsMigrationVersion](./kibana-plugin-core-server.savedobjectsmigrationversion.md) | +| [refresh](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md) | MutatingOperationRefreshSetting | (default='wait\_for') The Elasticsearch refresh setting for this operation. See [MutatingOperationRefreshSetting](./kibana-plugin-core-server.mutatingoperationrefreshsetting.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md index 417db99fd5a27..aff80138d61cf 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.migrationversion.md @@ -4,6 +4,8 @@ ## SavedObjectsIncrementCounterOptions.migrationVersion property +[SavedObjectsMigrationVersion](./kibana-plugin-core-server.savedobjectsmigrationversion.md) + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md index 31d957ca30a3e..4f217cc223d46 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsincrementcounteroptions.refresh.md @@ -4,7 +4,7 @@ ## SavedObjectsIncrementCounterOptions.refresh property -The Elasticsearch Refresh setting for this operation +(default='wait\_for') The Elasticsearch refresh setting for this operation. See [MutatingOperationRefreshSetting](./kibana-plugin-core-server.mutatingoperationrefreshsetting.md) Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md index f3a2ee38cbdbd..dc62cacf6741b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md @@ -4,26 +4,53 @@ ## SavedObjectsRepository.incrementCounter() method -Increases a counter field by one. Creates the document if one doesn't exist for the given id. +Increments all the specified counter fields by one. Creates the document if one doesn't exist for the given id. Signature: ```typescript -incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise; +incrementCounter(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| id | string | | -| counterFieldName | string | | -| options | SavedObjectsIncrementCounterOptions | | +| type | string | The type of saved object whose fields should be incremented | +| id | string | The id of the document whose fields should be incremented | +| counterFieldNames | string[] | An array of field names to increment | +| options | SavedObjectsIncrementCounterOptions | [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) | Returns: `Promise` -{promise} +The saved object after the specified fields were incremented + +## Remarks + +When supplying a field name like `stats.api.counter` the field name will be used as-is to create a document like: `{attributes: {'stats.api.counter': 1}}` It will not create a nested structure like: `{attributes: {stats: {api: {counter: 1}}}}` + +When using incrementCounter for collecting usage data, you need to ensure that usage collection happens on a best-effort basis and doesn't negatively affect your plugin or users. See https://github.com/elastic/kibana/blob/master/src/plugins/usage\_collection/README.md\#tracking-interactions-with-incrementcounter) + +## Example + + +```ts +const repository = coreStart.savedObjects.createInternalRepository(); + +// Initialize all fields to 0 +repository + .incrementCounter('dashboard_counter_type', 'counter_id', [ + 'stats.apiCalls', + 'stats.sampleDataInstalled', + ], {initialize: true}); + +// Increment the apiCalls field counter +repository + .incrementCounter('dashboard_counter_type', 'counter_id', [ + 'stats.apiCalls', + ]) + +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md index 6a56f0bee718b..e0a6b8af5658a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -26,7 +26,7 @@ export declare class SavedObjectsRepository | [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[addToNamespaces\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | | [find(options)](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object | -| [incrementCounter(type, id, counterFieldName, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increases a counter field by one. Creates the document if one doesn't exist for the given id. | +| [incrementCounter(type, id, counterFieldNames, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increments all the specified counter fields by one. Creates the document if one doesn't exist for the given id. | | [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md) | | Updates all objects containing a reference to the given {type, id} tuple to remove the said reference. | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md index b2f8e83d8e654..a370c67f460f4 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md @@ -17,6 +17,7 @@ export interface ISearchSetup | Property | Type | Description | | --- | --- | --- | | [aggs](./kibana-plugin-plugins-data-public.isearchsetup.aggs.md) | AggsSetup | | -| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService | session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | +| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService | Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | +| [sessionsClient](./kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md) | ISessionsClient | Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | | [usageCollector](./kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md) | SearchUsageCollector | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md index 739fdfdeb5fc3..451dbc86b86b6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md @@ -4,7 +4,7 @@ ## ISearchSetup.session property -session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) +Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md new file mode 100644 index 0000000000000..d9af202cf1018 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) > [sessionsClient](./kibana-plugin-plugins-data-public.isearchsetup.sessionsclient.md) + +## ISearchSetup.sessionsClient property + +Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) + +Signature: + +```typescript +sessionsClient: ISessionsClient; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md index dba60c7bdf147..a27e155dda111 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md @@ -19,6 +19,7 @@ export interface ISearchStart | [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) | AggsStart | agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | | [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | ISearchGeneric | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | ISearchStartSearchSource | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | -| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService | session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | +| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService | Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | +| [sessionsClient](./kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md) | ISessionsClient | Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | | [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | (e: Error) => void | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md index 1ad194a9bec86..892b0fa6acb60 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md @@ -4,7 +4,7 @@ ## ISearchStart.session property -session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) +Current session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md new file mode 100644 index 0000000000000..9c3210d2ec417 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) > [sessionsClient](./kibana-plugin-plugins-data-public.isearchstart.sessionsclient.md) + +## ISearchStart.sessionsClient property + +Background search sessions SO CRUD [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) + +Signature: + +```typescript +sessionsClient: ISessionsClient; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionsclient.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionsclient.md new file mode 100644 index 0000000000000..d6efabb1b9518 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionsclient.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) + +## ISessionsClient type + +Signature: + +```typescript +export declare type ISessionsClient = PublicContract; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md deleted file mode 100644 index fc3d214eb4cad..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [clear](./kibana-plugin-plugins-data-public.isessionservice.clear.md) - -## ISessionService.clear property - -Clears the active session. - -Signature: - -```typescript -clear: () => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md deleted file mode 100644 index eabb966160c4d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [delete](./kibana-plugin-plugins-data-public.isessionservice.delete.md) - -## ISessionService.delete property - -Deletes a session - -Signature: - -```typescript -delete: (sessionId: string) => Promise; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md deleted file mode 100644 index 58e2fea0e6fe9..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [find](./kibana-plugin-plugins-data-public.isessionservice.find.md) - -## ISessionService.find property - -Gets a list of saved sessions - -Signature: - -```typescript -find: (options: SearchSessionFindOptions) => Promise>; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md deleted file mode 100644 index a2dff2f18253b..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [get](./kibana-plugin-plugins-data-public.isessionservice.get.md) - -## ISessionService.get property - -Gets a saved session - -Signature: - -```typescript -get: (sessionId: string) => Promise>; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md deleted file mode 100644 index e30c89fb1a9fd..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [getSession$](./kibana-plugin-plugins-data-public.isessionservice.getsession_.md) - -## ISessionService.getSession$ property - -Returns the observable that emits an update every time the session ID changes - -Signature: - -```typescript -getSession$: () => Observable; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md deleted file mode 100644 index 838023ff1d8b9..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [getSessionId](./kibana-plugin-plugins-data-public.isessionservice.getsessionid.md) - -## ISessionService.getSessionId property - -Returns the active session ID - -Signature: - -```typescript -getSessionId: () => string | undefined; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md deleted file mode 100644 index 8d8cd1f31bb95..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [isRestore](./kibana-plugin-plugins-data-public.isessionservice.isrestore.md) - -## ISessionService.isRestore property - -Whether the active session is restored (i.e. reusing previous search IDs) - -Signature: - -```typescript -isRestore: () => boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md deleted file mode 100644 index db737880bb84e..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [isStored](./kibana-plugin-plugins-data-public.isessionservice.isstored.md) - -## ISessionService.isStored property - -Whether the active session is already saved (i.e. sent to background) - -Signature: - -```typescript -isStored: () => boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md index 02c0a821e552d..8938c880a0471 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md @@ -2,28 +2,10 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) -## ISessionService interface +## ISessionService type Signature: ```typescript -export interface ISessionService +export declare type ISessionService = PublicContract; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [clear](./kibana-plugin-plugins-data-public.isessionservice.clear.md) | () => void | Clears the active session. | -| [delete](./kibana-plugin-plugins-data-public.isessionservice.delete.md) | (sessionId: string) => Promise<void> | Deletes a session | -| [find](./kibana-plugin-plugins-data-public.isessionservice.find.md) | (options: SearchSessionFindOptions) => Promise<SavedObjectsFindResponse<BackgroundSessionSavedObjectAttributes>> | Gets a list of saved sessions | -| [get](./kibana-plugin-plugins-data-public.isessionservice.get.md) | (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>> | Gets a saved session | -| [getSession$](./kibana-plugin-plugins-data-public.isessionservice.getsession_.md) | () => Observable<string | undefined> | Returns the observable that emits an update every time the session ID changes | -| [getSessionId](./kibana-plugin-plugins-data-public.isessionservice.getsessionid.md) | () => string | undefined | Returns the active session ID | -| [isRestore](./kibana-plugin-plugins-data-public.isessionservice.isrestore.md) | () => boolean | Whether the active session is restored (i.e. reusing previous search IDs) | -| [isStored](./kibana-plugin-plugins-data-public.isessionservice.isstored.md) | () => boolean | Whether the active session is already saved (i.e. sent to background) | -| [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) | (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>> | Restores existing session | -| [save](./kibana-plugin-plugins-data-public.isessionservice.save.md) | (name: string, url: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>> | Saves a session | -| [start](./kibana-plugin-plugins-data-public.isessionservice.start.md) | () => string | Starts a new session | -| [update](./kibana-plugin-plugins-data-public.isessionservice.update.md) | (sessionId: string, attributes: Partial<BackgroundSessionSavedObjectAttributes>) => Promise<any> | Updates a session | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md deleted file mode 100644 index 96106a6ef7e2d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) - -## ISessionService.restore property - -Restores existing session - -Signature: - -```typescript -restore: (sessionId: string) => Promise>; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md deleted file mode 100644 index 4ac4a96614467..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [save](./kibana-plugin-plugins-data-public.isessionservice.save.md) - -## ISessionService.save property - -Saves a session - -Signature: - -```typescript -save: (name: string, url: string) => Promise>; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md deleted file mode 100644 index 9e14c5ed26765..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [start](./kibana-plugin-plugins-data-public.isessionservice.start.md) - -## ISessionService.start property - -Starts a new session - -Signature: - -```typescript -start: () => string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md deleted file mode 100644 index 5e2ff53d58ab7..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [update](./kibana-plugin-plugins-data-public.isessionservice.update.md) - -## ISessionService.update property - -Updates a session - -Signature: - -```typescript -update: (sessionId: string, attributes: Partial) => Promise; -``` 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 b8e45cde3c18b..9121b0aade470 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 @@ -34,6 +34,7 @@ | [KBN\_FIELD\_TYPES](./kibana-plugin-plugins-data-public.kbn_field_types.md) | \* | | [METRIC\_TYPES](./kibana-plugin-plugins-data-public.metric_types.md) | | | [QuerySuggestionTypes](./kibana-plugin-plugins-data-public.querysuggestiontypes.md) | | +| [SessionState](./kibana-plugin-plugins-data-public.sessionstate.md) | Possible state that current session can be in | | [SortDirection](./kibana-plugin-plugins-data-public.sortdirection.md) | | | [TimeoutErrorMode](./kibana-plugin-plugins-data-public.timeouterrormode.md) | | @@ -74,7 +75,6 @@ | [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) | The setup contract exposed by the Search plugin exposes the search strategy extension point. | | [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) | search service | | [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | high level search service | -| [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | | | [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) | | | [OptionedValueProp](./kibana-plugin-plugins-data-public.optionedvalueprop.md) | | | [QueryState](./kibana-plugin-plugins-data-public.querystate.md) | All query state service state | @@ -89,6 +89,7 @@ | [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | | | [SearchError](./kibana-plugin-plugins-data-public.searcherror.md) | | | [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | | +| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) | Provide info about current search session to be stored in backgroundSearch saved object | | [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields | | [TabbedAggColumn](./kibana-plugin-plugins-data-public.tabbedaggcolumn.md) | \* | | [TabbedTable](./kibana-plugin-plugins-data-public.tabbedtable.md) | \* | @@ -166,6 +167,8 @@ | [InputTimeRange](./kibana-plugin-plugins-data-public.inputtimerange.md) | | | [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | | [ISearchSource](./kibana-plugin-plugins-data-public.isearchsource.md) | search source interface | +| [ISessionsClient](./kibana-plugin-plugins-data-public.isessionsclient.md) | | +| [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | | | [KibanaContext](./kibana-plugin-plugins-data-public.kibanacontext.md) | | | [MatchAllFilter](./kibana-plugin-plugins-data-public.matchallfilter.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-public.parsedinterval.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md new file mode 100644 index 0000000000000..0f0b616066dd6 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) > [getName](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.getname.md) + +## SearchSessionInfoProvider.getName property + +User-facing name of the session. e.g. will be displayed in background sessions management list + +Signature: + +```typescript +getName: () => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md new file mode 100644 index 0000000000000..207adaf2bd50b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) > [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.geturlgeneratordata.md) + +## SearchSessionInfoProvider.getUrlGeneratorData property + +Signature: + +```typescript +getUrlGeneratorData: () => Promise<{ + urlGeneratorId: ID; + initialState: UrlGeneratorStateMapping[ID]['State']; + restoreState: UrlGeneratorStateMapping[ID]['State']; + }>; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md new file mode 100644 index 0000000000000..a3d294f5e3303 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) + +## SearchSessionInfoProvider interface + +Provide info about current search session to be stored in backgroundSearch saved object + +Signature: + +```typescript +export interface SearchSessionInfoProvider +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [getName](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.getname.md) | () => Promise<string> | User-facing name of the session. e.g. will be displayed in background sessions management list | +| [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.geturlgeneratordata.md) | () => Promise<{
urlGeneratorId: ID;
initialState: UrlGeneratorStateMapping[ID]['State'];
restoreState: UrlGeneratorStateMapping[ID]['State'];
}> | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md new file mode 100644 index 0000000000000..9a60a5b2a9f9b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.sessionstate.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SessionState](./kibana-plugin-plugins-data-public.sessionstate.md) + +## SessionState enum + +Possible state that current session can be in + +Signature: + +```typescript +export declare enum SessionState +``` + +## Enumeration Members + +| Member | Value | Description | +| --- | --- | --- | +| BackgroundCompleted | "backgroundCompleted" | Page load completed with background session created. | +| BackgroundLoading | "backgroundLoading" | Search request was sent to the background. The page is loading in background. | +| Canceled | "canceled" | Current session requests where explicitly canceled by user Displaying none or partial results | +| Completed | "completed" | No action was taken and the page completed loading without background session creation. | +| Loading | "loading" | Pending search request has not been sent to the background yet | +| None | "none" | Session is not active, e.g. didn't start | +| Restored | "restored" | Revisiting the page after background completion | + diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index 05d022d039b23..667038739d45f 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -89,6 +89,8 @@ Here's a list of the available global configuration options and an explanation o * `xpack.actions.proxyRejectUnauthorizedCertificates`: Set to `false` to bypass certificate validation for proxy, if using a proxy for actions. * `xpack.actions.rejectUnauthorized`: Set to `false` to bypass certificate validation for actions. +*NOTE:* As an alternative to both `xpack.actions.proxyRejectUnauthorizedCertificates` and `xpack.actions.rejectUnauthorized`, the OS level environment variable `NODE_EXTRA_CA_CERTS` can be set to point to a file that contains the root CA(s) needed for certificates to be trusted. + [float] === Managing alerts diff --git a/packages/kbn-babel-preset/common_preset.js b/packages/kbn-babel-preset/common_preset.js index 8e2f1d207f3f4..b14dcd8971c31 100644 --- a/packages/kbn-babel-preset/common_preset.js +++ b/packages/kbn-babel-preset/common_preset.js @@ -28,18 +28,19 @@ const plugins = [ // See https://github.com/babel/proposals/issues/12 for progress require.resolve('@babel/plugin-proposal-class-properties'), - // Optional Chaining proposal is stage 3 (https://github.com/tc39/proposal-optional-chaining) + // Optional Chaining proposal is stage 4 (https://github.com/tc39/proposal-optional-chaining) // Need this since we are using TypeScript 3.7+ require.resolve('@babel/plugin-proposal-optional-chaining'), - // Nullish coalescing proposal is stage 3 (https://github.com/tc39/proposal-nullish-coalescing) + + // Nullish coalescing proposal is stage 4 (https://github.com/tc39/proposal-nullish-coalescing) // Need this since we are using TypeScript 3.7+ require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'), - // Proposal is on stage 4 (https://github.com/tc39/proposal-export-ns-from) + // Proposal is on stage 4, and included in ECMA-262 (https://github.com/tc39/proposal-export-ns-from) // Need this since we are using TypeScript 3.8+ require.resolve('@babel/plugin-proposal-export-namespace-from'), - // Proposal is on stage 4 (https://github.com/tc39/proposal-export-ns-from) + // Proposal is on stage 4, and included in ECMA-262 (https://github.com/tc39/proposal-export-ns-from) // Need this since we are using TypeScript 3.9+ require.resolve('@babel/plugin-proposal-private-methods'), ]; diff --git a/packages/kbn-monaco/package.json b/packages/kbn-monaco/package.json index e2406a73f5342..eef68d3a35e0c 100644 --- a/packages/kbn-monaco/package.json +++ b/packages/kbn-monaco/package.json @@ -11,5 +11,8 @@ "devDependencies": { "@kbn/babel-preset": "link:../kbn-babel-preset", "@kbn/dev-utils": "link:../kbn-dev-utils" + }, + "dependencies": { + "@kbn/i18n": "link:../kbn-i18n" } } \ No newline at end of file diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 6af14734444d1..15df6b34e22ff 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -39,9 +39,9 @@ export class DocLinksService { dashboard: { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html`, drilldowns: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html`, - drilldownsTriggerPicker: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html#url-drilldown`, + drilldownsTriggerPicker: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html#url-drilldowns`, urlDrilldownTemplateSyntax: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html`, - urlDrilldownVariables: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html#variables`, + urlDrilldownVariables: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html#url-template-variables`, }, filebeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`, diff --git a/src/core/server/saved_objects/service/lib/integration_tests/repository.test.ts b/src/core/server/saved_objects/service/lib/integration_tests/repository.test.ts new file mode 100644 index 0000000000000..2f64776501df0 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/integration_tests/repository.test.ts @@ -0,0 +1,152 @@ +/* + * 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 { InternalCoreStart } from 'src/core/server/internal_types'; +import * as kbnTestServer from '../../../../../test_helpers/kbn_server'; +import { Root } from '../../../../root'; + +const { startES } = kbnTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), +}); +let esServer: kbnTestServer.TestElasticsearchUtils; + +describe('SavedObjectsRepository', () => { + let root: Root; + let start: InternalCoreStart; + + beforeAll(async () => { + esServer = await startES(); + root = kbnTestServer.createRootWithCorePlugins({ + server: { + basePath: '/hello', + }, + }); + + const setup = await root.setup(); + setup.savedObjects.registerType({ + hidden: false, + mappings: { + dynamic: false, + properties: {}, + }, + name: 'test_counter_type', + namespaceType: 'single', + }); + start = await root.start(); + }); + + afterAll(async () => { + await esServer.stop(); + await root.shutdown(); + }); + + describe('#incrementCounter', () => { + describe('initialize=false', () => { + it('creates a new document if none exists and sets all counter fields set to 1', async () => { + const now = new Date().getTime(); + const repository = start.savedObjects.createInternalRepository(); + await repository.incrementCounter('test_counter_type', 'counter_1', [ + 'stats.api.count', + 'stats.api.count2', + 'stats.total', + ]); + const result = await repository.get('test_counter_type', 'counter_1'); + expect(result.attributes).toMatchInlineSnapshot(` + Object { + "stats.api.count": 1, + "stats.api.count2": 1, + "stats.total": 1, + } + `); + expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + }); + it('increments the specified counters of an existing document', async () => { + const repository = start.savedObjects.createInternalRepository(); + // Create document + await repository.incrementCounter('test_counter_type', 'counter_2', [ + 'stats.api.count', + 'stats.api.count2', + 'stats.total', + ]); + + const now = new Date().getTime(); + // Increment counters + await repository.incrementCounter('test_counter_type', 'counter_2', [ + 'stats.api.count', + 'stats.api.count2', + 'stats.total', + ]); + const result = await repository.get('test_counter_type', 'counter_2'); + expect(result.attributes).toMatchInlineSnapshot(` + Object { + "stats.api.count": 2, + "stats.api.count2": 2, + "stats.total": 2, + } + `); + expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + }); + }); + describe('initialize=true', () => { + it('creates a new document if none exists and sets all counter fields to 0', async () => { + const now = new Date().getTime(); + const repository = start.savedObjects.createInternalRepository(); + await repository.incrementCounter( + 'test_counter_type', + 'counter_3', + ['stats.api.count', 'stats.api.count2', 'stats.total'], + { initialize: true } + ); + const result = await repository.get('test_counter_type', 'counter_3'); + expect(result.attributes).toMatchInlineSnapshot(` + Object { + "stats.api.count": 0, + "stats.api.count2": 0, + "stats.total": 0, + } + `); + expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + }); + it('sets any undefined counter fields to 0 but does not alter existing fields of an existing document', async () => { + const repository = start.savedObjects.createInternalRepository(); + // Create document + await repository.incrementCounter('test_counter_type', 'counter_4', [ + 'stats.existing_field', + ]); + + const now = new Date().getTime(); + // Initialize counters + await repository.incrementCounter( + 'test_counter_type', + 'counter_4', + ['stats.existing_field', 'stats.new_field'], + { initialize: true } + ); + const result = await repository.get('test_counter_type', 'counter_4'); + expect(result.attributes).toMatchInlineSnapshot(` + Object { + "stats.existing_field": 1, + "stats.new_field": 0, + } + `); + expect(Date.parse(result.updated_at!)).toBeGreaterThanOrEqual(now); + }); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 6f885f17fd82b..8443d1dd07184 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -3272,11 +3272,11 @@ describe('SavedObjectsRepository', () => { describe('#incrementCounter', () => { const type = 'config'; const id = 'one'; - const field = 'buildNum'; + const counterFields = ['buildNum', 'apiCallsCount']; const namespace = 'foo-namespace'; const originId = 'some-origin-id'; - const incrementCounterSuccess = async (type, id, field, options) => { + const incrementCounterSuccess = async (type, id, fields, options) => { const isMultiNamespace = registry.isMultiNamespace(type); if (isMultiNamespace) { const response = getMockGetResponse({ type, id }, options?.namespace); @@ -3295,7 +3295,10 @@ describe('SavedObjectsRepository', () => { type, ...mockTimestampFields, [type]: { - [field]: 8468, + ...fields.reduce((acc, field) => { + acc[field] = 8468; + return acc; + }, {}), defaultIndex: 'logstash-*', }, }, @@ -3303,25 +3306,25 @@ describe('SavedObjectsRepository', () => { }) ); - const result = await savedObjectsRepository.incrementCounter(type, id, field, options); + const result = await savedObjectsRepository.incrementCounter(type, id, fields, options); expect(client.get).toHaveBeenCalledTimes(isMultiNamespace ? 1 : 0); return result; }; describe('client calls', () => { it(`should use the ES update action if type is not multi-namespace`, async () => { - await incrementCounterSuccess(type, id, field, { namespace }); + await incrementCounterSuccess(type, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledTimes(1); }); it(`should use the ES get action then update action if type is multi-namespace, ID is defined, and overwrite=true`, async () => { - await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, field, { namespace }); + await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, counterFields, { namespace }); expect(client.get).toHaveBeenCalledTimes(1); expect(client.update).toHaveBeenCalledTimes(1); }); it(`defaults to a refresh setting of wait_for`, async () => { - await incrementCounterSuccess(type, id, field, { namespace }); + await incrementCounterSuccess(type, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ refresh: 'wait_for', @@ -3331,7 +3334,7 @@ describe('SavedObjectsRepository', () => { }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - await incrementCounterSuccess(type, id, field, { namespace }); + await incrementCounterSuccess(type, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ id: `${namespace}:${type}:${id}`, @@ -3341,7 +3344,7 @@ describe('SavedObjectsRepository', () => { }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - await incrementCounterSuccess(type, id, field); + await incrementCounterSuccess(type, id, counterFields); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ id: `${type}:${id}`, @@ -3351,7 +3354,7 @@ describe('SavedObjectsRepository', () => { }); it(`normalizes options.namespace from 'default' to undefined`, async () => { - await incrementCounterSuccess(type, id, field, { namespace: 'default' }); + await incrementCounterSuccess(type, id, counterFields, { namespace: 'default' }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ id: `${type}:${id}`, @@ -3361,7 +3364,7 @@ describe('SavedObjectsRepository', () => { }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - await incrementCounterSuccess(NAMESPACE_AGNOSTIC_TYPE, id, field, { namespace }); + await incrementCounterSuccess(NAMESPACE_AGNOSTIC_TYPE, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ id: `${NAMESPACE_AGNOSTIC_TYPE}:${id}`, @@ -3370,7 +3373,7 @@ describe('SavedObjectsRepository', () => { ); client.update.mockClear(); - await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, field, { namespace }); + await incrementCounterSuccess(MULTI_NAMESPACE_TYPE, id, counterFields, { namespace }); expect(client.update).toHaveBeenCalledWith( expect.objectContaining({ id: `${MULTI_NAMESPACE_TYPE}:${id}`, @@ -3389,7 +3392,7 @@ describe('SavedObjectsRepository', () => { it(`throws when options.namespace is '*'`, async () => { await expect( - savedObjectsRepository.incrementCounter(type, id, field, { + savedObjectsRepository.incrementCounter(type, id, counterFields, { namespace: ALL_NAMESPACES_STRING, }) ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); @@ -3398,7 +3401,7 @@ describe('SavedObjectsRepository', () => { it(`throws when type is not a string`, async () => { const test = async (type) => { await expect( - savedObjectsRepository.incrementCounter(type, id, field) + savedObjectsRepository.incrementCounter(type, id, counterFields) ).rejects.toThrowError(`"type" argument must be a string`); expect(client.update).not.toHaveBeenCalled(); }; @@ -3413,23 +3416,24 @@ describe('SavedObjectsRepository', () => { const test = async (field) => { await expect( savedObjectsRepository.incrementCounter(type, id, field) - ).rejects.toThrowError(`"counterFieldName" argument must be a string`); + ).rejects.toThrowError(`"counterFieldNames" argument must be an array of strings`); expect(client.update).not.toHaveBeenCalled(); }; - await test(null); - await test(42); - await test(false); - await test({}); + await test([null]); + await test([42]); + await test([false]); + await test([{}]); + await test([{}, false, 42, null, 'string']); }); it(`throws when type is invalid`, async () => { - await expectUnsupportedTypeError('unknownType', id, field); + await expectUnsupportedTypeError('unknownType', id, counterFields); expect(client.update).not.toHaveBeenCalled(); }); it(`throws when type is hidden`, async () => { - await expectUnsupportedTypeError(HIDDEN_TYPE, id, field); + await expectUnsupportedTypeError(HIDDEN_TYPE, id, counterFields); expect(client.update).not.toHaveBeenCalled(); }); @@ -3439,7 +3443,9 @@ describe('SavedObjectsRepository', () => { elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); await expect( - savedObjectsRepository.incrementCounter(MULTI_NAMESPACE_TYPE, id, field, { namespace }) + savedObjectsRepository.incrementCounter(MULTI_NAMESPACE_TYPE, id, counterFields, { + namespace, + }) ).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_TYPE, id)); expect(client.get).toHaveBeenCalledTimes(1); }); @@ -3452,8 +3458,8 @@ describe('SavedObjectsRepository', () => { it(`migrates a document and serializes the migrated doc`, async () => { const migrationVersion = mockMigrationVersion; - await incrementCounterSuccess(type, id, field, { migrationVersion }); - const attributes = { buildNum: 1 }; // this is added by the incrementCounter function + await incrementCounterSuccess(type, id, counterFields, { migrationVersion }); + const attributes = { buildNum: 1, apiCallsCount: 1 }; // this is added by the incrementCounter function const doc = { type, id, attributes, migrationVersion, ...mockTimestampFields }; expectMigrationArgs(doc); @@ -3476,6 +3482,7 @@ describe('SavedObjectsRepository', () => { ...mockTimestampFields, config: { buildNum: 8468, + apiCallsCount: 100, defaultIndex: 'logstash-*', }, originId, @@ -3487,7 +3494,7 @@ describe('SavedObjectsRepository', () => { const response = await savedObjectsRepository.incrementCounter( 'config', '6.0.0-alpha1', - 'buildNum', + ['buildNum', 'apiCallsCount'], { namespace: 'foo-namespace', } @@ -3500,6 +3507,7 @@ describe('SavedObjectsRepository', () => { version: mockVersion, attributes: { buildNum: 8468, + apiCallsCount: 100, defaultIndex: 'logstash-*', }, originId, diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index d362c02de4915..2f09ad71de558 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -101,8 +101,17 @@ export interface SavedObjectsRepositoryOptions { * @public */ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions { + /** + * (default=false) If true, sets all the counter fields to 0 if they don't + * already exist. Existing fields will be left as-is and won't be incremented. + */ + initialize?: boolean; + /** {@link SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; - /** The Elasticsearch Refresh setting for this operation */ + /** + * (default='wait_for') The Elasticsearch refresh setting for this + * operation. See {@link MutatingOperationRefreshSetting} + */ refresh?: MutatingOperationRefreshSetting; } @@ -1515,32 +1524,64 @@ export class SavedObjectsRepository { } /** - * Increases a counter field by one. Creates the document if one doesn't exist for the given id. + * Increments all the specified counter fields by one. Creates the document + * if one doesn't exist for the given id. * - * @param {string} type - * @param {string} id - * @param {string} counterFieldName - * @param {object} [options={}] - * @property {object} [options.migrationVersion=undefined] - * @returns {promise} + * @remarks + * When supplying a field name like `stats.api.counter` the field name will + * be used as-is to create a document like: + * `{attributes: {'stats.api.counter': 1}}` + * It will not create a nested structure like: + * `{attributes: {stats: {api: {counter: 1}}}}` + * + * When using incrementCounter for collecting usage data, you need to ensure + * that usage collection happens on a best-effort basis and doesn't + * negatively affect your plugin or users. See https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.md#tracking-interactions-with-incrementcounter) + * + * @example + * ```ts + * const repository = coreStart.savedObjects.createInternalRepository(); + * + * // Initialize all fields to 0 + * repository + * .incrementCounter('dashboard_counter_type', 'counter_id', [ + * 'stats.apiCalls', + * 'stats.sampleDataInstalled', + * ], {initialize: true}); + * + * // Increment the apiCalls field counter + * repository + * .incrementCounter('dashboard_counter_type', 'counter_id', [ + * 'stats.apiCalls', + * ]) + * ``` + * + * @param type - The type of saved object whose fields should be incremented + * @param id - The id of the document whose fields should be incremented + * @param counterFieldNames - An array of field names to increment + * @param options - {@link SavedObjectsIncrementCounterOptions} + * @returns The saved object after the specified fields were incremented */ async incrementCounter( type: string, id: string, - counterFieldName: string, + counterFieldNames: string[], options: SavedObjectsIncrementCounterOptions = {} ): Promise { if (typeof type !== 'string') { throw new Error('"type" argument must be a string'); } - if (typeof counterFieldName !== 'string') { - throw new Error('"counterFieldName" argument must be a string'); + const isArrayOfStrings = + Array.isArray(counterFieldNames) && + !counterFieldNames.some((field) => typeof field !== 'string'); + if (!isArrayOfStrings) { + throw new Error('"counterFieldNames" argument must be an array of strings'); } if (!this._allowedTypes.includes(type)) { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } - const { migrationVersion, refresh = DEFAULT_REFRESH_SETTING } = options; + const { migrationVersion, refresh = DEFAULT_REFRESH_SETTING, initialize = false } = options; const namespace = normalizeNamespace(options.namespace); const time = this._getCurrentTime(); @@ -1558,7 +1599,10 @@ export class SavedObjectsRepository { type, ...(savedObjectNamespace && { namespace: savedObjectNamespace }), ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), - attributes: { [counterFieldName]: 1 }, + attributes: counterFieldNames.reduce((acc, counterFieldName) => { + acc[counterFieldName] = initialize ? 0 : 1; + return acc; + }, {} as Record), migrationVersion, updated_at: time, }); @@ -1573,20 +1617,22 @@ export class SavedObjectsRepository { body: { script: { source: ` - if (ctx._source[params.type][params.counterFieldName] == null) { - ctx._source[params.type][params.counterFieldName] = params.count; - } - else { - ctx._source[params.type][params.counterFieldName] += params.count; + for (counterFieldName in params.counterFieldNames) { + if (ctx._source[params.type][counterFieldName] == null) { + ctx._source[params.type][counterFieldName] = params.count; + } + else { + ctx._source[params.type][counterFieldName] += params.count; + } } ctx._source.updated_at = params.time; `, lang: 'painless', params: { - count: 1, + count: initialize ? 0 : 1, time, type, - counterFieldName, + counterFieldNames, }, }, upsert: raw._source, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 8dddff07a0e4c..36a8d9a52fd52 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2369,6 +2369,7 @@ export interface SavedObjectsImportUnsupportedTypeError { // @public (undocumented) export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions { + initialize?: boolean; // (undocumented) migrationVersion?: SavedObjectsMigrationVersion; refresh?: MutatingOperationRefreshSetting; @@ -2447,7 +2448,7 @@ export class SavedObjectsRepository { // (undocumented) find(options: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; - incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise; + incrementCounter(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise; removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; } diff --git a/src/plugins/charts/README.md b/src/plugins/charts/README.md index 31727b7acb7a1..dae7b9695ed60 100644 --- a/src/plugins/charts/README.md +++ b/src/plugins/charts/README.md @@ -27,3 +27,7 @@ Truncated color mappings in `value`/`text` form ## Theme See Theme service [docs](public/services/theme/README.md) + +## Palettes + +See palette service [docs](public/services/palettes/README.md) diff --git a/src/plugins/charts/public/services/palettes/README.md b/src/plugins/charts/public/services/palettes/README.md new file mode 100644 index 0000000000000..3403d422682bd --- /dev/null +++ b/src/plugins/charts/public/services/palettes/README.md @@ -0,0 +1,33 @@ +# Palette Service + +The `palette` service offers a collection of palettes which implement a uniform interface for assigning colors to charts. The service provides methods for switching palettes +easily. It's used by the x-pack plugins `canvas` and `lens`. + +Each palette is allowed to store some state as well which has to be handled by the consumer. + +Palettes are integrated with the expression as well using the `system_palette` and `palette` functions. + +## Using the palette service + +To consume the palette service, use `charts.palettes.getPalettes` to lazily load the async bundle implementing existing palettes. This is recommended to be called in the renderer, not as part of the `setup` or `start` phases of a plugin. + +All palette definitions can be loaded using `paletteService.getAll()`. If the id of the palette is known, it can be fetched using `paleteService.get(id)`. + +One a palette is loaded, there are two ways to request colors - either by fetching a list of colors (`getColors`) or by specifying the chart object to be colored (`getColor`). If possible, using `getColor` is recommended because it allows the palette implementation to apply custom logic to coloring (e.g. lightening up colors or syncing colors) which has to be implemented by the consumer if `getColors` is used). + +### SeriesLayer + +If `getColor` is used, an array of `SeriesLayer` objects has to be passed in. These correspond with the current series in the chart a color has to be determined for. An array is necessary as some charts are constructed hierarchically (e.g. pie charts or treemaps). The array of objects represents the current series with all ancestors up to the corresponding root series. For each layer in the series hierarchy, the number of "sibling" series and the position of the current series has to be specified along with the name of the series. + +## Custom palette + +All palettes are stateless and define their own colors except for the `custom` palette which takes a state of the form +```ts +{ colors: string[]; gradient: boolean } +``` + +This state has to be passed into the `getColors` and `getColor` function to retrieve specific colors. + +## Registering new palettes + +Currently palettes can't be extended dynamically. diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index c99e4e4e06987..0d9e7e51b4a97 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -74,7 +74,7 @@ import { NavAction, SavedDashboardPanel } from '../types'; import { showOptionsPopover } from './top_nav/show_options_popover'; import { DashboardSaveModal, SaveOptions } from './top_nav/save_modal'; import { showCloneModal } from './top_nav/show_clone_modal'; -import { saveDashboard } from './lib'; +import { createSessionRestorationDataProvider, saveDashboard } from './lib'; import { DashboardStateManager } from './dashboard_state_manager'; import { createDashboardEditUrl, DashboardConstants } from '../dashboard_constants'; import { getTopNavConfig } from './top_nav/get_top_nav_config'; @@ -150,7 +150,7 @@ export class DashboardAppController { dashboardCapabilities, scopedHistory, embeddableCapabilities: { visualizeCapabilities, mapsCapabilities }, - data: { query: queryService, search: searchService }, + data, core: { notifications, overlays, @@ -168,6 +168,8 @@ export class DashboardAppController { navigation, savedObjectsTagging, }: DashboardAppControllerDependencies) { + const queryService = data.query; + const searchService = data.search; const filterManager = queryService.filterManager; const timefilter = queryService.timefilter.timefilter; const queryStringManager = queryService.queryString; @@ -262,6 +264,16 @@ export class DashboardAppController { $scope.showSaveQuery = dashboardCapabilities.saveQuery as boolean; + const landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`; + + const getDashTitle = () => + getDashboardTitle( + dashboardStateManager.getTitle(), + dashboardStateManager.getViewMode(), + dashboardStateManager.getIsDirty(timefilter), + dashboardStateManager.isNew() + ); + const getShouldShowEditHelp = () => !dashboardStateManager.getPanels().length && dashboardStateManager.getIsEditMode() && @@ -429,6 +441,15 @@ export class DashboardAppController { DashboardContainer >(DASHBOARD_CONTAINER_TYPE); + searchService.session.setSearchSessionInfoProvider( + createSessionRestorationDataProvider({ + data, + getDashboardTitle: () => getDashTitle(), + getDashboardId: () => dash.id, + getAppState: () => dashboardStateManager.getAppState(), + }) + ); + if (dashboardFactory) { const searchSessionIdFromURL = getSearchSessionIdFromURL(history); if (searchSessionIdFromURL) { @@ -552,16 +573,6 @@ export class DashboardAppController { filterManager.getFilters() ); - const landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`; - - const getDashTitle = () => - getDashboardTitle( - dashboardStateManager.getTitle(), - dashboardStateManager.getViewMode(), - dashboardStateManager.getIsDirty(timefilter), - dashboardStateManager.isNew() - ); - // Push breadcrumbs to new header navigation const updateBreadcrumbs = () => { chrome.setBreadcrumbs([ @@ -638,6 +649,13 @@ export class DashboardAppController { } }; + const searchServiceSessionRefreshSubscribtion = searchService.session.onRefresh$.subscribe( + () => { + lastReloadRequestTime = new Date().getTime(); + refreshDashboardContainer(); + } + ); + const updateStateFromSavedQuery = (savedQuery: SavedQuery) => { const allFilters = filterManager.getFilters(); dashboardStateManager.applyFilters(savedQuery.attributes.query, allFilters); @@ -1199,6 +1217,7 @@ export class DashboardAppController { if (dashboardContainer) { dashboardContainer.destroy(); } + searchServiceSessionRefreshSubscribtion.unsubscribe(); searchService.session.clear(); }); } diff --git a/src/plugins/dashboard/public/application/lib/index.ts b/src/plugins/dashboard/public/application/lib/index.ts index e9ebe73c3b34d..6741bbbc5d4b1 100644 --- a/src/plugins/dashboard/public/application/lib/index.ts +++ b/src/plugins/dashboard/public/application/lib/index.ts @@ -21,3 +21,4 @@ export { saveDashboard } from './save_dashboard'; export { getAppStateDefaults } from './get_app_state_defaults'; export { migrateAppState } from './migrate_app_state'; export { getDashboardIdFromUrl } from './url'; +export { createSessionRestorationDataProvider } from './session_restoration'; diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.ts b/src/plugins/dashboard/public/application/lib/session_restoration.ts new file mode 100644 index 0000000000000..f8ea8f8dcd76d --- /dev/null +++ b/src/plugins/dashboard/public/application/lib/session_restoration.ts @@ -0,0 +1,66 @@ +/* + * 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 { DASHBOARD_APP_URL_GENERATOR, DashboardUrlGeneratorState } from '../../url_generator'; +import { DataPublicPluginStart } from '../../../../data/public'; +import { DashboardAppState } from '../../types'; + +export function createSessionRestorationDataProvider(deps: { + data: DataPublicPluginStart; + getAppState: () => DashboardAppState; + getDashboardTitle: () => string; + getDashboardId: () => string; +}) { + return { + getName: async () => deps.getDashboardTitle(), + getUrlGeneratorData: async () => { + return { + urlGeneratorId: DASHBOARD_APP_URL_GENERATOR, + initialState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: false }), + restoreState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: true }), + }; + }, + }; +} + +function getUrlGeneratorState({ + data, + getAppState, + getDashboardId, + forceAbsoluteTime, // TODO: not implemented +}: { + data: DataPublicPluginStart; + getAppState: () => DashboardAppState; + getDashboardId: () => string; + forceAbsoluteTime: boolean; +}): DashboardUrlGeneratorState { + const appState = getAppState(); + return { + dashboardId: getDashboardId(), + timeRange: data.query.timefilter.timefilter.getTime(), + filters: data.query.filterManager.getFilters(), + query: data.query.queryString.formatQuery(appState.query), + savedQuery: appState.savedQuery, + useHash: false, + preserveSavedFilters: false, + viewMode: appState.viewMode, + panels: getDashboardId() ? undefined : appState.panels, + searchSessionId: data.search.session.getSessionId(), + }; +} diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts index 461caedc5cba7..0272e9d3ebdf7 100644 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ b/src/plugins/dashboard/public/url_generator.test.ts @@ -142,6 +142,39 @@ describe('dashboard url generator', () => { ); }); + test('savedQuery', async () => { + const generator = createDashboardUrlGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) + ); + const url = await generator.createUrl!({ + savedQuery: '__savedQueryId__', + }); + expect(url).toMatchInlineSnapshot( + `"xyz/app/dashboards#/create?_a=(savedQuery:__savedQueryId__)&_g=()"` + ); + expect(url).toContain('__savedQueryId__'); + }); + + test('panels', async () => { + const generator = createDashboardUrlGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) + ); + const url = await generator.createUrl!({ + panels: [{ fakePanelContent: 'fakePanelContent' } as any], + }); + expect(url).toMatchInlineSnapshot( + `"xyz/app/dashboards#/create?_a=(panels:!((fakePanelContent:fakePanelContent)))&_g=()"` + ); + }); + test('if no useHash setting is given, uses the one was start services', async () => { const generator = createDashboardUrlGenerator(() => Promise.resolve({ diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts index b23b26e4022dd..182020d032e4e 100644 --- a/src/plugins/dashboard/public/url_generator.ts +++ b/src/plugins/dashboard/public/url_generator.ts @@ -30,6 +30,7 @@ import { UrlGeneratorsDefinition } from '../../share/public'; import { SavedObjectLoader } from '../../saved_objects/public'; import { ViewMode } from '../../embeddable/public'; import { DashboardConstants } from './dashboard_constants'; +import { SavedDashboardPanel } from '../common/types'; export const STATE_STORAGE_KEY = '_a'; export const GLOBAL_STATE_STORAGE_KEY = '_g'; @@ -86,6 +87,16 @@ export interface DashboardUrlGeneratorState { * (Background search) */ searchSessionId?: string; + + /** + * List of dashboard panels + */ + panels?: SavedDashboardPanel[]; + + /** + * Saved query ID + */ + savedQuery?: string; } export const createDashboardUrlGenerator = ( @@ -137,6 +148,8 @@ export const createDashboardUrlGenerator = ( query: state.query, filters: filters?.filter((f) => !esFilters.isFilterPinned(f)), viewMode: state.viewMode, + panels: state.panels, + savedQuery: state.savedQuery, }), { useHash }, `${appBasePath}#/${hash}` diff --git a/src/plugins/data/common/index_patterns/utils.ts b/src/plugins/data/common/index_patterns/utils.ts index d9e1cfa0d952a..b7e1f28d5d60f 100644 --- a/src/plugins/data/common/index_patterns/utils.ts +++ b/src/plugins/data/common/index_patterns/utils.ts @@ -17,8 +17,8 @@ * under the License. */ -import { find } from 'lodash'; -import { SavedObjectsClientCommon, SavedObject } from '..'; +import type { IndexPatternSavedObjectAttrs } from './index_patterns'; +import type { SavedObjectsClientCommon } from '../types'; /** * Returns an object matching a given title @@ -27,24 +27,16 @@ import { SavedObjectsClientCommon, SavedObject } from '..'; * @param title {string} * @returns {Promise} */ -export async function findByTitle( - client: SavedObjectsClientCommon, - title: string -): Promise | void> { - if (!title) { - return Promise.resolve(); - } - - const savedObjects = await client.find({ - type: 'index-pattern', - perPage: 10, - search: `"${title}"`, - searchFields: ['title'], - fields: ['title'], - }); +export async function findByTitle(client: SavedObjectsClientCommon, title: string) { + if (title) { + const savedObjects = await client.find({ + type: 'index-pattern', + perPage: 10, + search: `"${title}"`, + searchFields: ['title'], + fields: ['title'], + }); - return find( - savedObjects, - (obj: SavedObject) => obj.attributes.title.toLowerCase() === title.toLowerCase() - ); + return savedObjects.find((obj) => obj.attributes.title.toLowerCase() === title.toLowerCase()); + } } diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 4f4a593764b1e..bf6fe11f746f9 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -55,7 +55,8 @@ export interface AggTypeConfig< aggConfig: TAggConfig, searchSource: ISearchSource, inspectorRequestAdapter?: RequestAdapter, - abortSignal?: AbortSignal + abortSignal?: AbortSignal, + searchSessionId?: string ) => Promise; getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat; getValue?: (agg: TAggConfig, bucket: any) => any; @@ -182,6 +183,8 @@ export class AggType< * @param searchSourceAggs - SearchSource aggregation configuration * @param resp - Response to the main request * @param nestedSearchSource - the new SearchSource that will be used to make post flight request + * @param abortSignal - `AbortSignal` to abort the request + * @param searchSessionId - searchSessionId to be used for grouping requests into a single search session * @return {Promise} */ postFlightRequest: ( @@ -190,7 +193,8 @@ export class AggType< aggConfig: TAggConfig, searchSource: ISearchSource, inspectorRequestAdapter?: RequestAdapter, - abortSignal?: AbortSignal + abortSignal?: AbortSignal, + searchSessionId?: string ) => Promise; /** * Get the serialized format for the values produced by this agg type, diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts index ac65e7fa813b3..7071d9c1dc9c4 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.ts @@ -102,7 +102,8 @@ export const getTermsBucketAgg = () => aggConfig, searchSource, inspectorRequestAdapter, - abortSignal + abortSignal, + searchSessionId ) => { if (!resp.aggregations) return resp; const nestedSearchSource = searchSource.createChild(); @@ -124,6 +125,7 @@ export const getTermsBucketAgg = () => 'This request counts the number of documents that fall ' + 'outside the criterion of the data buckets.', }), + searchSessionId, } ); nestedSearchSource.getSearchRequestBody().then((body) => { @@ -132,7 +134,10 @@ export const getTermsBucketAgg = () => request.stats(getRequestInspectorStats(nestedSearchSource)); } - const response = await nestedSearchSource.fetch({ abortSignal }); + const response = await nestedSearchSource.fetch({ + abortSignal, + sessionId: searchSessionId, + }); if (request) { request .stats(getResponseInspectorStats(response, nestedSearchSource)) diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/common/search/session/types.ts index d1ab22057695a..50ca3ca390ece 100644 --- a/src/plugins/data/common/search/session/types.ts +++ b/src/plugins/data/common/search/session/types.ts @@ -17,82 +17,19 @@ * under the License. */ -import { Observable } from 'rxjs'; -import type { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; - -export interface ISessionService { - /** - * Returns the active session ID - * @returns The active session ID - */ - getSessionId: () => string | undefined; - /** - * Returns the observable that emits an update every time the session ID changes - * @returns `Observable` - */ - getSession$: () => Observable; - - /** - * Whether the active session is already saved (i.e. sent to background) - */ - isStored: () => boolean; - - /** - * Whether the active session is restored (i.e. reusing previous search IDs) - */ - isRestore: () => boolean; - - /** - * Starts a new session - */ - start: () => string; - - /** - * Restores existing session - */ - restore: (sessionId: string) => Promise>; - - /** - * Clears the active session. - */ - clear: () => void; - - /** - * Saves a session - */ - save: (name: string, url: string) => Promise>; - - /** - * Gets a saved session - */ - get: (sessionId: string) => Promise>; - - /** - * Gets a list of saved sessions - */ - find: ( - options: SearchSessionFindOptions - ) => Promise>; - +export interface BackgroundSessionSavedObjectAttributes { /** - * Updates a session + * User-facing session name to be displayed in session management */ - update: ( - sessionId: string, - attributes: Partial - ) => Promise; - + name: string; /** - * Deletes a session + * App that created the session. e.g 'discover' */ - delete: (sessionId: string) => Promise; -} - -export interface BackgroundSessionSavedObjectAttributes { - name: string; + appId: string; created: string; expires: string; status: string; + urlGeneratorId: string; initialState: Record; restoreState: Record; idMapping: Record; diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index 3e4d08c8faa1b..06b083e0ff3aa 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -6,7 +6,8 @@ "requiredPlugins": [ "bfetch", "expressions", - "uiActions" + "uiActions", + "share" ], "optionalPlugins": ["usageCollection"], "extraPublicDirs": ["common"], diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index e0b0c5a0ea980..1c07b4b99e4c0 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -385,6 +385,7 @@ export { SearchRequest, SearchSourceFields, SortDirection, + SessionState, // expression functions and types EsdslExpressionFunctionDefinition, EsRawResponseExpressionTypeDefinition, @@ -395,7 +396,12 @@ export { PainlessError, } from './search'; -export type { SearchSource, ISessionService } from './search'; +export type { + SearchSource, + ISessionService, + SearchSessionInfoProvider, + ISessionsClient, +} from './search'; export { ISearchOptions, isErrorResponse, isCompleteResponse, isPartialResponse } from '../common'; diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 1b83eb569b1a1..67c1ff7e09dd7 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -64,6 +64,7 @@ const createStartContract = (): Start => { SearchBar: jest.fn().mockReturnValue(null), }, indexPatterns: ({ + find: jest.fn((search) => [{ id: search, title: search }]), createField: jest.fn(() => {}), createFieldList: jest.fn(() => []), ensureDefaultIndexPattern: jest.fn(), diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 8ceb91269adbe..ad1861cecea0b 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -40,6 +40,7 @@ import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_formats/utils'; import { History } from 'history'; import { Href } from 'history'; +import { HttpSetup } from 'kibana/public'; import { IconType } from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n/react'; import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; @@ -62,7 +63,9 @@ import { PackageInfo } from '@kbn/config'; import { Path } from 'history'; import { Plugin as Plugin_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; +import { PluginInitializerContext as PluginInitializerContext_3 } from 'kibana/public'; import { PopoverAnchorPosition } from '@elastic/eui'; +import { PublicContract } from '@kbn/utility-types'; import { PublicMethodsOf } from '@kbn/utility-types'; import { PublicUiSettingsParams } from 'src/core/server/types'; import React from 'react'; @@ -82,6 +85,7 @@ import { SavedObjectsFindResponse } from 'kibana/server'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; +import { StartServicesAccessor } from 'kibana/public'; import { ToastInputFields } from 'src/core/public/notifications'; import { ToastsSetup } from 'kibana/public'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; @@ -1478,6 +1482,7 @@ export interface ISearchSetup { // (undocumented) aggs: AggsSetup; session: ISessionService; + sessionsClient: ISessionsClient; // Warning: (ae-forgotten-export) The symbol "SearchUsageCollector" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1493,6 +1498,7 @@ export interface ISearchStart { search: ISearchGeneric; searchSource: ISearchStartSearchSource; session: ISessionService; + sessionsClient: ISessionsClient; // (undocumented) showError: (e: Error) => void; } @@ -1508,25 +1514,17 @@ export interface ISearchStartSearchSource { // @public (undocumented) export const isErrorResponse: (response?: IKibanaSearchResponse | undefined) => boolean | undefined; +// Warning: (ae-forgotten-export) The symbol "SessionsClient" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "ISessionsClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ISessionsClient = PublicContract; + +// Warning: (ae-forgotten-export) The symbol "SessionService" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ISessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface ISessionService { - clear: () => void; - delete: (sessionId: string) => Promise; - // Warning: (ae-forgotten-export) The symbol "SearchSessionFindOptions" needs to be exported by the entry point index.d.ts - find: (options: SearchSessionFindOptions) => Promise>; - get: (sessionId: string) => Promise>; - getSession$: () => Observable; - getSessionId: () => string | undefined; - isRestore: () => boolean; - isStored: () => boolean; - // Warning: (ae-forgotten-export) The symbol "BackgroundSessionSavedObjectAttributes" needs to be exported by the entry point index.d.ts - restore: (sessionId: string) => Promise>; - save: (name: string, url: string) => Promise>; - start: () => string; - update: (sessionId: string, attributes: Partial) => Promise; -} +export type ISessionService = PublicContract; // Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2107,6 +2105,7 @@ export class SearchInterceptor { timeoutSignal: AbortSignal; combinedSignal: AbortSignal; cleanup: () => void; + abort: () => void; }; // (undocumented) showError(e: Error): void; @@ -2135,6 +2134,20 @@ export interface SearchInterceptorDeps { // @internal export type SearchRequest = Record; +// Warning: (ae-forgotten-export) The symbol "UrlGeneratorId" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "SearchSessionInfoProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface SearchSessionInfoProvider { + getName: () => Promise; + // (undocumented) + getUrlGeneratorData: () => Promise<{ + urlGeneratorId: ID; + initialState: UrlGeneratorStateMapping[ID]['State']; + restoreState: UrlGeneratorStateMapping[ID]['State']; + }>; +} + // @public (undocumented) export class SearchSource { // Warning: (ae-forgotten-export) The symbol "SearchSourceDependencies" needs to be exported by the entry point index.d.ts @@ -2240,6 +2253,17 @@ export class SearchTimeoutError extends KbnError { mode: TimeoutErrorMode; } +// @public +export enum SessionState { + BackgroundCompleted = "backgroundCompleted", + BackgroundLoading = "backgroundLoading", + Canceled = "canceled", + Completed = "completed", + Loading = "loading", + None = "none", + Restored = "restored" +} + // Warning: (ae-missing-release-tag) "SortDirection" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2415,22 +2439,23 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:246:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:429:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:432:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:436:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/search/session/session_service.ts:46:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/search/expressions/esaggs/request_handler.ts b/src/plugins/data/public/search/expressions/esaggs/request_handler.ts index 93b5705b821c0..7a27d65267149 100644 --- a/src/plugins/data/public/search/expressions/esaggs/request_handler.ts +++ b/src/plugins/data/public/search/expressions/esaggs/request_handler.ts @@ -182,7 +182,8 @@ export const handleRequest = async ({ agg, requestSearchSource, inspectorAdapters.requests, - abortSignal + abortSignal, + searchSessionId ); } } diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index f6bd46c17192c..2a767d1bf7c0d 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -40,9 +40,15 @@ export { SearchSourceDependencies, SearchSourceFields, SortDirection, - ISessionService, } from '../../common/search'; - +export { + SessionService, + ISessionService, + SearchSessionInfoProvider, + SessionState, + SessionsClient, + ISessionsClient, +} from './session'; export { getEsPreference } from './es_search'; export { SearchInterceptor, SearchInterceptorDeps } from './search_interceptor'; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index 836ddb618e746..b799c661051fa 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -20,13 +20,14 @@ import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; import { searchSourceMock } from './search_source/mocks'; import { ISearchSetup, ISearchStart } from './types'; -import { getSessionServiceMock } from '../../common/mocks'; +import { getSessionsClientMock, getSessionServiceMock } from './session/mocks'; function createSetupContract(): jest.Mocked { return { aggs: searchAggsSetupMock(), __enhance: jest.fn(), session: getSessionServiceMock(), + sessionsClient: getSessionsClientMock(), }; } @@ -36,6 +37,7 @@ function createStartContract(): jest.Mocked { search: jest.fn(), showError: jest.fn(), session: getSessionServiceMock(), + sessionsClient: getSessionsClientMock(), searchSource: searchSourceMock.createStartContract(), }; } diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts index 6dc52d7016797..947dac1b32640 100644 --- a/src/plugins/data/public/search/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -24,7 +24,7 @@ import { SearchInterceptor } from './search_interceptor'; import { AbortError } from '../../../kibana_utils/public'; import { SearchTimeoutError, PainlessError, TimeoutErrorMode } from './errors'; import { searchServiceMock } from './mocks'; -import { ISearchStart } from '.'; +import { ISearchStart, ISessionService } from '.'; import { bfetchPluginMock } from '../../../bfetch/public/mocks'; import { BfetchPublicSetup } from 'src/plugins/bfetch/public'; @@ -104,7 +104,99 @@ describe('SearchInterceptor', () => { params: {}, }; const response = searchInterceptor.search(mockRequest); - expect(response.toPromise()).resolves.toBe(mockResponse); + await expect(response.toPromise()).resolves.toBe(mockResponse); + }); + + describe('Search session', () => { + const setup = ({ + isRestore = false, + isStored = false, + sessionId, + }: { + isRestore?: boolean; + isStored?: boolean; + sessionId?: string; + }) => { + const sessionServiceMock = searchMock.session as jest.Mocked; + sessionServiceMock.getSessionId.mockImplementation(() => sessionId); + sessionServiceMock.isRestore.mockImplementation(() => isRestore); + sessionServiceMock.isStored.mockImplementation(() => isStored); + fetchMock.mockResolvedValue({ result: 200 }); + }; + + const mockRequest: IEsSearchRequest = { + params: {}, + }; + + afterEach(() => { + const sessionServiceMock = searchMock.session as jest.Mocked; + sessionServiceMock.getSessionId.mockReset(); + sessionServiceMock.isRestore.mockReset(); + sessionServiceMock.isStored.mockReset(); + fetchMock.mockReset(); + }); + + test('infers isRestore from session service state', async () => { + const sessionId = 'sid'; + setup({ + isRestore: true, + sessionId, + }); + + await searchInterceptor.search(mockRequest, { sessionId }).toPromise(); + expect(fetchMock.mock.calls[0][0]).toEqual( + expect.objectContaining({ + options: { sessionId: 'sid', isStored: false, isRestore: true }, + }) + ); + }); + + test('infers isStored from session service state', async () => { + const sessionId = 'sid'; + setup({ + isStored: true, + sessionId, + }); + + await searchInterceptor.search(mockRequest, { sessionId }).toPromise(); + expect(fetchMock.mock.calls[0][0]).toEqual( + expect.objectContaining({ + options: { sessionId: 'sid', isStored: true, isRestore: false }, + }) + ); + }); + + test('skips isRestore & isStore in case not a current session Id', async () => { + setup({ + isStored: true, + isRestore: true, + sessionId: 'session id', + }); + + await searchInterceptor + .search(mockRequest, { sessionId: 'different session id' }) + .toPromise(); + expect(fetchMock.mock.calls[0][0]).toEqual( + expect.objectContaining({ + options: { sessionId: 'different session id', isStored: false, isRestore: false }, + }) + ); + }); + + test('skips isRestore & isStore in case no session Id', async () => { + setup({ + isStored: true, + isRestore: true, + sessionId: undefined, + }); + + await searchInterceptor.search(mockRequest, { sessionId: 'sessionId' }).toPromise(); + expect(fetchMock.mock.calls[0][0]).toEqual( + expect.objectContaining({ + options: { sessionId: 'sessionId', isStored: false, isRestore: false }, + }) + ); + }); }); describe('Should throw typed errors', () => { diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 055b3a71705bf..8548a2a9f2b2a 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -24,12 +24,7 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { BatchedFunc, BfetchPublicSetup } from 'src/plugins/bfetch/public'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, - ISearchOptions, - ISessionService, -} from '../../common'; +import { IKibanaSearchRequest, IKibanaSearchResponse, ISearchOptions } from '../../common'; import { SearchUsageCollector } from './collectors'; import { SearchTimeoutError, @@ -42,6 +37,7 @@ import { } from './errors'; import { toMountPoint } from '../../../kibana_react/public'; import { AbortError, getCombinedAbortSignal } from '../../../kibana_utils/public'; +import { ISessionService } from './session'; export interface SearchInterceptorDeps { bfetch: BfetchPublicSetup; @@ -133,10 +129,18 @@ export class SearchInterceptor { options?: ISearchOptions ): Promise { const { abortSignal, ...requestOptions } = options || {}; + + const isCurrentSession = + options?.sessionId && this.deps.session.getSessionId() === options.sessionId; + return this.batchedFetch( { request, - options: requestOptions, + options: { + ...requestOptions, + isStored: isCurrentSession ? this.deps.session.isStored() : false, + isRestore: isCurrentSession ? this.deps.session.isRestore() : false, + }, }, abortSignal ); @@ -160,13 +164,18 @@ export class SearchInterceptor { timeoutController.abort(); }); + const selfAbortController = new AbortController(); + // Get a combined `AbortSignal` that will be aborted whenever the first of the following occurs: // 1. The user manually aborts (via `cancelPending`) // 2. The request times out - // 3. The passed-in signal aborts (e.g. when re-fetching, or whenever the app determines) + // 3. abort() is called on `selfAbortController`. This is used by session service to abort all pending searches that it tracks + // in the current session + // 4. The passed-in signal aborts (e.g. when re-fetching, or whenever the app determines) const signals = [ this.abortController.signal, timeoutSignal, + selfAbortController.signal, ...(abortSignal ? [abortSignal] : []), ]; @@ -184,6 +193,9 @@ export class SearchInterceptor { timeoutSignal, combinedSignal, cleanup, + abort: () => { + selfAbortController.abort(); + }, }; } diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index 3179da4d03a1a..d617010d13011 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -49,6 +49,8 @@ describe('Search service', () => { expect(setup).toHaveProperty('aggs'); expect(setup).toHaveProperty('usageCollector'); expect(setup).toHaveProperty('__enhance'); + expect(setup).toHaveProperty('sessionsClient'); + expect(setup).toHaveProperty('session'); }); }); @@ -61,6 +63,8 @@ describe('Search service', () => { expect(start).toHaveProperty('aggs'); expect(start).toHaveProperty('search'); expect(start).toHaveProperty('searchSource'); + expect(start).toHaveProperty('sessionsClient'); + expect(start).toHaveProperty('session'); }); }); }); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index b76b5846d3d93..60d2dfdf866cf 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -28,7 +28,6 @@ import { kibanaContext, kibanaContextFunction, ISearchGeneric, - ISessionService, SearchSourceDependencies, SearchSourceService, } from '../../common/search'; @@ -40,7 +39,7 @@ import { SearchUsageCollector, createUsageCollector } from './collectors'; import { UsageCollectionSetup } from '../../../usage_collection/public'; import { esdsl, esRawResponse } from './expressions'; import { ExpressionsSetup } from '../../../expressions/public'; -import { SessionService } from './session_service'; +import { ISessionsClient, ISessionService, SessionsClient, SessionService } from './session'; import { ConfigSchema } from '../../config'; import { SHARD_DELAY_AGG_NAME, @@ -67,6 +66,7 @@ export class SearchService implements Plugin { private searchInterceptor!: ISearchInterceptor; private usageCollector?: SearchUsageCollector; private sessionService!: ISessionService; + private sessionsClient!: ISessionsClient; constructor(private initializerContext: PluginInitializerContext) {} @@ -76,7 +76,12 @@ export class SearchService implements Plugin { ): ISearchSetup { this.usageCollector = createUsageCollector(getStartServices, usageCollection); - this.sessionService = new SessionService(this.initializerContext, getStartServices); + this.sessionsClient = new SessionsClient({ http }); + this.sessionService = new SessionService( + this.initializerContext, + getStartServices, + this.sessionsClient + ); /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. @@ -115,6 +120,7 @@ export class SearchService implements Plugin { this.searchInterceptor = enhancements.searchInterceptor; }, session: this.sessionService, + sessionsClient: this.sessionsClient, }; } @@ -146,6 +152,7 @@ export class SearchService implements Plugin { this.searchInterceptor.showError(e); }, session: this.sessionService, + sessionsClient: this.sessionsClient, searchSource: this.searchSourceService.start(indexPatterns, searchSourceDependencies), }; } diff --git a/src/plugins/data/public/search/session/index.ts b/src/plugins/data/public/search/session/index.ts new file mode 100644 index 0000000000000..ee0121aacad5e --- /dev/null +++ b/src/plugins/data/public/search/session/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { SessionService, ISessionService, SearchSessionInfoProvider } from './session_service'; +export { SessionState } from './session_state'; +export { SessionsClient, ISessionsClient } from './sessions_client'; diff --git a/src/plugins/data/common/search/session/mocks.ts b/src/plugins/data/public/search/session/mocks.ts similarity index 67% rename from src/plugins/data/common/search/session/mocks.ts rename to src/plugins/data/public/search/session/mocks.ts index 4604e15e4e93b..0ff99b3080365 100644 --- a/src/plugins/data/common/search/session/mocks.ts +++ b/src/plugins/data/public/search/session/mocks.ts @@ -17,8 +17,20 @@ * under the License. */ -import { BehaviorSubject } from 'rxjs'; -import { ISessionService } from './types'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { ISessionsClient } from './sessions_client'; +import { ISessionService } from './session_service'; +import { SessionState } from './session_state'; + +export function getSessionsClientMock(): jest.Mocked { + return { + get: jest.fn(), + create: jest.fn(), + find: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }; +} export function getSessionServiceMock(): jest.Mocked { return { @@ -27,12 +39,15 @@ export function getSessionServiceMock(): jest.Mocked { restore: jest.fn(), getSessionId: jest.fn(), getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()), + state$: new BehaviorSubject(SessionState.None).asObservable(), + setSearchSessionInfoProvider: jest.fn(), + trackSearch: jest.fn((searchDescriptor) => () => {}), + destroy: jest.fn(), + onRefresh$: new Subject(), + refresh: jest.fn(), + cancel: jest.fn(), isStored: jest.fn(), isRestore: jest.fn(), save: jest.fn(), - get: jest.fn(), - find: jest.fn(), - update: jest.fn(), - delete: jest.fn(), }; } diff --git a/src/plugins/data/public/search/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts similarity index 53% rename from src/plugins/data/public/search/session_service.test.ts rename to src/plugins/data/public/search/session/session_service.test.ts index bcfd06944d983..83c3185ead63e 100644 --- a/src/plugins/data/public/search/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -17,20 +17,27 @@ * under the License. */ -import { SessionService } from './session_service'; -import { ISessionService } from '../../common'; -import { coreMock } from '../../../../core/public/mocks'; +import { SessionService, ISessionService } from './session_service'; +import { coreMock } from '../../../../../core/public/mocks'; import { take, toArray } from 'rxjs/operators'; +import { getSessionsClientMock } from './mocks'; +import { BehaviorSubject } from 'rxjs'; +import { SessionState } from './session_state'; describe('Session service', () => { let sessionService: ISessionService; + let state$: BehaviorSubject; beforeEach(() => { const initializerContext = coreMock.createPluginInitializerContext(); sessionService = new SessionService( initializerContext, - coreMock.createSetup().getStartServices + coreMock.createSetup().getStartServices, + getSessionsClientMock(), + { freezeState: false } // needed to use mocks inside state container ); + state$ = new BehaviorSubject(SessionState.None); + sessionService.state$.subscribe(state$); }); describe('Session management', () => { @@ -55,5 +62,35 @@ describe('Session service', () => { expect(await emittedValues).toEqual(['1', '2', undefined]); }); + + it('Tracks searches for current session', () => { + expect(() => sessionService.trackSearch({ abort: () => {} })).toThrowError(); + expect(state$.getValue()).toBe(SessionState.None); + + sessionService.start(); + const untrack1 = sessionService.trackSearch({ abort: () => {} }); + expect(state$.getValue()).toBe(SessionState.Loading); + const untrack2 = sessionService.trackSearch({ abort: () => {} }); + expect(state$.getValue()).toBe(SessionState.Loading); + untrack1(); + expect(state$.getValue()).toBe(SessionState.Loading); + untrack2(); + expect(state$.getValue()).toBe(SessionState.Completed); + }); + + it('Cancels all tracked searches within current session', async () => { + const abort = jest.fn(); + + sessionService.start(); + sessionService.trackSearch({ abort }); + sessionService.trackSearch({ abort }); + sessionService.trackSearch({ abort }); + const untrack = sessionService.trackSearch({ abort }); + + untrack(); + await sessionService.cancel(); + + expect(abort).toBeCalledTimes(3); + }); }); }); diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts new file mode 100644 index 0000000000000..ef0b36a33be52 --- /dev/null +++ b/src/plugins/data/public/search/session/session_service.ts @@ -0,0 +1,242 @@ +/* + * 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 { PublicContract } from '@kbn/utility-types'; +import { distinctUntilChanged, map, startWith } from 'rxjs/operators'; +import { Observable, Subject, Subscription } from 'rxjs'; +import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; +import { UrlGeneratorId, UrlGeneratorStateMapping } from '../../../../share/public/'; +import { ConfigSchema } from '../../../config'; +import { createSessionStateContainer, SessionState, SessionStateContainer } from './session_state'; +import { ISessionsClient } from './sessions_client'; + +export type ISessionService = PublicContract; + +export interface TrackSearchDescriptor { + abort: () => void; +} + +/** + * Provide info about current search session to be stored in backgroundSearch saved object + */ +export interface SearchSessionInfoProvider { + /** + * User-facing name of the session. + * e.g. will be displayed in background sessions management list + */ + getName: () => Promise; + getUrlGeneratorData: () => Promise<{ + urlGeneratorId: ID; + initialState: UrlGeneratorStateMapping[ID]['State']; + restoreState: UrlGeneratorStateMapping[ID]['State']; + }>; +} + +/** + * Responsible for tracking a current search session. Supports only a single session at a time. + */ +export class SessionService { + public readonly state$: Observable; + private readonly state: SessionStateContainer; + + private searchSessionInfoProvider?: SearchSessionInfoProvider; + private appChangeSubscription$?: Subscription; + private curApp?: string; + + constructor( + initializerContext: PluginInitializerContext, + getStartServices: StartServicesAccessor, + private readonly sessionsClient: ISessionsClient, + { freezeState = true }: { freezeState: boolean } = { freezeState: true } + ) { + const { stateContainer, sessionState$ } = createSessionStateContainer({ + freeze: freezeState, + }); + this.state$ = sessionState$; + this.state = stateContainer; + + getStartServices().then(([coreStart]) => { + // Apps required to clean up their sessions before unmounting + // Make sure that apps don't leave sessions open. + this.appChangeSubscription$ = coreStart.application.currentAppId$.subscribe((appName) => { + if (this.state.get().sessionId) { + const message = `Application '${this.curApp}' had an open session while navigating`; + if (initializerContext.env.mode.dev) { + // TODO: This setTimeout is necessary due to a race condition while navigating. + setTimeout(() => { + coreStart.fatalErrors.add(message); + }, 100); + } else { + // eslint-disable-next-line no-console + console.warn(message); + this.clear(); + } + } + this.curApp = appName; + }); + }); + } + + /** + * Set a provider of info about current session + * This will be used for creating a background session saved object + * @param searchSessionInfoProvider + */ + public setSearchSessionInfoProvider( + searchSessionInfoProvider: SearchSessionInfoProvider | undefined + ) { + this.searchSessionInfoProvider = searchSessionInfoProvider; + } + + /** + * Used to track pending searches within current session + * + * @param searchDescriptor - uniq object that will be used to untrack the search + * @returns untrack function + */ + public trackSearch(searchDescriptor: TrackSearchDescriptor): () => void { + this.state.transitions.trackSearch(searchDescriptor); + return () => { + this.state.transitions.unTrackSearch(searchDescriptor); + }; + } + + public destroy() { + if (this.appChangeSubscription$) { + this.appChangeSubscription$.unsubscribe(); + } + this.clear(); + } + + /** + * Get current session id + */ + public getSessionId() { + return this.state.get().sessionId; + } + + /** + * Get observable for current session id + */ + public getSession$() { + return this.state.state$.pipe( + startWith(this.state.get()), + map((s) => s.sessionId), + distinctUntilChanged() + ); + } + + /** + * Is current session already saved as SO (send to background) + */ + public isStored() { + return this.state.get().isStored; + } + + /** + * Is restoring the older saved searches + */ + public isRestore() { + return this.state.get().isRestore; + } + + /** + * Start a new search session + * @returns sessionId + */ + public start() { + this.state.transitions.start(); + return this.getSessionId()!; + } + + /** + * Restore previously saved search session + * @param sessionId + */ + public restore(sessionId: string) { + this.state.transitions.restore(sessionId); + } + + /** + * Cleans up current state + */ + public clear() { + this.state.transitions.clear(); + this.setSearchSessionInfoProvider(undefined); + } + + private refresh$ = new Subject(); + /** + * Observable emits when search result refresh was requested + * For example, search to background UI could have it's own "refresh" button + * Application would use this observable to handle user interaction on that button + */ + public onRefresh$ = this.refresh$.asObservable(); + + /** + * Request a search results refresh + */ + public refresh() { + this.refresh$.next(); + } + + /** + * Request a cancellation of on-going search requests within current session + */ + public async cancel(): Promise { + const isStoredSession = this.state.get().isStored; + this.state.get().pendingSearches.forEach((s) => { + s.abort(); + }); + this.state.transitions.cancel(); + if (isStoredSession) { + await this.sessionsClient.delete(this.state.get().sessionId!); + } + } + + /** + * Save current session as SO to get back to results later + * (Send to background) + */ + public async save(): Promise { + const sessionId = this.getSessionId(); + if (!sessionId) throw new Error('No current session'); + if (!this.curApp) throw new Error('No current app id'); + const currentSessionInfoProvider = this.searchSessionInfoProvider; + if (!currentSessionInfoProvider) throw new Error('No info provider for current session'); + const [name, { initialState, restoreState, urlGeneratorId }] = await Promise.all([ + currentSessionInfoProvider.getName(), + currentSessionInfoProvider.getUrlGeneratorData(), + ]); + + await this.sessionsClient.create({ + name, + appId: this.curApp, + restoreState: (restoreState as unknown) as Record, + initialState: (initialState as unknown) as Record, + urlGeneratorId, + sessionId, + }); + + // if we are still interested in this result + if (this.getSessionId() === sessionId) { + this.state.transitions.store(); + } + } +} diff --git a/src/plugins/data/public/search/session/session_state.test.ts b/src/plugins/data/public/search/session/session_state.test.ts new file mode 100644 index 0000000000000..5f709c75bb5d2 --- /dev/null +++ b/src/plugins/data/public/search/session/session_state.test.ts @@ -0,0 +1,124 @@ +/* + * 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 { createSessionStateContainer, SessionState } from './session_state'; + +describe('Session state container', () => { + const { stateContainer: state } = createSessionStateContainer(); + + afterEach(() => { + state.transitions.clear(); + }); + + describe('transitions', () => { + test('start', () => { + state.transitions.start(); + expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.get().sessionId).not.toBeUndefined(); + }); + + test('track', () => { + expect(() => state.transitions.trackSearch({})).toThrowError(); + + state.transitions.start(); + state.transitions.trackSearch({}); + + expect(state.selectors.getState()).toBe(SessionState.Loading); + }); + + test('untrack', () => { + state.transitions.start(); + const search = {}; + state.transitions.trackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.Loading); + state.transitions.unTrackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.Completed); + }); + + test('clear', () => { + state.transitions.start(); + state.transitions.clear(); + expect(state.selectors.getState()).toBe(SessionState.None); + expect(state.get().sessionId).toBeUndefined(); + }); + + test('cancel', () => { + expect(() => state.transitions.cancel()).toThrowError(); + + state.transitions.start(); + const search = {}; + state.transitions.trackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.Loading); + state.transitions.cancel(); + expect(state.selectors.getState()).toBe(SessionState.Canceled); + state.transitions.clear(); + expect(state.selectors.getState()).toBe(SessionState.None); + }); + + test('store -> completed', () => { + expect(() => state.transitions.store()).toThrowError(); + + state.transitions.start(); + const search = {}; + state.transitions.trackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.Loading); + state.transitions.store(); + expect(state.selectors.getState()).toBe(SessionState.BackgroundLoading); + state.transitions.unTrackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.BackgroundCompleted); + state.transitions.clear(); + expect(state.selectors.getState()).toBe(SessionState.None); + }); + test('store -> cancel', () => { + state.transitions.start(); + const search = {}; + state.transitions.trackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.Loading); + state.transitions.store(); + expect(state.selectors.getState()).toBe(SessionState.BackgroundLoading); + state.transitions.cancel(); + expect(state.selectors.getState()).toBe(SessionState.Canceled); + + state.transitions.trackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.Canceled); + + state.transitions.start(); + expect(state.selectors.getState()).toBe(SessionState.None); + }); + + test('restore', () => { + const id = 'id'; + state.transitions.restore(id); + expect(state.selectors.getState()).toBe(SessionState.None); + const search = {}; + state.transitions.trackSearch(search); + expect(state.selectors.getState()).toBe(SessionState.BackgroundLoading); + state.transitions.unTrackSearch(search); + + expect(state.selectors.getState()).toBe(SessionState.Restored); + expect(() => state.transitions.store()).toThrowError(); + expect(state.selectors.getState()).toBe(SessionState.Restored); + expect(() => state.transitions.cancel()).toThrowError(); + expect(state.selectors.getState()).toBe(SessionState.Restored); + + state.transitions.start(); + expect(state.selectors.getState()).toBe(SessionState.None); + }); + }); +}); diff --git a/src/plugins/data/public/search/session/session_state.ts b/src/plugins/data/public/search/session/session_state.ts new file mode 100644 index 0000000000000..087417263e5bf --- /dev/null +++ b/src/plugins/data/public/search/session/session_state.ts @@ -0,0 +1,234 @@ +/* + * 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 uuid from 'uuid'; +import { Observable } from 'rxjs'; +import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'; +import { createStateContainer, StateContainer } from '../../../../kibana_utils/public'; + +/** + * Possible state that current session can be in + * + * @public + */ +export enum SessionState { + /** + * Session is not active, e.g. didn't start + */ + None = 'none', + + /** + * Pending search request has not been sent to the background yet + */ + Loading = 'loading', + + /** + * No action was taken and the page completed loading without background session creation. + */ + Completed = 'completed', + + /** + * Search request was sent to the background. + * The page is loading in background. + */ + BackgroundLoading = 'backgroundLoading', + + /** + * Page load completed with background session created. + */ + BackgroundCompleted = 'backgroundCompleted', + + /** + * Revisiting the page after background completion + */ + Restored = 'restored', + + /** + * Current session requests where explicitly canceled by user + * Displaying none or partial results + */ + Canceled = 'canceled', +} + +/** + * Internal state of SessionService + * {@link SessionState} is inferred from this state + * + * @private + */ +export interface SessionStateInternal { + /** + * Current session Id + * Empty means there is no current active session. + */ + sessionId?: string; + + /** + * Has the session already been stored (i.e. "sent to background")? + */ + isStored: boolean; + + /** + * Is this session a restored session (have these requests already been made, and we're just + * looking to re-use the previous search IDs)? + */ + isRestore: boolean; + + /** + * Set of currently running searches + * within a session and any info associated with them + */ + pendingSearches: SearchDescriptor[]; + + /** + * There was at least a single search in this session + */ + isStarted: boolean; + + /** + * If user has explicitly canceled search requests + */ + isCanceled: boolean; +} + +const createSessionDefaultState: < + SearchDescriptor = unknown +>() => SessionStateInternal = () => ({ + sessionId: undefined, + isStored: false, + isRestore: false, + isCanceled: false, + isStarted: false, + pendingSearches: [], +}); + +export interface SessionPureTransitions< + SearchDescriptor = unknown, + S = SessionStateInternal +> { + start: (state: S) => () => S; + restore: (state: S) => (sessionId: string) => S; + clear: (state: S) => () => S; + store: (state: S) => () => S; + trackSearch: (state: S) => (search: SearchDescriptor) => S; + unTrackSearch: (state: S) => (search: SearchDescriptor) => S; + cancel: (state: S) => () => S; +} + +export const sessionPureTransitions: SessionPureTransitions = { + start: (state) => () => ({ ...createSessionDefaultState(), sessionId: uuid.v4() }), + restore: (state) => (sessionId: string) => ({ + ...createSessionDefaultState(), + sessionId, + isRestore: true, + isStored: true, + }), + clear: (state) => () => createSessionDefaultState(), + store: (state) => () => { + if (!state.sessionId) throw new Error("Can't store session. Missing sessionId"); + if (state.isStored || state.isRestore) + throw new Error('Can\'t store because current session is already stored"'); + return { + ...state, + isStored: true, + }; + }, + trackSearch: (state) => (search) => { + if (!state.sessionId) throw new Error("Can't track search. Missing sessionId"); + return { + ...state, + isStarted: true, + pendingSearches: state.pendingSearches.concat(search), + }; + }, + unTrackSearch: (state) => (search) => { + return { + ...state, + pendingSearches: state.pendingSearches.filter((s) => s !== search), + }; + }, + cancel: (state) => () => { + if (!state.sessionId) throw new Error("Can't cancel searches. Missing sessionId"); + if (state.isRestore) throw new Error("Can't cancel searches when restoring older searches"); + return { + ...state, + pendingSearches: [], + isCanceled: true, + isStored: false, + }; + }, +}; + +export interface SessionPureSelectors< + SearchDescriptor = unknown, + S = SessionStateInternal +> { + getState: (state: S) => () => SessionState; +} + +export const sessionPureSelectors: SessionPureSelectors = { + getState: (state) => () => { + if (!state.sessionId) return SessionState.None; + if (!state.isStarted) return SessionState.None; + if (state.isCanceled) return SessionState.Canceled; + switch (true) { + case state.isRestore: + return state.pendingSearches.length > 0 + ? SessionState.BackgroundLoading + : SessionState.Restored; + case state.isStored: + return state.pendingSearches.length > 0 + ? SessionState.BackgroundLoading + : SessionState.BackgroundCompleted; + default: + return state.pendingSearches.length > 0 ? SessionState.Loading : SessionState.Completed; + } + return SessionState.None; + }, +}; + +export type SessionStateContainer = StateContainer< + SessionStateInternal, + SessionPureTransitions, + SessionPureSelectors +>; + +export const createSessionStateContainer = ( + { freeze = true }: { freeze: boolean } = { freeze: true } +): { + stateContainer: SessionStateContainer; + sessionState$: Observable; +} => { + const stateContainer = createStateContainer( + createSessionDefaultState(), + sessionPureTransitions, + sessionPureSelectors, + freeze ? undefined : { freeze: (s) => s } + ) as SessionStateContainer; + + const sessionState$: Observable = stateContainer.state$.pipe( + map(() => stateContainer.selectors.getState()), + distinctUntilChanged(), + shareReplay(1) + ); + return { + stateContainer, + sessionState$, + }; +}; diff --git a/src/plugins/data/public/search/session/sessions_client.ts b/src/plugins/data/public/search/session/sessions_client.ts new file mode 100644 index 0000000000000..c19c5db064094 --- /dev/null +++ b/src/plugins/data/public/search/session/sessions_client.ts @@ -0,0 +1,91 @@ +/* + * 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 { PublicContract } from '@kbn/utility-types'; +import { HttpSetup } from 'kibana/public'; +import type { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; +import { BackgroundSessionSavedObjectAttributes, SearchSessionFindOptions } from '../../../common'; + +export type ISessionsClient = PublicContract; +export interface SessionsClientDeps { + http: HttpSetup; +} + +/** + * CRUD backgroundSession SO + */ +export class SessionsClient { + private readonly http: HttpSetup; + + constructor(deps: SessionsClientDeps) { + this.http = deps.http; + } + + public get(sessionId: string): Promise> { + return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`); + } + + public create({ + name, + appId, + urlGeneratorId, + initialState, + restoreState, + sessionId, + }: { + name: string; + appId: string; + initialState: Record; + restoreState: Record; + urlGeneratorId: string; + sessionId: string; + }): Promise> { + return this.http.post(`/internal/session`, { + body: JSON.stringify({ + name, + initialState, + restoreState, + sessionId, + appId, + urlGeneratorId, + }), + }); + } + + public find( + options: SearchSessionFindOptions + ): Promise> { + return this.http!.post(`/internal/session`, { + body: JSON.stringify(options), + }); + } + + public update( + sessionId: string, + attributes: Partial + ): Promise> { + return this.http!.put(`/internal/session/${encodeURIComponent(sessionId)}`, { + body: JSON.stringify(attributes), + }); + } + + public delete(sessionId: string): Promise { + return this.http!.delete(`/internal/session/${encodeURIComponent(sessionId)}`); + } +} diff --git a/src/plugins/data/public/search/session_service.ts b/src/plugins/data/public/search/session_service.ts deleted file mode 100644 index 0141cff258a9f..0000000000000 --- a/src/plugins/data/public/search/session_service.ts +++ /dev/null @@ -1,149 +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 uuid from 'uuid'; -import { BehaviorSubject, Subscription } from 'rxjs'; -import { HttpStart, PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; -import { ConfigSchema } from '../../config'; -import { - ISessionService, - BackgroundSessionSavedObjectAttributes, - SearchSessionFindOptions, -} from '../../common'; - -export class SessionService implements ISessionService { - private session$ = new BehaviorSubject(undefined); - private get sessionId() { - return this.session$.getValue(); - } - private appChangeSubscription$?: Subscription; - private curApp?: string; - private http!: HttpStart; - - /** - * Has the session already been stored (i.e. "sent to background")? - */ - private _isStored: boolean = false; - - /** - * Is this session a restored session (have these requests already been made, and we're just - * looking to re-use the previous search IDs)? - */ - private _isRestore: boolean = false; - - constructor( - initializerContext: PluginInitializerContext, - getStartServices: StartServicesAccessor - ) { - /* - Make sure that apps don't leave sessions open. - */ - getStartServices().then(([coreStart]) => { - this.http = coreStart.http; - - this.appChangeSubscription$ = coreStart.application.currentAppId$.subscribe((appName) => { - if (this.sessionId) { - const message = `Application '${this.curApp}' had an open session while navigating`; - if (initializerContext.env.mode.dev) { - // TODO: This setTimeout is necessary due to a race condition while navigating. - setTimeout(() => { - coreStart.fatalErrors.add(message); - }, 100); - } else { - // eslint-disable-next-line no-console - console.warn(message); - } - } - this.curApp = appName; - }); - }); - } - - public destroy() { - this.appChangeSubscription$?.unsubscribe(); - } - - public getSessionId() { - return this.sessionId; - } - - public getSession$() { - return this.session$.asObservable(); - } - - public isStored() { - return this._isStored; - } - - public isRestore() { - return this._isRestore; - } - - public start() { - this._isStored = false; - this._isRestore = false; - this.session$.next(uuid.v4()); - return this.sessionId!; - } - - public restore(sessionId: string) { - this._isStored = true; - this._isRestore = true; - this.session$.next(sessionId); - return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`); - } - - public clear() { - this._isStored = false; - this._isRestore = false; - this.session$.next(undefined); - } - - public async save(name: string, url: string) { - const response = await this.http.post(`/internal/session`, { - body: JSON.stringify({ - name, - url, - sessionId: this.sessionId, - }), - }); - this._isStored = true; - return response; - } - - public get(sessionId: string) { - return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`); - } - - public find(options: SearchSessionFindOptions) { - return this.http.post(`/internal/session`, { - body: JSON.stringify(options), - }); - } - - public update(sessionId: string, attributes: Partial) { - return this.http.put(`/internal/session/${encodeURIComponent(sessionId)}`, { - body: JSON.stringify(attributes), - }); - } - - public delete(sessionId: string) { - return this.http.delete(`/internal/session/${encodeURIComponent(sessionId)}`); - } -} diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index c08d9f4c7be3f..057b242c22f20 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -21,9 +21,10 @@ import { PackageInfo } from 'kibana/server'; import { ISearchInterceptor } from './search_interceptor'; import { SearchUsageCollector } from './collectors'; import { AggsSetup, AggsSetupDependencies, AggsStartDependencies, AggsStart } from './aggs'; -import { ISearchGeneric, ISessionService, ISearchStartSearchSource } from '../../common/search'; +import { ISearchGeneric, ISearchStartSearchSource } from '../../common/search'; import { IndexPatternsContract } from '../../common/index_patterns/index_patterns'; import { UsageCollectionSetup } from '../../../usage_collection/public'; +import { ISessionsClient, ISessionService } from './session'; export { ISearchStartSearchSource }; @@ -39,10 +40,15 @@ export interface ISearchSetup { aggs: AggsSetup; usageCollector?: SearchUsageCollector; /** - * session management + * Current session management * {@link ISessionService} */ session: ISessionService; + /** + * Background search sessions SO CRUD + * {@link ISessionsClient} + */ + sessionsClient: ISessionsClient; /** * @internal */ @@ -73,10 +79,15 @@ export interface ISearchStart { */ searchSource: ISearchStartSearchSource; /** - * session management + * Current session management * {@link ISessionService} */ session: ISessionService; + /** + * Background search sessions SO CRUD + * {@link ISessionsClient} + */ + sessionsClient: ISessionsClient; } export { SEARCH_EVENT_TYPE } from './collectors'; diff --git a/src/plugins/data/server/kql_telemetry/route.ts b/src/plugins/data/server/kql_telemetry/route.ts index efcb3d038bcc6..c93500f360ad0 100644 --- a/src/plugins/data/server/kql_telemetry/route.ts +++ b/src/plugins/data/server/kql_telemetry/route.ts @@ -45,7 +45,7 @@ export function registerKqlTelemetryRoute( const counterName = optIn ? 'optInCount' : 'optOutCount'; try { - await internalRepository.incrementCounter('kql-telemetry', 'kql-telemetry', counterName); + await internalRepository.incrementCounter('kql-telemetry', 'kql-telemetry', [counterName]); } catch (error) { logger.warn(`Unable to increment counter: ${error}`); return response.customError({ diff --git a/src/plugins/data/server/saved_objects/background_session.ts b/src/plugins/data/server/saved_objects/background_session.ts index 74b03c4d867e4..e81272628c091 100644 --- a/src/plugins/data/server/saved_objects/background_session.ts +++ b/src/plugins/data/server/saved_objects/background_session.ts @@ -39,6 +39,12 @@ export const backgroundSessionMapping: SavedObjectsType = { status: { type: 'keyword', }, + appId: { + type: 'keyword', + }, + urlGeneratorId: { + type: 'keyword', + }, initialState: { type: 'object', enabled: false, diff --git a/src/plugins/data/server/search/routes/session.ts b/src/plugins/data/server/search/routes/session.ts index 93f07ecfb92ff..f7dfc776565e0 100644 --- a/src/plugins/data/server/search/routes/session.ts +++ b/src/plugins/data/server/search/routes/session.ts @@ -28,19 +28,31 @@ export function registerSessionRoutes(router: IRouter): void { body: schema.object({ sessionId: schema.string(), name: schema.string(), + appId: schema.string(), expires: schema.maybe(schema.string()), + urlGeneratorId: schema.string(), initialState: schema.maybe(schema.object({}, { unknowns: 'allow' })), restoreState: schema.maybe(schema.object({}, { unknowns: 'allow' })), }), }, }, async (context, request, res) => { - const { sessionId, name, expires, initialState, restoreState } = request.body; + const { + sessionId, + name, + expires, + initialState, + restoreState, + appId, + urlGeneratorId, + } = request.body; try { const response = await context.search!.session.save(sessionId, { name, + appId, expires, + urlGeneratorId, initialState, restoreState, }); diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts index 1ceebae967d4c..5ff6d4b932487 100644 --- a/src/plugins/data/server/search/session/session_service.test.ts +++ b/src/plugins/data/server/search/session/session_service.test.ts @@ -33,6 +33,8 @@ describe('BackgroundSessionService', () => { type: BACKGROUND_SESSION_TYPE, attributes: { name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', idMapping: {}, }, references: [], @@ -121,6 +123,8 @@ describe('BackgroundSessionService', () => { const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const isStored = false; const name = 'my saved background search session'; + const appId = 'my_app_id'; + const urlGeneratorId = 'my_url_generator_id'; const created = new Date().toISOString(); const expires = new Date().toISOString(); @@ -133,7 +137,11 @@ describe('BackgroundSessionService', () => { expect(savedObjectsClient.update).not.toHaveBeenCalled(); - await service.save(sessionId, { name, created, expires }, { savedObjectsClient }); + await service.save( + sessionId, + { name, created, expires, appId, urlGeneratorId }, + { savedObjectsClient } + ); expect(savedObjectsClient.create).toHaveBeenCalledWith( BACKGROUND_SESSION_TYPE, @@ -145,6 +153,8 @@ describe('BackgroundSessionService', () => { restoreState: {}, status: BackgroundSessionStatus.IN_PROGRESS, idMapping: { [requestHash]: searchId }, + appId, + urlGeneratorId, }, { id: sessionId } ); @@ -215,6 +225,8 @@ describe('BackgroundSessionService', () => { type: BACKGROUND_SESSION_TYPE, attributes: { name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', idMapping: { [requestHash]: searchId }, }, references: [], diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index eca5f428b8555..b9a738413ede4 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -64,20 +64,34 @@ export class BackgroundSessionService { sessionId: string, { name, + appId, created = new Date().toISOString(), expires = new Date(Date.now() + DEFAULT_EXPIRATION).toISOString(), status = BackgroundSessionStatus.IN_PROGRESS, + urlGeneratorId, initialState = {}, restoreState = {}, }: Partial, { savedObjectsClient }: BackgroundSessionDependencies ) => { if (!name) throw new Error('Name is required'); + if (!appId) throw new Error('AppId is required'); + if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); // Get the mapping of request hash/search ID for this session const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map(); const idMapping = Object.fromEntries(searchMap.entries()); - const attributes = { name, created, expires, status, initialState, restoreState, idMapping }; + const attributes = { + name, + created, + expires, + status, + initialState, + restoreState, + idMapping, + urlGeneratorId, + appId, + }; const session = await savedObjectsClient.create( BACKGROUND_SESSION_TYPE, attributes, diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 7059593c0c4e7..d0340c2cf4edd 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -23,7 +23,7 @@ import { debounceTime } from 'rxjs/operators'; import moment from 'moment'; import dateMath from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; -import { getState, splitState } from './discover_state'; +import { createSearchSessionRestorationDataProvider, getState, splitState } from './discover_state'; import { RequestAdapter } from '../../../../inspector/public'; import { @@ -60,14 +60,14 @@ import { getSwitchIndexPatternAppState } from '../helpers/get_switch_index_patte import { addFatalError } from '../../../../kibana_legacy/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../url_generator'; -import { removeQueryParam, getQueryParams } from '../../../../kibana_utils/public'; +import { getQueryParams, removeQueryParam } from '../../../../kibana_utils/public'; import { DEFAULT_COLUMNS_SETTING, MODIFY_COLUMNS_ON_SWITCH, SAMPLE_SIZE_SETTING, SEARCH_ON_PAGE_LOAD_SETTING, } from '../../../common'; -import { resolveIndexPattern, loadIndexPattern } from '../helpers/resolve_index_pattern'; +import { loadIndexPattern, resolveIndexPattern } from '../helpers/resolve_index_pattern'; import { getTopNavLinks } from '../components/top_nav/get_top_nav_links'; import { updateSearchSource } from '../helpers/update_search_source'; import { calcFieldCounts } from '../helpers/calc_field_counts'; @@ -85,7 +85,7 @@ const { toastNotifications, uiSettings: config, trackUiMetric, -} = services; +} = getServices(); const fetchStatuses = { UNINITIALIZED: 'uninitialized', @@ -204,12 +204,20 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise // used for restoring background session let isInitialSearch = true; + // search session requested a data refresh + subscriptions.add( + data.search.session.onRefresh$.subscribe(() => { + refetch$.next(); + }) + ); + const state = getState({ getStateDefaults, storeInSessionStorage: config.get('state:storeInSessionStorage'), history, toasts: core.notifications.toasts, }); + const { appStateContainer, startSync: startStateSync, @@ -280,6 +288,14 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise } }); + data.search.session.setSearchSessionInfoProvider( + createSearchSessionRestorationDataProvider({ + appStateContainer, + data, + getSavedSearchId: () => savedSearch.id, + }) + ); + $scope.setIndexPattern = async (id) => { const nextIndexPattern = await indexPatterns.get(id); if (nextIndexPattern) { diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index 3c6ef1d3e4334..7de4ac27dd81f 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -20,15 +20,23 @@ import { isEqual } from 'lodash'; import { History } from 'history'; import { NotificationsStart } from 'kibana/public'; import { - createStateContainer, createKbnUrlStateStorage, - syncState, - ReduxLikeStateContainer, + createStateContainer, IKbnUrlStateStorage, + ReduxLikeStateContainer, + StateContainer, + syncState, withNotifyOnErrors, } from '../../../../kibana_utils/public'; -import { esFilters, Filter, Query } from '../../../../data/public'; +import { + DataPublicPluginStart, + esFilters, + Filter, + Query, + SearchSessionInfoProvider, +} from '../../../../data/public'; import { migrateLegacyQuery } from '../helpers/migrate_legacy_query'; +import { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from '../../url_generator'; export interface AppState { /** @@ -247,3 +255,47 @@ export function isEqualState(stateA: AppState, stateB: AppState) { const { filters: stateBFilters = [], ...stateBPartial } = stateB; return isEqual(stateAPartial, stateBPartial) && isEqualFilters(stateAFilters, stateBFilters); } + +export function createSearchSessionRestorationDataProvider(deps: { + appStateContainer: StateContainer; + data: DataPublicPluginStart; + getSavedSearchId: () => string | undefined; +}): SearchSessionInfoProvider { + return { + getName: async () => 'Discover', + getUrlGeneratorData: async () => { + return { + urlGeneratorId: DISCOVER_APP_URL_GENERATOR, + initialState: createUrlGeneratorState({ ...deps, forceAbsoluteTime: false }), + restoreState: createUrlGeneratorState({ ...deps, forceAbsoluteTime: true }), + }; + }, + }; +} + +function createUrlGeneratorState({ + appStateContainer, + data, + getSavedSearchId, + forceAbsoluteTime, // TODO: not implemented +}: { + appStateContainer: StateContainer; + data: DataPublicPluginStart; + getSavedSearchId: () => string | undefined; + forceAbsoluteTime: boolean; +}): DiscoverUrlGeneratorState { + const appState = appStateContainer.get(); + return { + filters: data.query.filterManager.getFilters(), + indexPatternId: appState.index, + query: appState.query, + savedSearchId: getSavedSearchId(), + timeRange: data.query.timefilter.timefilter.getTime(), // TODO: handle relative time range + searchSessionId: data.search.session.getSessionId(), + columns: appState.columns, + sort: appState.sort, + savedQuery: appState.savedQuery, + interval: appState.interval, + useHash: false, + }; +} diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 980e90d0acf20..e592d0b0ec8fd 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -342,12 +342,11 @@ export class SearchEmbeddable if (isFetchRequired) { this.filtersSearchSource!.setField('filter', this.input.filters); this.filtersSearchSource!.setField('query', this.input.query); - - this.fetch(); - this.prevFilters = this.input.filters; this.prevQuery = this.input.query; this.prevTimeRange = this.input.timeRange; + + this.fetch(); } else if (this.searchScope) { // trigger a digest cycle to make sure non-fetch relevant changes are propagated this.searchScope.$applyAsync(); diff --git a/src/plugins/discover/public/url_generator.test.ts b/src/plugins/discover/public/url_generator.test.ts index 98b7625e63c72..95bff6b1fdc9c 100644 --- a/src/plugins/discover/public/url_generator.test.ts +++ b/src/plugins/discover/public/url_generator.test.ts @@ -221,6 +221,19 @@ describe('Discover url generator', () => { expect(url).toContain('__test__'); }); + test('can specify columns, interval, sort and savedQuery', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + columns: ['_source'], + interval: 'auto', + sort: [['timestamp, asc']], + savedQuery: '__savedQueryId__', + }); + expect(url).toMatchInlineSnapshot( + `"xyz/app/discover#/?_g=()&_a=(columns:!(_source),interval:auto,savedQuery:__savedQueryId__,sort:!(!('timestamp,%20asc')))"` + ); + }); + describe('useHash property', () => { describe('when default useHash is set to false', () => { test('when using default, sets index pattern ID in the generated URL', async () => { diff --git a/src/plugins/discover/public/url_generator.ts b/src/plugins/discover/public/url_generator.ts index df9b16a4627ec..6d86818910b11 100644 --- a/src/plugins/discover/public/url_generator.ts +++ b/src/plugins/discover/public/url_generator.ts @@ -52,7 +52,7 @@ export interface DiscoverUrlGeneratorState { refreshInterval?: RefreshInterval; /** - * Optionally apply filers. + * Optionally apply filters. */ filters?: Filter[]; @@ -72,6 +72,24 @@ export interface DiscoverUrlGeneratorState { * Background search session id */ searchSessionId?: string; + + /** + * Columns displayed in the table + */ + columns?: string[]; + + /** + * Used interval of the histogram + */ + interval?: string; + /** + * Array of the used sorting [[field,direction],...] + */ + sort?: string[][]; + /** + * id of the used saved query + */ + savedQuery?: string; } interface Params { @@ -88,20 +106,28 @@ export class DiscoverUrlGenerator public readonly id = DISCOVER_APP_URL_GENERATOR; public readonly createUrl = async ({ + useHash = this.params.useHash, filters, indexPatternId, query, refreshInterval, savedSearchId, timeRange, - useHash = this.params.useHash, searchSessionId, + columns, + savedQuery, + sort, + interval, }: DiscoverUrlGeneratorState): Promise => { const savedSearchPath = savedSearchId ? encodeURIComponent(savedSearchId) : ''; const appState: { query?: Query; filters?: Filter[]; index?: string; + columns?: string[]; + interval?: string; + sort?: string[][]; + savedQuery?: string; } = {}; const queryState: QueryState = {}; @@ -109,6 +135,10 @@ export class DiscoverUrlGenerator if (filters && filters.length) appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); if (indexPatternId) appState.index = indexPatternId; + if (columns) appState.columns = columns; + if (savedQuery) appState.savedQuery = savedQuery; + if (sort) appState.sort = sort; + if (interval) appState.interval = interval; if (timeRange) queryState.time = timeRange; if (filters && filters.length) diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 023cb3d19b632..534ab0f331e87 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -34,6 +34,7 @@ import { ExclusiveUnion } from '@elastic/eui'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { History } from 'history'; import { Href } from 'history'; +import { HttpSetup as HttpSetup_2 } from 'kibana/public'; import { I18nStart as I18nStart_2 } from 'src/core/public'; import { IconType } from '@elastic/eui'; import { ISearchOptions } from 'src/plugins/data/public'; @@ -56,7 +57,9 @@ import { OverlayStart as OverlayStart_2 } from 'src/core/public'; import { PackageInfo } from '@kbn/config'; import { Path } from 'history'; import { PluginInitializerContext } from 'src/core/public'; +import { PluginInitializerContext as PluginInitializerContext_3 } from 'kibana/public'; import * as PropTypes from 'prop-types'; +import { PublicContract } from '@kbn/utility-types'; import { PublicMethodsOf } from '@kbn/utility-types'; import { PublicUiSettingsParams } from 'src/core/server/types'; import React from 'react'; @@ -77,6 +80,7 @@ import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/ex import { ShallowPromise } from '@kbn/utility-types'; import { SimpleSavedObject as SimpleSavedObject_2 } from 'src/core/public'; import { Start as Start_2 } from 'src/plugins/inspector/public'; +import { StartServicesAccessor as StartServicesAccessor_2 } from 'kibana/public'; import { ToastInputFields as ToastInputFields_2 } from 'src/core/public/notifications'; import { ToastsSetup as ToastsSetup_2 } from 'kibana/public'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; diff --git a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts index 37657912deb95..60d05890028d1 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts @@ -294,8 +294,7 @@ export const getSavedObjects = (): SavedObject[] => [ attributes: { title: 'kibana_sample_data_ecommerce', timeFieldName: 'order_date', - fields: - '[{"name":"_id","type":"string","esTypes":["_id"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"name":"_index","type":"string","esTypes":["_index"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"name":"_score","type":"number","count":0,"scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"name":"_source","type":"_source","esTypes":["_source"],"count":0,"scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"name":"_type","type":"string","esTypes":["_type"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"name":"category","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"category.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"category"}}},{"name":"currency","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"customer_birth_date","type":"date","esTypes":["date"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"customer_first_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"customer_first_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_first_name"}}},{"name":"customer_full_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"customer_full_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_full_name"}}},{"name":"customer_gender","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"customer_id","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"customer_last_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"customer_last_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_last_name"}}},{"name":"customer_phone","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"day_of_week","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"day_of_week_i","type":"number","esTypes":["integer"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"email","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"event.dataset","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geoip.city_name","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geoip.continent_name","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geoip.country_iso_code","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geoip.location","type":"geo_point","esTypes":["geo_point"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geoip.region_name","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"manufacturer","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"manufacturer.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"manufacturer"}}},{"name":"order_date","type":"date","esTypes":["date"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"order_id","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products._id","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"products._id.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products._id"}}},{"name":"products.base_price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.base_unit_price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.category","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"products.category.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products.category"}}},{"name":"products.created_on","type":"date","esTypes":["date"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.discount_amount","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.discount_percentage","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.manufacturer","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"products.manufacturer.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products.manufacturer"}}},{"name":"products.min_price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.product_id","type":"number","esTypes":["long"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.product_name","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"products.product_name.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products.product_name"}}},{"name":"products.quantity","type":"number","esTypes":["integer"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.sku","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.tax_amount","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.taxful_price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.taxless_price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"products.unit_discount_amount","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"sku","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"taxful_total_price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"taxless_total_price","type":"number","esTypes":["half_float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"total_quantity","type":"number","esTypes":["integer"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"total_unique_products","type":"number","esTypes":["integer"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"type","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"user","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true}]', + fields: '[]', fieldFormatMap: '{"taxful_total_price":{"id":"number","params":{"pattern":"$0,0.[00]"}}}', }, references: [], diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts index 6f701d75e7d52..e65b6ad40651b 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts @@ -439,7 +439,7 @@ export const getSavedObjects = (): SavedObject[] => [ title: 'kibana_sample_data_flights', timeFieldName: 'timestamp', fields: - '[{"name":"AvgTicketPrice","type":"number","esTypes":["float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"Cancelled","type":"boolean","esTypes":["boolean"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"Carrier","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"Dest","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DestAirportID","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DestCityName","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DestCountry","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DestLocation","type":"geo_point","esTypes":["geo_point"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DestRegion","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DestWeather","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DistanceKilometers","type":"number","esTypes":["float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"DistanceMiles","type":"number","esTypes":["float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"FlightDelay","type":"boolean","esTypes":["boolean"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"FlightDelayMin","type":"number","esTypes":["integer"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"FlightDelayType","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"FlightNum","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"FlightTimeHour","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"FlightTimeMin","type":"number","esTypes":["float"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"Origin","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"OriginAirportID","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"OriginCityName","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"OriginCountry","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"OriginLocation","type":"geo_point","esTypes":["geo_point"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"OriginRegion","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"OriginWeather","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"_id","type":"string","esTypes":["_id"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"name":"_index","type":"string","esTypes":["_index"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"name":"_score","type":"number","count":0,"scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"name":"_source","type":"_source","esTypes":["_source"],"count":0,"scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"name":"_type","type":"string","esTypes":["_type"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"name":"dayOfWeek","type":"number","esTypes":["integer"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"timestamp","type":"date","esTypes":["date"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"hour_of_day","type":"number","count":0,"scripted":true,"script":"doc[\'timestamp\'].value.hourOfDay","lang":"painless","searchable":true,"aggregatable":true,"readFromDocValues":false}]', + '[{"name":"hour_of_day","type":"number","count":0,"scripted":true,"script":"doc[\'timestamp\'].value.hourOfDay","lang":"painless","searchable":true,"aggregatable":true,"readFromDocValues":false}]', fieldFormatMap: '{"hour_of_day":{"id":"number","params":{"pattern":"00"}},"AvgTicketPrice":{"id":"number","params":{"pattern":"$0,0.[00]"}}}', }, diff --git a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts index f8d39e6689fa8..068ba66c4b0de 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts @@ -270,7 +270,7 @@ export const getSavedObjects = (): SavedObject[] => [ title: 'kibana_sample_data_logs', timeFieldName: 'timestamp', fields: - '[{"name":"@timestamp","type":"date","esTypes":["date"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"_id","type":"string","esTypes":["_id"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"name":"_index","type":"string","esTypes":["_index"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"name":"_score","type":"number","count":0,"scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"name":"_source","type":"_source","esTypes":["_source"],"count":0,"scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"name":"_type","type":"string","esTypes":["_type"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"name":"agent","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"agent.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "agent"}}},{"name":"bytes","type":"number","esTypes":["long"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"clientip","type":"ip","esTypes":["ip"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"event.dataset","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"extension","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"extension.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "extension"}}},{"name":"geo.coordinates","type":"geo_point","esTypes":["geo_point"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geo.dest","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geo.src","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"geo.srcdest","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"host","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"host.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "host"}}},{"name":"index","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"index.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "index"}}},{"name":"ip","type":"ip","esTypes":["ip"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"machine.os","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"machine.os.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "machine.os"}}},{"name":"machine.ram","type":"number","esTypes":["long"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"memory","type":"number","esTypes":["double"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"message","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"message.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "message"}}},{"name":"phpmemory","type":"number","esTypes":["long"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"referer","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"request","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"request.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "request"}}},{"name":"response","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"response.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "response"}}},{"name":"tags","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"tags.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "tags"}}},{"name":"timestamp","type":"date","esTypes":["date"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"url","type":"string","esTypes":["text"],"count":0,"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"name":"url.keyword","type":"string","esTypes":["keyword"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent": "url"}}},{"name":"utc_time","type":"date","esTypes":["date"],"count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"name":"hour_of_day","type":"number","count":0,"scripted":true,"script":"doc[\'timestamp\'].value.getHour()","lang":"painless","searchable":true,"aggregatable":true,"readFromDocValues":false}]', + '[{"name":"hour_of_day","type":"number","count":0,"scripted":true,"script":"doc[\'timestamp\'].value.getHour()","lang":"painless","searchable":true,"aggregatable":true,"readFromDocValues":false}]', fieldFormatMap: '{"hour_of_day":{}}', }, references: [], diff --git a/src/plugins/home/server/services/sample_data/usage/usage.ts b/src/plugins/home/server/services/sample_data/usage/usage.ts index ba67906febf1a..6a243b47dee55 100644 --- a/src/plugins/home/server/services/sample_data/usage/usage.ts +++ b/src/plugins/home/server/services/sample_data/usage/usage.ts @@ -43,7 +43,7 @@ export function usage( addInstall: async (dataSet: string) => { try { const internalRepository = await internalRepositoryPromise; - await internalRepository.incrementCounter(SAVED_OBJECT_ID, dataSet, `installCount`); + await internalRepository.incrementCounter(SAVED_OBJECT_ID, dataSet, [`installCount`]); } catch (err) { handleIncrementError(err); } @@ -51,7 +51,7 @@ export function usage( addUninstall: async (dataSet: string) => { try { const internalRepository = await internalRepositoryPromise; - await internalRepository.incrementCounter(SAVED_OBJECT_ID, dataSet, `unInstallCount`); + await internalRepository.incrementCounter(SAVED_OBJECT_ID, dataSet, [`unInstallCount`]); } catch (err) { handleIncrementError(err); } diff --git a/src/plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap b/src/plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap index 35349b4719676..696b74d040e0c 100644 --- a/src/plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap +++ b/src/plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap @@ -2,7 +2,7 @@ exports[`interpreter/functions#input_control_vis returns an object with the correct structure 1`] = ` Object { - "as": "visualization", + "as": "input_control_vis", "type": "render", "value": Object { "visConfig": Object { diff --git a/src/plugins/input_control_vis/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/input_control_vis/public/__snapshots__/to_ast.test.ts.snap new file mode 100644 index 0000000000000..edd44d8dd0337 --- /dev/null +++ b/src/plugins/input_control_vis/public/__snapshots__/to_ast.test.ts.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`input_control_vis toExpressionAst should build an expression based on vis.params 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "visConfig": Array [ + "{\\"controls\\":[{\\"id\\":\\"1536977437774\\",\\"fieldName\\":\\"manufacturer.keyword\\",\\"parent\\":\\"\\",\\"label\\":\\"Manufacturer\\",\\"type\\":\\"list\\",\\"options\\":{\\"type\\":\\"terms\\",\\"multiselect\\":true,\\"dynamicOptions\\":true,\\"size\\":5,\\"order\\":\\"desc\\"},\\"indexPattern\\":\\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\\"}],\\"updateFiltersOnChange\\":false,\\"useTimeFilter\\":true,\\"pinFilters\\":false}", + ], + }, + "function": "input_control_vis", + "type": "function", + }, + ], + "type": "expression", +} +`; diff --git a/src/plugins/input_control_vis/public/components/editor/_index.scss b/src/plugins/input_control_vis/public/components/editor/_index.scss deleted file mode 100644 index 9af8f8d6e8222..0000000000000 --- a/src/plugins/input_control_vis/public/components/editor/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './control_editor'; diff --git a/src/plugins/input_control_vis/public/components/editor/_control_editor.scss b/src/plugins/input_control_vis/public/components/editor/control_editor.scss similarity index 100% rename from src/plugins/input_control_vis/public/components/editor/_control_editor.scss rename to src/plugins/input_control_vis/public/components/editor/control_editor.scss diff --git a/src/plugins/input_control_vis/public/components/editor/control_editor.tsx b/src/plugins/input_control_vis/public/components/editor/control_editor.tsx index aa473095aaf3f..109237f8db4ec 100644 --- a/src/plugins/input_control_vis/public/components/editor/control_editor.tsx +++ b/src/plugins/input_control_vis/public/components/editor/control_editor.tsx @@ -36,6 +36,8 @@ import { getTitle, ControlParams, CONTROL_TYPES, ControlParamsOptions } from '.. import { IIndexPattern } from '../../../../data/public'; import { InputControlVisDependencies } from '../../plugin'; +import './control_editor.scss'; + interface ControlEditorUiProps { controlIndex: number; controlParams: ControlParams; diff --git a/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx b/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx index a85f98c7b89ba..c05dec8fccbe1 100644 --- a/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx +++ b/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx @@ -21,16 +21,16 @@ import React from 'react'; import { shallowWithIntl, mountWithIntl } from '@kbn/test/jest'; import { findTestSubject } from '@elastic/eui/lib/test'; import { getDepsMock, getIndexPatternMock } from '../../test_utils'; -import { ControlsTab, ControlsTabUiProps } from './controls_tab'; +import ControlsTab, { ControlsTabProps } from './controls_tab'; import { Vis } from '../../../../visualizations/public'; const indexPatternsMock = { get: getIndexPatternMock, }; -let props: ControlsTabUiProps; +let props: ControlsTabProps; beforeEach(() => { - props = { + props = ({ deps: getDepsMock(), vis: ({ API: { @@ -78,18 +78,18 @@ beforeEach(() => { }, setValue: jest.fn(), intl: null as any, - }; + } as unknown) as ControlsTabProps; }); test('renders ControlsTab', () => { - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); describe('behavior', () => { test('add control button', () => { - const component = mountWithIntl(); + const component = mountWithIntl(); findTestSubject(component, 'inputControlEditorAddBtn').simulate('click'); @@ -102,7 +102,7 @@ describe('behavior', () => { }); test('remove control button', () => { - const component = mountWithIntl(); + const component = mountWithIntl(); findTestSubject(component, 'inputControlEditorRemoveControl0').simulate('click'); const expectedParams = [ 'controls', @@ -125,7 +125,7 @@ describe('behavior', () => { }); test('move down control button', () => { - const component = mountWithIntl(); + const component = mountWithIntl(); findTestSubject(component, 'inputControlEditorMoveDownControl0').simulate('click'); const expectedParams = [ 'controls', @@ -162,7 +162,7 @@ describe('behavior', () => { }); test('move up control button', () => { - const component = mountWithIntl(); + const component = mountWithIntl(); findTestSubject(component, 'inputControlEditorMoveUpControl1').simulate('click'); const expectedParams = [ 'controls', diff --git a/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx b/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx index a9f04a86f8d03..0e622e08c529f 100644 --- a/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx +++ b/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx @@ -18,7 +18,8 @@ */ import React, { PureComponent } from 'react'; -import { injectI18n, FormattedMessage, InjectedIntlProps } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiButton, @@ -44,22 +45,17 @@ import { } from '../../editor_utils'; import { getLineageMap, getParentCandidates } from '../../lineage'; import { InputControlVisDependencies } from '../../plugin'; +import { InputControlVisParams } from '../../types'; interface ControlsTabUiState { type: CONTROL_TYPES; } -interface ControlsTabUiParams { - controls: ControlParams[]; -} -type ControlsTabUiInjectedProps = InjectedIntlProps & - Pick, 'vis' | 'stateParams' | 'setValue'> & { - deps: InputControlVisDependencies; - }; +export type ControlsTabProps = VisOptionsProps & { + deps: InputControlVisDependencies; +}; -export type ControlsTabUiProps = ControlsTabUiInjectedProps; - -class ControlsTabUi extends PureComponent { +class ControlsTab extends PureComponent { state = { type: CONTROL_TYPES.LIST, }; @@ -161,8 +157,6 @@ class ControlsTabUi extends PureComponent {this.renderControls()} @@ -176,25 +170,31 @@ class ControlsTabUi extends PureComponent this.setState({ type: event.target.value as CONTROL_TYPES })} - aria-label={intl.formatMessage({ - id: 'inputControl.editor.controlsTab.select.controlTypeAriaLabel', - defaultMessage: 'Select control type', - })} + aria-label={i18n.translate( + 'inputControl.editor.controlsTab.select.controlTypeAriaLabel', + { + defaultMessage: 'Select control type', + } + )} /> @@ -205,10 +205,12 @@ class ControlsTabUi extends PureComponent ( - props: Omit -) => ; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { ControlsTab as default }; diff --git a/src/plugins/input_control_vis/public/components/editor/index.tsx b/src/plugins/input_control_vis/public/components/editor/index.tsx new file mode 100644 index 0000000000000..11b3c2ea4ee8a --- /dev/null +++ b/src/plugins/input_control_vis/public/components/editor/index.tsx @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { InputControlVisDependencies } from '../../plugin'; +import { InputControlVisParams } from '../../types'; + +const ControlsTab = lazy(() => import('./controls_tab')); +const OptionsTab = lazy(() => import('./options_tab')); + +export const getControlsTab = (deps: InputControlVisDependencies) => ( + props: VisOptionsProps +) => ; + +export const OptionsTabLazy = (props: VisOptionsProps) => ( + +); diff --git a/src/plugins/input_control_vis/public/components/editor/options_tab.test.tsx b/src/plugins/input_control_vis/public/components/editor/options_tab.test.tsx index 0f126e915a68c..0970d1cd3c298 100644 --- a/src/plugins/input_control_vis/public/components/editor/options_tab.test.tsx +++ b/src/plugins/input_control_vis/public/components/editor/options_tab.test.tsx @@ -22,13 +22,13 @@ import { shallow } from 'enzyme'; import { mountWithIntl } from '@kbn/test/jest'; import { Vis } from '../../../../visualizations/public'; -import { OptionsTab, OptionsTabProps } from './options_tab'; +import OptionsTab, { OptionsTabProps } from './options_tab'; describe('OptionsTab', () => { let props: OptionsTabProps; beforeEach(() => { - props = { + props = ({ vis: {} as Vis, stateParams: { updateFiltersOnChange: false, @@ -36,7 +36,7 @@ describe('OptionsTab', () => { pinFilters: false, }, setValue: jest.fn(), - }; + } as unknown) as OptionsTabProps; }); it('should renders OptionsTab', () => { diff --git a/src/plugins/input_control_vis/public/components/editor/options_tab.tsx b/src/plugins/input_control_vis/public/components/editor/options_tab.tsx index cdff6cabad8ba..306d1141e75bd 100644 --- a/src/plugins/input_control_vis/public/components/editor/options_tab.tsx +++ b/src/plugins/input_control_vis/public/components/editor/options_tab.tsx @@ -24,20 +24,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSwitchEvent } from '@elastic/eui'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { InputControlVisParams } from '../../types'; -interface OptionsTabParams { - updateFiltersOnChange: boolean; - useTimeFilter: boolean; - pinFilters: boolean; -} -type OptionsTabInjectedProps = Pick< - VisOptionsProps, - 'vis' | 'setValue' | 'stateParams' ->; - -export type OptionsTabProps = OptionsTabInjectedProps; +export type OptionsTabProps = VisOptionsProps; -export class OptionsTab extends PureComponent { +class OptionsTab extends PureComponent { handleUpdateFiltersChange = (event: EuiSwitchEvent) => { this.props.setValue('updateFiltersOnChange', event.target.checked); }; @@ -98,3 +89,6 @@ export class OptionsTab extends PureComponent { ); } } +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { OptionsTab as default }; diff --git a/src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap index 5a76967c71fbb..5e1f25993616b 100644 --- a/src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap +++ b/src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap @@ -2,355 +2,371 @@ exports[`Apply and Cancel change btns enabled when there are changes 1`] = `
- - - - - - - - - - - - + + - - - - - - + + + + - - - - + + + + + + + + + + +
`; exports[`Clear btns enabled when there are values 1`] = `
- - - - - - - - - - - - + + - - - - - - + + + + - - - - + + + + + + + + + + +
`; exports[`Renders list control 1`] = `
- - - - - - - - - - - - + + - - - - - - + + + + - - - - + + + + + + + + + + +
`; exports[`Renders range control 1`] = `
- - - - - - - - - - - - + + - - - - - - + + + + - - - - + + + + + + + + + + +
`; diff --git a/src/plugins/input_control_vis/public/components/vis/_index.scss b/src/plugins/input_control_vis/public/components/vis/_index.scss deleted file mode 100644 index a428a7c1782e3..0000000000000 --- a/src/plugins/input_control_vis/public/components/vis/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './vis'; diff --git a/src/plugins/input_control_vis/public/components/vis/_vis.scss b/src/plugins/input_control_vis/public/components/vis/_vis.scss deleted file mode 100644 index d42c2c5f263c7..0000000000000 --- a/src/plugins/input_control_vis/public/components/vis/_vis.scss +++ /dev/null @@ -1,5 +0,0 @@ -.icvContainer { - width: 100%; - margin: 0 $euiSizeXS; - padding: $euiSizeS; -} diff --git a/src/plugins/input_control_vis/public/components/vis/input_control_vis.scss b/src/plugins/input_control_vis/public/components/vis/input_control_vis.scss new file mode 100644 index 0000000000000..322573446f762 --- /dev/null +++ b/src/plugins/input_control_vis/public/components/vis/input_control_vis.scss @@ -0,0 +1,13 @@ +.icvContainer__wrapper { + @include euiScrollBar; + min-height: 0; + flex: 1 1 0; + display: flex; + overflow: auto; +} + +.icvContainer { + width: 100%; + margin: 0 $euiSizeXS; + padding: $euiSizeS; +} diff --git a/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx b/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx index 95edb4a35bc22..058f39cb8a6d4 100644 --- a/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx +++ b/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx @@ -26,6 +26,8 @@ import { RangeControl } from '../../control/range_control_factory'; import { ListControl as ListControlComponent } from './list_control'; import { RangeControl as RangeControlComponent } from './range_control'; +import './input_control_vis.scss'; + function isListControl(control: RangeControl | ListControl): control is ListControl { return control.type === CONTROL_TYPES.LIST; } @@ -165,9 +167,11 @@ export class InputControlVis extends Component { } return ( -
- {this.renderControls()} - {stagingButtons} +
+
+ {this.renderControls()} + {stagingButtons} +
); } diff --git a/src/plugins/input_control_vis/public/index.scss b/src/plugins/input_control_vis/public/index.scss deleted file mode 100644 index 42fded23d7761..0000000000000 --- a/src/plugins/input_control_vis/public/index.scss +++ /dev/null @@ -1,9 +0,0 @@ -// Prefix all styles with "icv" to avoid conflicts. -// Examples -// icvChart -// icvChart__legend -// icvChart__legend--small -// icvChart__legend-isLoading - -@import './components/editor/index'; -@import './components/vis/index'; diff --git a/src/plugins/input_control_vis/public/index.ts b/src/plugins/input_control_vis/public/index.ts index 8edd3fd9996c3..b6fee12f6d9cb 100644 --- a/src/plugins/input_control_vis/public/index.ts +++ b/src/plugins/input_control_vis/public/index.ts @@ -17,8 +17,6 @@ * under the License. */ -import './index.scss'; - import { PluginInitializerContext } from '../../../core/public'; import { InputControlVisPlugin as Plugin } from './plugin'; diff --git a/src/plugins/input_control_vis/public/input_control_fn.ts b/src/plugins/input_control_vis/public/input_control_fn.ts index 1664555b916b6..46fba66264bcb 100644 --- a/src/plugins/input_control_vis/public/input_control_fn.ts +++ b/src/plugins/input_control_vis/public/input_control_fn.ts @@ -20,24 +20,25 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; +import { InputControlVisParams } from './types'; interface Arguments { visConfig: string; } -type VisParams = Required; - -interface RenderValue { +export interface InputControlRenderValue { visType: 'input_control_vis'; - visConfig: VisParams; + visConfig: InputControlVisParams; } -export const createInputControlVisFn = (): ExpressionFunctionDefinition< +export type InputControlExpressionFunctionDefinition = ExpressionFunctionDefinition< 'input_control_vis', Datatable, Arguments, - Render -> => ({ + Render +>; + +export const createInputControlVisFn = (): InputControlExpressionFunctionDefinition => ({ name: 'input_control_vis', type: 'render', inputTypes: [], @@ -52,10 +53,10 @@ export const createInputControlVisFn = (): ExpressionFunctionDefinition< }, }, fn(input, args) { - const params = JSON.parse(args.visConfig); + const params: InputControlVisParams = JSON.parse(args.visConfig); return { type: 'render', - as: 'visualization', + as: 'input_control_vis', value: { visType: 'input_control_vis', visConfig: params, diff --git a/src/plugins/input_control_vis/public/input_control_vis_renderer.tsx b/src/plugins/input_control_vis/public/input_control_vis_renderer.tsx new file mode 100644 index 0000000000000..6431ed6ebed1e --- /dev/null +++ b/src/plugins/input_control_vis/public/input_control_vis_renderer.tsx @@ -0,0 +1,51 @@ +/* + * 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 { ExpressionRenderDefinition } from 'src/plugins/expressions'; +import { InputControlVisDependencies } from './plugin'; +import { InputControlRenderValue } from './input_control_fn'; +import type { InputControlVisControllerType } from './vis_controller'; + +const inputControlVisRegistry = new Map(); + +export const getInputControlVisRenderer: ( + deps: InputControlVisDependencies +) => ExpressionRenderDefinition = (deps) => ({ + name: 'input_control_vis', + reuseDomNode: true, + render: async (domNode, { visConfig }, handlers) => { + let registeredController = inputControlVisRegistry.get(domNode); + + if (!registeredController) { + const { createInputControlVisController } = await import('./vis_controller'); + + const Controller = createInputControlVisController(deps, handlers); + registeredController = new Controller(domNode); + inputControlVisRegistry.set(domNode, registeredController); + + handlers.onDestroy(() => { + registeredController?.destroy(); + inputControlVisRegistry.delete(domNode); + }); + } + + await registeredController.render(visConfig); + handlers.done(); + }, +}); diff --git a/src/plugins/input_control_vis/public/input_control_vis_type.ts b/src/plugins/input_control_vis/public/input_control_vis_type.ts index 6e33e18c1603b..686327a1ba774 100644 --- a/src/plugins/input_control_vis/public/input_control_vis_type.ts +++ b/src/plugins/input_control_vis/public/input_control_vis_type.ts @@ -19,15 +19,14 @@ import { i18n } from '@kbn/i18n'; import { VisGroups, BaseVisTypeOptions } from '../../visualizations/public'; -import { createInputControlVisController } from './vis_controller'; -import { getControlsTab } from './components/editor/controls_tab'; -import { OptionsTab } from './components/editor/options_tab'; +import { getControlsTab, OptionsTabLazy } from './components/editor'; import { InputControlVisDependencies } from './plugin'; +import { toExpressionAst } from './to_ast'; +import { InputControlVisParams } from './types'; export function createInputControlVisTypeDefinition( deps: InputControlVisDependencies -): BaseVisTypeOptions { - const InputControlVisController = createInputControlVisController(deps); +): BaseVisTypeOptions { const ControlsTab = getControlsTab(deps); return { @@ -41,7 +40,6 @@ export function createInputControlVisTypeDefinition( defaultMessage: 'Add dropdown menus and range sliders to your dashboard.', }), stage: 'experimental', - visualization: InputControlVisController, visConfig: { defaults: { controls: [], @@ -64,12 +62,12 @@ export function createInputControlVisTypeDefinition( title: i18n.translate('inputControl.register.tabs.optionsTitle', { defaultMessage: 'Options', }), - editor: OptionsTab, + editor: OptionsTabLazy, }, ], }, inspectorAdapters: {}, requestHandler: 'none', - responseHandler: 'none', + toExpressionAst, }; } diff --git a/src/plugins/input_control_vis/public/plugin.ts b/src/plugins/input_control_vis/public/plugin.ts index 2c93a529c25b1..afaaa27d74c82 100644 --- a/src/plugins/input_control_vis/public/plugin.ts +++ b/src/plugins/input_control_vis/public/plugin.ts @@ -22,6 +22,7 @@ import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/p import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public'; import { createInputControlVisFn } from './input_control_fn'; +import { getInputControlVisRenderer } from './input_control_vis_renderer'; import { createInputControlVisTypeDefinition } from './input_control_vis_type'; type InputControlVisCoreSetup = CoreSetup; @@ -76,6 +77,7 @@ export class InputControlVisPlugin implements Plugin { }; expressions.registerFunction(createInputControlVisFn); + expressions.registerRenderer(getInputControlVisRenderer(visualizationDependencies)); visualizations.createBaseVisualization( createInputControlVisTypeDefinition(visualizationDependencies) ); diff --git a/src/plugins/input_control_vis/public/to_ast.test.ts b/src/plugins/input_control_vis/public/to_ast.test.ts new file mode 100644 index 0000000000000..fbeb78ee93a1e --- /dev/null +++ b/src/plugins/input_control_vis/public/to_ast.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { Vis } from '../../visualizations/public'; +import { InputControlVisParams } from './types'; +import { toExpressionAst } from './to_ast'; + +describe('input_control_vis toExpressionAst', () => { + const vis = { + params: { + controls: [ + { + id: '1536977437774', + fieldName: 'manufacturer.keyword', + parent: '', + label: 'Manufacturer', + type: 'list', + options: { + type: 'terms', + multiselect: true, + dynamicOptions: true, + size: 5, + order: 'desc', + }, + indexPattern: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + }, + ], + updateFiltersOnChange: false, + useTimeFilter: true, + pinFilters: false, + }, + } as Vis; + + it('should build an expression based on vis.params', () => { + const expression = toExpressionAst(vis); + expect(expression).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/input_control_vis/public/to_ast.ts b/src/plugins/input_control_vis/public/to_ast.ts new file mode 100644 index 0000000000000..93c0b4a87cfe6 --- /dev/null +++ b/src/plugins/input_control_vis/public/to_ast.ts @@ -0,0 +1,36 @@ +/* + * 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 { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { Vis } from '../../visualizations/public'; +import { InputControlExpressionFunctionDefinition } from './input_control_fn'; +import { InputControlVisParams } from './types'; + +export const toExpressionAst = (vis: Vis) => { + const inputControl = buildExpressionFunction( + 'input_control_vis', + { + visConfig: JSON.stringify(vis.params), + } + ); + + const ast = buildExpression([inputControl]); + + return ast.toAst(); +}; diff --git a/src/plugins/data/common/mocks.ts b/src/plugins/input_control_vis/public/types.ts similarity index 79% rename from src/plugins/data/common/mocks.ts rename to src/plugins/input_control_vis/public/types.ts index dde70b1d07443..2898ab49590ed 100644 --- a/src/plugins/data/common/mocks.ts +++ b/src/plugins/input_control_vis/public/types.ts @@ -17,4 +17,11 @@ * under the License. */ -export { getSessionServiceMock } from './search/session/mocks'; +import { ControlParams } from './editor_utils'; + +export interface InputControlVisParams { + controls: ControlParams[]; + pinFilters: boolean; + updateFiltersOnChange: boolean; + useTimeFilter: boolean; +} diff --git a/src/plugins/input_control_vis/public/vis_controller.tsx b/src/plugins/input_control_vis/public/vis_controller.tsx index 6f35e17866120..8e762a38671e9 100644 --- a/src/plugins/input_control_vis/public/vis_controller.tsx +++ b/src/plugins/input_control_vis/public/vis_controller.tsx @@ -20,20 +20,29 @@ import React from 'react'; import { isEqual } from 'lodash'; import { render, unmountComponentAtNode } from 'react-dom'; - import { Subscription } from 'rxjs'; + import { I18nStart } from 'kibana/public'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { VisualizationContainer } from '../../visualizations/public'; +import { FilterManager, Filter } from '../../data/public'; + import { InputControlVis } from './components/vis/input_control_vis'; import { getControlFactory } from './control/control_factory'; import { getLineageMap } from './lineage'; -import { ControlParams } from './editor_utils'; import { RangeControl } from './control/range_control_factory'; import { ListControl } from './control/list_control_factory'; import { InputControlVisDependencies } from './plugin'; -import { FilterManager, Filter } from '../../data/public'; -import { VisParams, ExprVis } from '../../visualizations/public'; +import { InputControlVisParams } from './types'; -export const createInputControlVisController = (deps: InputControlVisDependencies) => { +export type InputControlVisControllerType = InstanceType< + ReturnType +>; + +export const createInputControlVisController = ( + deps: InputControlVisDependencies, + handlers: IInterpreterRenderHandlers +) => { return class InputControlVisController { private I18nContext?: I18nStart['Context']; private _isLoaded = false; @@ -43,9 +52,9 @@ export const createInputControlVisController = (deps: InputControlVisDependencie filterManager: FilterManager; updateSubsciption: any; timeFilterSubscription: Subscription; - visParams?: VisParams; + visParams?: InputControlVisParams; - constructor(public el: Element, public vis: ExprVis) { + constructor(public el: Element) { this.controls = []; this.queryBarUpdateHandler = this.updateControlsFromKbn.bind(this); @@ -63,7 +72,7 @@ export const createInputControlVisController = (deps: InputControlVisDependencie }); } - async render(visData: any, visParams: VisParams) { + async render(visParams: InputControlVisParams) { if (!this.I18nContext) { const [{ i18n }] = await deps.core.getStartServices(); this.I18nContext = i18n.Context; @@ -71,7 +80,7 @@ export const createInputControlVisController = (deps: InputControlVisDependencie if (!this._isLoaded || !isEqual(visParams, this.visParams)) { this.visParams = visParams; this.controls = []; - this.controls = await this.initControls(); + this.controls = await this.initControls(visParams); this._isLoaded = true; } this.drawVis(); @@ -91,34 +100,34 @@ export const createInputControlVisController = (deps: InputControlVisDependencie render( - + + + , this.el ); }; - async initControls() { - const controlParamsList = (this.visParams?.controls as ControlParams[])?.filter( - (controlParams) => { - // ignore controls that do not have indexPattern or field - return controlParams.indexPattern && controlParams.fieldName; - } - ); + async initControls(visParams: InputControlVisParams) { + const controlParamsList = visParams.controls.filter((controlParams) => { + // ignore controls that do not have indexPattern or field + return controlParams.indexPattern && controlParams.fieldName; + }); const controlFactoryPromises = controlParamsList.map((controlParams) => { const factory = getControlFactory(controlParams); - return factory(controlParams, this.visParams?.useTimeFilter, deps); + return factory(controlParams, visParams.useTimeFilter, deps); }); const controls = await Promise.all(controlFactoryPromises); diff --git a/src/plugins/telemetry/server/fetcher.ts b/src/plugins/telemetry/server/fetcher.ts index a3649f51577ac..820f2c7c4c4af 100644 --- a/src/plugins/telemetry/server/fetcher.ts +++ b/src/plugins/telemetry/server/fetcher.ts @@ -17,7 +17,6 @@ * under the License. */ -import moment from 'moment'; import { Observable, Subscription, timer } from 'rxjs'; import { take } from 'rxjs/operators'; // @ts-ignore @@ -213,7 +212,6 @@ export class FetcherTask { private async fetchTelemetry() { return await this.telemetryCollectionManager!.getStats({ unencrypted: false, - timestamp: moment().valueOf(), }); } diff --git a/src/plugins/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts index e9887456e2f36..326c87a75b0ea 100644 --- a/src/plugins/telemetry/server/index.ts +++ b/src/plugins/telemetry/server/index.ts @@ -44,7 +44,6 @@ export const plugin = (initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); @@ -109,12 +105,7 @@ export class TelemetryPlugin implements Plugin this.elasticsearchClient, - () => this.savedObjectsService - ); + registerCollection(telemetryCollectionManager); const router = http.createRouter(); registerRoutes({ @@ -138,11 +129,9 @@ export class TelemetryPlugin implements Plugin; * @param {Object} config contains the usageCollection, callCluster (deprecated), the esClient and Saved Objects client scoped to the request or the internal repository, and the kibana request * @param {Object} StatsCollectionContext contains logger and version (string) */ -export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( +export const getLocalStats: StatsGetter = async ( clustersDetails, config, context diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts index 40cbf0e4caa1d..77894091f6133 100644 --- a/src/plugins/telemetry/server/telemetry_collection/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/index.ts @@ -24,6 +24,5 @@ export { buildDataTelemetryPayload, } from './get_data_telemetry'; export { getLocalStats, TelemetryLocalStats } from './get_local_stats'; -export { getLocalLicense } from './get_local_license'; export { getClusterUuids } from './get_cluster_stats'; export { registerCollection } from './register_collection'; diff --git a/src/plugins/telemetry/server/telemetry_collection/register_collection.ts b/src/plugins/telemetry/server/telemetry_collection/register_collection.ts index 27ca5ae746512..fac315b01493e 100644 --- a/src/plugins/telemetry/server/telemetry_collection/register_collection.ts +++ b/src/plugins/telemetry/server/telemetry_collection/register_collection.ts @@ -36,27 +36,17 @@ * under the License. */ -import { ILegacyClusterClient, SavedObjectsServiceStart } from 'kibana/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; -import { IClusterClient } from '../../../../../src/core/server'; import { getLocalStats } from './get_local_stats'; import { getClusterUuids } from './get_cluster_stats'; -import { getLocalLicense } from './get_local_license'; export function registerCollection( - telemetryCollectionManager: TelemetryCollectionManagerPluginSetup, - esCluster: ILegacyClusterClient, - esClientGetter: () => IClusterClient | undefined, - soServiceGetter: () => SavedObjectsServiceStart | undefined + telemetryCollectionManager: TelemetryCollectionManagerPluginSetup ) { - telemetryCollectionManager.setCollection({ - esCluster, - esClientGetter, - soServiceGetter, + telemetryCollectionManager.setCollectionStrategy({ title: 'local', priority: 0, statsGetter: getLocalStats, clusterDetailsGetter: getClusterUuids, - licenseGetter: getLocalLicense, }); } diff --git a/src/plugins/telemetry_collection_manager/server/index.ts b/src/plugins/telemetry_collection_manager/server/index.ts index 36ab64731fe58..de2080059c80b 100644 --- a/src/plugins/telemetry_collection_manager/server/index.ts +++ b/src/plugins/telemetry_collection_manager/server/index.ts @@ -30,13 +30,11 @@ export function plugin(initializerContext: PluginInitializerContext) { export { TelemetryCollectionManagerPluginSetup, TelemetryCollectionManagerPluginStart, - ESLicense, StatsCollectionConfig, StatsGetter, StatsGetterConfig, StatsCollectionContext, ClusterDetails, ClusterDetailsGetter, - LicenseGetter, UsageStatsPayload, } from './types'; diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index bc33e9fbc82c5..a135f4b115b21 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -26,14 +26,15 @@ import { Logger, IClusterClient, SavedObjectsServiceStart, -} from '../../../core/server'; + ILegacyClusterClient, +} from 'src/core/server'; import { TelemetryCollectionManagerPluginSetup, TelemetryCollectionManagerPluginStart, BasicStatsPayload, - CollectionConfig, - Collection, + CollectionStrategyConfig, + CollectionStrategy, StatsGetterConfig, StatsCollectionConfig, UsageStatsPayload, @@ -49,9 +50,12 @@ interface TelemetryCollectionPluginsDepsSetup { export class TelemetryCollectionManagerPlugin implements Plugin { private readonly logger: Logger; - private readonly collections: Array> = []; + private collectionStrategy: CollectionStrategy | undefined; private usageGetterMethodPriority = -1; private usageCollection?: UsageCollectionSetup; + private legacyElasticsearchClient?: ILegacyClusterClient; + private elasticsearchClient?: IClusterClient; + private savedObjectsService?: SavedObjectsServiceStart; private readonly isDistributable: boolean; private readonly version: string; @@ -65,7 +69,7 @@ export class TelemetryCollectionManagerPlugin this.usageCollection = usageCollection; return { - setCollection: this.setCollection.bind(this), + setCollectionStrategy: this.setCollectionStrategy.bind(this), getOptInStats: this.getOptInStats.bind(this), getStats: this.getStats.bind(this), areAllCollectorsReady: this.areAllCollectorsReady.bind(this), @@ -73,8 +77,11 @@ export class TelemetryCollectionManagerPlugin } public start(core: CoreStart) { + this.legacyElasticsearchClient = core.elasticsearch.legacy.client; // TODO: Remove when all the collectors have migrated + this.elasticsearchClient = core.elasticsearch.client; + this.savedObjectsService = core.savedObjects; + return { - setCollection: this.setCollection.bind(this), getOptInStats: this.getOptInStats.bind(this), getStats: this.getStats.bind(this), areAllCollectorsReady: this.areAllCollectorsReady.bind(this), @@ -83,19 +90,10 @@ export class TelemetryCollectionManagerPlugin public stop() {} - private setCollection, T extends BasicStatsPayload>( - collectionConfig: CollectionConfig + private setCollectionStrategy( + collectionConfig: CollectionStrategyConfig ) { - const { - title, - priority, - esCluster, - esClientGetter, - soServiceGetter, - statsGetter, - clusterDetailsGetter, - licenseGetter, - } = collectionConfig; + const { title, priority, statsGetter, clusterDetailsGetter } = collectionConfig; if (typeof priority !== 'number') { throw new Error('priority must be set.'); @@ -108,78 +106,58 @@ export class TelemetryCollectionManagerPlugin if (!statsGetter) { throw Error('Stats getter method not set.'); } - if (!esCluster) { - throw Error('esCluster name must be set for the getCluster method.'); - } - if (!esClientGetter) { - throw Error('esClientGetter method not set.'); - } - if (!soServiceGetter) { - throw Error('soServiceGetter method not set.'); - } if (!clusterDetailsGetter) { throw Error('Cluster UUIds method is not set.'); } - if (!licenseGetter) { - throw Error('License getter method not set.'); - } - this.collections.unshift({ - licenseGetter, - statsGetter, - clusterDetailsGetter, - esCluster, - title, - esClientGetter, - soServiceGetter, - }); + this.logger.debug(`Setting ${title} as the telemetry collection strategy`); + + // Overwrite the collection strategy + this.collectionStrategy = collectionConfig; this.usageGetterMethodPriority = priority; } } + /** + * Returns the context to provide to the Collection Strategies. + * It may return undefined if the ES and SO clients are not initialised yet. + * @param config {@link StatsGetterConfig} + * @param usageCollection {@link UsageCollectionSetup} + * @private + */ private getStatsCollectionConfig( config: StatsGetterConfig, - collection: Collection, - collectionEsClient: IClusterClient, - collectionSoService: SavedObjectsServiceStart, usageCollection: UsageCollectionSetup - ): StatsCollectionConfig { - const { request } = config; - + ): StatsCollectionConfig | undefined { const callCluster = config.unencrypted - ? collection.esCluster.asScoped(request).callAsCurrentUser - : collection.esCluster.callAsInternalUser; + ? this.legacyElasticsearchClient?.asScoped(config.request).callAsCurrentUser + : this.legacyElasticsearchClient?.callAsInternalUser; // Scope the new elasticsearch Client appropriately and pass to the stats collection config const esClient = config.unencrypted - ? collectionEsClient.asScoped(config.request).asCurrentUser - : collectionEsClient.asInternalUser; + ? this.elasticsearchClient?.asScoped(config.request).asCurrentUser + : this.elasticsearchClient?.asInternalUser; // Scope the saved objects client appropriately and pass to the stats collection config const soClient = config.unencrypted - ? collectionSoService.getScopedClient(config.request) - : collectionSoService.createInternalRepository(); + ? this.savedObjectsService?.getScopedClient(config.request) + : this.savedObjectsService?.createInternalRepository(); // Provide the kibanaRequest so opted-in plugins can scope their custom clients only if the request is not encrypted - const kibanaRequest = config.unencrypted ? request : void 0; + const kibanaRequest = config.unencrypted ? config.request : void 0; - return { callCluster, usageCollection, esClient, soClient, kibanaRequest }; + if (callCluster && esClient && soClient) { + return { callCluster, usageCollection, esClient, soClient, kibanaRequest }; + } } private async getOptInStats(optInStatus: boolean, config: StatsGetterConfig) { if (!this.usageCollection) { return []; } - for (const collection of this.collections) { - // first fetch the client and make sure it's not undefined. - const collectionEsClient = collection.esClientGetter(); - const collectionSoService = collection.soServiceGetter(); - if (collectionEsClient !== undefined && collectionSoService !== undefined) { - const statsCollectionConfig = this.getStatsCollectionConfig( - config, - collection, - collectionEsClient, - collectionSoService, - this.usageCollection - ); + const collection = this.collectionStrategy; + if (collection) { + // Build the context (clients and others) to send to the CollectionStrategies + const statsCollectionConfig = this.getStatsCollectionConfig(config, this.usageCollection); + if (statsCollectionConfig) { try { const optInStats = await this.getOptInStatsForCollection( collection, @@ -194,8 +172,9 @@ export class TelemetryCollectionManagerPlugin return encryptTelemetry(optInStats, { useProdKey: this.isDistributable }); } } catch (err) { - this.logger.debug(`Failed to collect any opt in stats with registered collections.`); - // swallow error to try next collection; + this.logger.debug( + `Failed to collect any opt in stats with collection ${collection.title}.` + ); } } } @@ -203,19 +182,18 @@ export class TelemetryCollectionManagerPlugin return []; } - private areAllCollectorsReady = async () => { + private async areAllCollectorsReady() { return await this.usageCollection?.areAllCollectorsReady(); - }; + } private getOptInStatsForCollection = async ( - collection: Collection, + collection: CollectionStrategy, optInStatus: boolean, statsCollectionConfig: StatsCollectionConfig ) => { const context: StatsCollectionContext = { logger: this.logger.get(collection.title), version: this.version, - ...collection.customContext, }; const clustersDetails = await collection.clusterDetailsGetter(statsCollectionConfig, context); @@ -229,17 +207,11 @@ export class TelemetryCollectionManagerPlugin if (!this.usageCollection) { return []; } - for (const collection of this.collections) { - const collectionEsClient = collection.esClientGetter(); - const collectionSavedObjectsService = collection.soServiceGetter(); - if (collectionEsClient !== undefined && collectionSavedObjectsService !== undefined) { - const statsCollectionConfig = this.getStatsCollectionConfig( - config, - collection, - collectionEsClient, - collectionSavedObjectsService, - this.usageCollection - ); + const collection = this.collectionStrategy; + if (collection) { + // Build the context (clients and others) to send to the CollectionStrategies + const statsCollectionConfig = this.getStatsCollectionConfig(config, this.usageCollection); + if (statsCollectionConfig) { try { const usageData = await this.getUsageForCollection(collection, statsCollectionConfig); if (usageData.length) { @@ -256,7 +228,6 @@ export class TelemetryCollectionManagerPlugin this.logger.debug( `Failed to collect any usage with registered collection ${collection.title}.` ); - // swallow error to try next collection; } } } @@ -265,34 +236,24 @@ export class TelemetryCollectionManagerPlugin } private async getUsageForCollection( - collection: Collection, + collection: CollectionStrategy, statsCollectionConfig: StatsCollectionConfig ): Promise { const context: StatsCollectionContext = { logger: this.logger.get(collection.title), version: this.version, - ...collection.customContext, }; const clustersDetails = await collection.clusterDetailsGetter(statsCollectionConfig, context); if (clustersDetails.length === 0) { - // don't bother doing a further lookup, try next collection. + // don't bother doing a further lookup. return []; } - const [stats, licenses] = await Promise.all([ - collection.statsGetter(clustersDetails, statsCollectionConfig, context), - collection.licenseGetter(clustersDetails, statsCollectionConfig, context), - ]); + const stats = await collection.statsGetter(clustersDetails, statsCollectionConfig, context); - return stats.map((stat) => { - const license = licenses[stat.cluster_uuid]; - return { - collectionSource: collection.title, - ...(license ? { license } : {}), - ...stat, - }; - }); + // Add the `collectionSource` to the resulting payload + return stats.map((stat) => ({ collectionSource: collection.title, ...stat })); } } diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index a6cf1a9e5aaf9..05641d5064593 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -19,21 +19,18 @@ import { LegacyAPICaller, + ElasticsearchClient, Logger, KibanaRequest, - ILegacyClusterClient, - IClusterClient, - SavedObjectsServiceStart, SavedObjectsClientContract, ISavedObjectsRepository, -} from 'kibana/server'; +} from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { ElasticsearchClient } from '../../../../src/core/server'; import { TelemetryCollectionManagerPlugin } from './plugin'; export interface TelemetryCollectionManagerPluginSetup { - setCollection: , T extends BasicStatsPayload>( - collectionConfig: CollectionConfig + setCollectionStrategy: ( + collectionConfig: CollectionStrategyConfig ) => void; getOptInStats: TelemetryCollectionManagerPlugin['getOptInStats']; getStats: TelemetryCollectionManagerPlugin['getStats']; @@ -41,9 +38,6 @@ export interface TelemetryCollectionManagerPluginSetup { } export interface TelemetryCollectionManagerPluginStart { - setCollection: , T extends BasicStatsPayload>( - collectionConfig: CollectionConfig - ) => void; getOptInStats: TelemetryCollectionManagerPlugin['getOptInStats']; getStats: TelemetryCollectionManagerPlugin['getStats']; areAllCollectorsReady: TelemetryCollectionManagerPlugin['areAllCollectorsReady']; @@ -91,74 +85,34 @@ export interface BasicStatsPayload { } export interface UsageStatsPayload extends BasicStatsPayload { - license?: ESLicense; collectionSource: string; } -// From https://www.elastic.co/guide/en/elasticsearch/reference/current/get-license.html -export interface ESLicense { - status: string; - uid: string; - type: string; - issue_date: string; - issue_date_in_millis: number; - expiry_date: string; - expirty_date_in_millis: number; - max_nodes: number; - issued_to: string; - issuer: string; - start_date_in_millis: number; -} - export interface StatsCollectionContext { logger: Logger | Console; version: string; } export type StatsGetterConfig = UnencryptedStatsGetterConfig | EncryptedStatsGetterConfig; -export type ClusterDetailsGetter = {}> = ( +export type ClusterDetailsGetter = ( config: StatsCollectionConfig, - context: StatsCollectionContext & CustomContext + context: StatsCollectionContext ) => Promise; -export type StatsGetter< - CustomContext extends Record = {}, - T extends BasicStatsPayload = BasicStatsPayload -> = ( +export type StatsGetter = ( clustersDetails: ClusterDetails[], config: StatsCollectionConfig, - context: StatsCollectionContext & CustomContext + context: StatsCollectionContext ) => Promise; -export type LicenseGetter = {}> = ( - clustersDetails: ClusterDetails[], - config: StatsCollectionConfig, - context: StatsCollectionContext & CustomContext -) => Promise<{ [clusterUuid: string]: ESLicense | undefined }>; -export interface CollectionConfig< - CustomContext extends Record = {}, - T extends BasicStatsPayload = BasicStatsPayload -> { +export interface CollectionStrategyConfig { title: string; priority: number; - esCluster: ILegacyClusterClient; - esClientGetter: () => IClusterClient | undefined; // --> by now we know that the client getter will return the IClusterClient but we assure that through a code check - soServiceGetter: () => SavedObjectsServiceStart | undefined; // --> by now we know that the service getter will return the SavedObjectsServiceStart but we assure that through a code check - statsGetter: StatsGetter; - clusterDetailsGetter: ClusterDetailsGetter; - licenseGetter: LicenseGetter; - customContext?: CustomContext; + statsGetter: StatsGetter; + clusterDetailsGetter: ClusterDetailsGetter; } -export interface Collection< - CustomContext extends Record = {}, - T extends BasicStatsPayload = BasicStatsPayload -> { - customContext?: CustomContext; - statsGetter: StatsGetter; - licenseGetter: LicenseGetter; - clusterDetailsGetter: ClusterDetailsGetter; - esCluster: ILegacyClusterClient; - esClientGetter: () => IClusterClient | undefined; // the collection could still return undefined for the es client getter. - soServiceGetter: () => SavedObjectsServiceStart | undefined; // the collection could still return undefined for the Saved Objects Service getter. +export interface CollectionStrategy { + statsGetter: StatsGetter; + clusterDetailsGetter: ClusterDetailsGetter; title: string; } diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 5e6ed901c7647..33f7993f14233 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -140,6 +140,98 @@ export function registerMyPluginUsageCollector( } ``` +## Tracking interactions with incrementCounter +There are several ways to collect data that can provide insight into how users +use your plugin or specific features. For tracking user interactions the +`SavedObjectsRepository` provided by Core provides a useful `incrementCounter` +method which can be used to increment one or more counter fields in a +document. Examples of interactions include tracking: + - the number of API calls + - the number of times users installed and uninstalled the sample datasets + +When using `incrementCounter` for collecting usage data, you need to ensure +that usage collection happens on a best-effort basis and doesn't +negatively affect your plugin or users (see the example): + - Swallow any exceptions thrown from the incrementCounter method and log + a message in development. + - Don't block your application on the incrementCounter method (e.g. + don't use `await`) + - Set the `refresh` option to false to prevent unecessary index refreshes + which slows down Elasticsearch performance + + +Note: for brevity the following example does not follow Kibana's conventions +for structuring your plugin code. +```ts +// src/plugins/dashboard/server/plugin.ts + +import { PluginInitializerContext, Plugin, CoreStart, CoreSetup } from '../../src/core/server'; + +export class DashboardPlugin implements Plugin { + private readonly logger: Logger; + private readonly isDevEnvironment: boolean; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + this.isDevEnvironment = initializerContext.env.cliArgs.dev; + } + public setup(core) { + // Register a saved object type to store our usage counters + core.savedObjects.registerType({ + // Don't expose this saved object type via the saved objects HTTP API + hidden: true, + mappings: { + // Since we're not querying or aggregating over our counter documents + // we don't define any fields. + dynamic: false, + properties: {}, + }, + name: 'dashboard_usage_counters', + namespaceType: 'single', + }); + } + public start(core) { + const repository = core.savedObjects.createInternalRepository(['dashboard_usage_counters']); + // Initialize all the counter fields to 0 when our plugin starts + // NOTE: Usage collection happens on a best-effort basis, so we don't + // `await` the promise returned by `incrementCounter` and we swallow any + // exceptions in production. + repository + .incrementCounter('dashboard_usage_counters', 'dashboard_usage_counters', [ + 'apiCalls', + 'settingToggled', + ], {refresh: false, initialize: true}) + .catch((e) => (this.isDevEnvironment ? this.logger.error(e) : e)); + + const router = core.http.createRouter(); + + router.post( + { + path: `api/v1/dashboard/counters/{counter}`, + validate: { + params: schema.object({ + counter: schema.oneOf([schema.literal('apiCalls'), schema.literal('settingToggled')]), + }), + }, + }, + async (context, request, response) => { + request.params.id + + // NOTE: Usage collection happens on a best-effort basis, so we don't + // `await` the promise returned by `incrementCounter` and we swallow any + // exceptions in production. + repository + .incrementCounter('dashboard_usage_counters', 'dashboard_usage_counters', [ + counter + ], {refresh: false}) + .catch((e) => (this.isDevEnvironement ? this.logger.error(e) : e)); + + return response.ok(); + } + ); + } +} + ## Schema Field The `schema` field is a proscribed data model assists with detecting changes in usage collector payloads. To define the collector schema add a schema field that specifies every possible field reported when registering the collector. Whenever the `schema` field is set or changed please run `node scripts/telemetry_check.js --fix` to update the stored schema json files. @@ -200,7 +292,6 @@ export const myCollector = makeUsageCollector({ }, }); ``` - ## Update the telemetry payload and telemetry cluster field mappings There is a module in the telemetry service that creates the payload of data that gets sent up to the telemetry cluster. 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 d8327eb834e12..939c37764ab0e 100644 --- a/src/plugins/usage_collection/server/report/store_report.test.ts +++ b/src/plugins/usage_collection/server/report/store_report.test.ts @@ -69,7 +69,7 @@ describe('store_report', () => { expect(savedObjectClient.incrementCounter).toHaveBeenCalledWith( 'ui-metric', 'test-app-name:test-event-name', - 'count' + ['count'] ); expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith([ { diff --git a/src/plugins/usage_collection/server/report/store_report.ts b/src/plugins/usage_collection/server/report/store_report.ts index d9aac23fd1ff0..a54d3d226d736 100644 --- a/src/plugins/usage_collection/server/report/store_report.ts +++ b/src/plugins/usage_collection/server/report/store_report.ts @@ -50,7 +50,7 @@ export async function storeReport( const savedObjectId = `${appName}:${eventName}`; return { saved_objects: [ - await internalRepository.incrementCounter('ui-metric', savedObjectId, 'count'), + await internalRepository.incrementCounter('ui-metric', savedObjectId, ['count']), ], }; }), diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index 2b75f69620629..41dc26c8c130d 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -24,7 +24,6 @@ import { PANEL_TYPES } from '../common/panel_types'; import { toExpressionAst } from './to_ast'; import { VIS_EVENT_TO_TRIGGER, VisGroups, VisParams } from '../../visualizations/public'; import { getDataStart } from './services'; -import { INDEXES_SEPARATOR } from '../common/constants'; export const metricsVisDefinition = { name: 'metrics', @@ -84,18 +83,7 @@ export const metricsVisDefinition = { inspectorAdapters: {}, getUsedIndexPattern: async (params: VisParams) => { const { indexPatterns } = getDataStart(); - const indexes: string = params.index_pattern; - if (indexes) { - const cachedIndexes = await indexPatterns.getIdsWithTitle(); - const ids = indexes - .split(INDEXES_SEPARATOR) - .map((title) => cachedIndexes.find((i) => i.title === title)?.id) - .filter((id) => id); - - return Promise.all(ids.map((id) => indexPatterns.get(id!))); - } - - return []; + return params.index_pattern ? await indexPatterns.find(params.index_pattern) : []; }, }; diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts index 0969174c7143c..46f46eaa3026f 100644 --- a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts +++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts @@ -83,7 +83,7 @@ export class ValidationTelemetryService implements Plugin getRequestName(item, index) === data.name); if (requestObject) { + requestObject.dataObject.url = requestObject.url; requestObject.dataObject.values = data.rawResponse; } }); diff --git a/src/plugins/vis_type_vega/public/data_model/types.ts b/src/plugins/vis_type_vega/public/data_model/types.ts index acd35e1747624..3bfe218099577 100644 --- a/src/plugins/vis_type_vega/public/data_model/types.ts +++ b/src/plugins/vis_type_vega/public/data_model/types.ts @@ -82,8 +82,9 @@ interface Projection { name: string; } -interface RequestDataObject { +interface RequestDataObject { name?: string; + url?: TUrlData; values: SearchResponse; } @@ -186,7 +187,7 @@ export interface CacheBounds { max: number; } -interface Requests { +interface Requests> { url: TUrlData; name: string; dataObject: TRequestDataObject; diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index 9fb80c6a1b19d..eb666d65b8670 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -185,21 +185,21 @@ describe('VegaParser._resolveEsQueries', () => { 'es', check( { data: { name: 'requestId', url: { index: 'a' }, x: 1 } }, - { data: { name: 'requestId', values: [42], x: 1 } } + { data: { name: 'requestId', url: { index: 'a', body: {} }, values: [42], x: 1 } } ) ); test( 'es 2', check( { data: { name: 'requestId', url: { '%type%': 'elasticsearch', index: 'a' } } }, - { data: { name: 'requestId', values: [42] } } + { data: { name: 'requestId', url: { index: 'a', body: {} }, values: [42] } } ) ); test( 'es arr', check( { arr: [{ data: { name: 'requestId', url: { index: 'a' }, x: 1 } }] }, - { arr: [{ data: { name: 'requestId', values: [42], x: 1 } }] } + { arr: [{ data: { name: 'requestId', url: { index: 'a', body: {} }, values: [42], x: 1 } }] } ) ); test( diff --git a/src/plugins/vis_type_vega/public/lib/extract_index_pattern.test.ts b/src/plugins/vis_type_vega/public/lib/extract_index_pattern.test.ts new file mode 100644 index 0000000000000..a13428d539ad9 --- /dev/null +++ b/src/plugins/vis_type_vega/public/lib/extract_index_pattern.test.ts @@ -0,0 +1,125 @@ +/* + * 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 { dataPluginMock } from '../../../data/public/mocks'; +import { extractIndexPatternsFromSpec } from './extract_index_pattern'; +import { setData } from '../services'; + +import type { VegaSpec } from '../data_model/types'; + +const getMockedSpec = (mockedObj: any) => (mockedObj as unknown) as VegaSpec; + +describe('extractIndexPatternsFromSpec', () => { + const dataStart = dataPluginMock.createStartContract(); + + beforeAll(() => { + setData(dataStart); + }); + + test('should not throw errors if no index is specified', async () => { + const spec = getMockedSpec({ + data: {}, + }); + + const indexes = await extractIndexPatternsFromSpec(spec); + + expect(indexes).toMatchInlineSnapshot(`Array []`); + }); + + test('should extract single index pattern', async () => { + const spec = getMockedSpec({ + data: { + url: { + index: 'test', + }, + }, + }); + + const indexes = await extractIndexPatternsFromSpec(spec); + + expect(indexes).toMatchInlineSnapshot(` + Array [ + Object { + "id": "test", + "title": "test", + }, + ] + `); + }); + + test('should extract multiple index patterns', async () => { + const spec = getMockedSpec({ + data: [ + { + url: { + index: 'test1', + }, + }, + { + url: { + index: 'test2', + }, + }, + ], + }); + + const indexes = await extractIndexPatternsFromSpec(spec); + + expect(indexes).toMatchInlineSnapshot(` + Array [ + Object { + "id": "test1", + "title": "test1", + }, + Object { + "id": "test2", + "title": "test2", + }, + ] + `); + }); + + test('should filter empty values', async () => { + const spec = getMockedSpec({ + data: [ + { + url: { + wrong: 'wrong', + }, + }, + { + url: { + index: 'ok', + }, + }, + ], + }); + + const indexes = await extractIndexPatternsFromSpec(spec); + + expect(indexes).toMatchInlineSnapshot(` + Array [ + Object { + "id": "ok", + "title": "ok", + }, + ] + `); + }); +}); diff --git a/src/plugins/vis_type_vega/public/lib/extract_index_pattern.ts b/src/plugins/vis_type_vega/public/lib/extract_index_pattern.ts new file mode 100644 index 0000000000000..12cbd6f7ebbfa --- /dev/null +++ b/src/plugins/vis_type_vega/public/lib/extract_index_pattern.ts @@ -0,0 +1,47 @@ +/* + * 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 { flatten } from 'lodash'; +import { getData } from '../services'; + +import type { Data, VegaSpec } from '../data_model/types'; +import type { IndexPattern } from '../../../data/public'; + +export const extractIndexPatternsFromSpec = async (spec: VegaSpec) => { + const { indexPatterns } = getData(); + let data: Data[] = []; + + if (Array.isArray(spec.data)) { + data = spec.data; + } else if (spec.data) { + data = [spec.data]; + } + + return flatten( + await Promise.all( + data.reduce>>((accumulator, currentValue) => { + if (currentValue.url?.index) { + accumulator.push(indexPatterns.find(currentValue.url.index)); + } + + return accumulator; + }, []) + ) + ); +}; diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index 04481685c841b..55a69ab11966c 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -25,7 +25,6 @@ import { Setup as InspectorSetup } from '../../inspector/public'; import { setNotifications, setData, - setSavedObjects, setInjectedVars, setUISettings, setMapsLegacyConfig, @@ -100,7 +99,6 @@ export class VegaPlugin implements Plugin, void> { public start(core: CoreStart, { data }: VegaPluginStartDependencies) { setNotifications(core.notifications); - setSavedObjects(core.savedObjects); setData(data); setInjectedMetadata(core.injectedMetadata); } diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts index 455fe67dbc423..43856c8324847 100644 --- a/src/plugins/vis_type_vega/public/services.ts +++ b/src/plugins/vis_type_vega/public/services.ts @@ -17,12 +17,7 @@ * under the License. */ -import { - CoreStart, - SavedObjectsStart, - NotificationsStart, - IUiSettingsClient, -} from 'src/core/public'; +import { CoreStart, NotificationsStart, IUiSettingsClient } from 'src/core/public'; import { DataPublicPluginStart } from '../../data/public'; import { createGetterSetter } from '../../kibana_utils/public'; @@ -40,10 +35,6 @@ export const [getInjectedMetadata, setInjectedMetadata] = createGetterSetter< CoreStart['injectedMetadata'] >('InjectedMetadata'); -export const [getSavedObjects, setSavedObjects] = createGetterSetter( - 'SavedObjects' -); - export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ enableExternalUrls: boolean; emsTileLayerId: unknown; diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index 2211abb54aa93..d81bfe02389e2 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -18,19 +18,24 @@ */ import { i18n } from '@kbn/i18n'; -import { BaseVisTypeOptions } from 'src/plugins/visualizations/public'; +import { parse } from 'hjson'; +import type { BaseVisTypeOptions } from 'src/plugins/visualizations/public'; + import { DefaultEditorSize } from '../../vis_default_editor/public'; -import { VegaVisualizationDependencies } from './plugin'; +import type { VegaVisualizationDependencies } from './plugin'; import { createVegaRequestHandler } from './vega_request_handler'; import { getDefaultSpec } from './default_spec'; +import { extractIndexPatternsFromSpec } from './lib/extract_index_pattern'; import { createInspectorAdapters } from './vega_inspector'; import { VIS_EVENT_TO_TRIGGER, VisGroups } from '../../visualizations/public'; import { toExpressionAst } from './to_ast'; -import { VisParams } from './vega_fn'; import { getInfoMessage } from './components/experimental_map_vis_info'; import { VegaVisEditorComponent } from './components/vega_vis_editor_lazy'; +import type { VegaSpec } from './data_model/types'; +import type { VisParams } from './vega_fn'; + export const createVegaTypeDefinition = ( dependencies: VegaVisualizationDependencies ): BaseVisTypeOptions => { @@ -68,6 +73,16 @@ export const createVegaTypeDefinition = ( getSupportedTriggers: () => { return [VIS_EVENT_TO_TRIGGER.applyFilter]; }, + getUsedIndexPattern: async (visParams) => { + try { + const spec = parse(visParams.spec, { legacyRoot: false, keepWsc: true }); + + return extractIndexPatternsFromSpec(spec as VegaSpec); + } catch (e) { + // spec is invalid + } + return []; + }, inspectorAdapters: createInspectorAdapters, }; }; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 25ea77ddbccb4..10f08edef1aa6 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -27,7 +27,8 @@ import { i18n } from '@kbn/i18n'; import { TooltipHandler } from './vega_tooltip'; import { esFilters } from '../../../data/public'; -import { getEnableExternalUrls } from '../services'; +import { getEnableExternalUrls, getData } from '../services'; +import { extractIndexPatternsFromSpec } from '../lib/extract_index_pattern'; vega.scheme('elastic', euiPaletteColorBlind()); @@ -65,7 +66,6 @@ export class VegaBaseView { this._filterManager = opts.filterManager; this._fireEvent = opts.fireEvent; this._timefilter = opts.timefilter; - this._findIndex = opts.findIndex; this._view = null; this._vegaViewConfig = null; this._$messages = null; @@ -127,6 +127,48 @@ export class VegaBaseView { } } + /** + * Find index pattern by its title, if not given, gets it from spec or a defaults one + * @param {string} [index] + * @returns {Promise} index id + */ + async findIndex(index) { + const { indexPatterns } = getData(); + let idxObj; + + if (index) { + [idxObj] = await indexPatterns.find(index); + if (!idxObj) { + throw new Error( + i18n.translate('visTypeVega.vegaParser.baseView.indexNotFoundErrorMessage', { + defaultMessage: 'Index {index} not found', + values: { index: `"${index}"` }, + }) + ); + } + } else { + [idxObj] = await extractIndexPatternsFromSpec( + this._parser.isVegaLite ? this._parser.vlspec : this._parser.spec + ); + + if (!idxObj) { + const defaultIdx = await indexPatterns.getDefault(); + + if (defaultIdx) { + idxObj = defaultIdx; + } else { + throw new Error( + i18n.translate('visTypeVega.vegaParser.baseView.unableToFindDefaultIndexErrorMessage', { + defaultMessage: 'Unable to find default index', + }) + ); + } + } + } + + return idxObj.id; + } + createViewConfig() { const config = { // eslint-disable-next-line import/namespace @@ -261,7 +303,7 @@ export class VegaBaseView { * @param {string} [index] as defined in Kibana, or default if missing */ async addFilterHandler(query, index) { - const indexId = await this._findIndex(index); + const indexId = await this.findIndex(index); const filter = esFilters.buildQueryFilter(query, indexId); this._fireEvent({ name: 'applyFilter', data: { filters: [filter] } }); @@ -272,7 +314,7 @@ export class VegaBaseView { * @param {string} [index] as defined in Kibana, or default if missing */ async removeFilterHandler(query, index) { - const indexId = await this._findIndex(index); + const indexId = await this.findIndex(index); const filterToRemove = esFilters.buildQueryFilter(query, indexId); const currentFilters = this._filterManager.getFilters(); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index a2214e139a296..8a073ca32b94a 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -30,7 +30,7 @@ import vegaMapGraph from './test_utils/vega_map_test.json'; import { VegaParser } from './data_model/vega_parser'; import { SearchAPI } from './data_model/search_api'; -import { setInjectedVars, setData, setSavedObjects, setNotifications } from './services'; +import { setInjectedVars, setData, setNotifications } from './services'; import { coreMock } from '../../../core/public/mocks'; import { dataPluginMock } from '../../data/public/mocks'; @@ -76,7 +76,6 @@ describe('VegaVisualizations', () => { enableExternalUrls: true, }); setData(dataPluginStart); - setSavedObjects(coreStart.savedObjects); setNotifications(coreStart.notifications); vegaVisualizationDependencies = { diff --git a/src/plugins/vis_type_vega/public/vega_visualization.ts b/src/plugins/vis_type_vega/public/vega_visualization.ts index 58c436bcd4be4..554ac8962df46 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.ts +++ b/src/plugins/vis_type_vega/public/vega_visualization.ts @@ -20,13 +20,12 @@ import { i18n } from '@kbn/i18n'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { VegaParser } from './data_model/vega_parser'; import { VegaVisualizationDependencies } from './plugin'; -import { getNotifications, getData, getSavedObjects } from './services'; +import { getNotifications, getData } from './services'; import type { VegaView } from './vega_view/vega_view'; export const createVegaVisualization = ({ getServiceSettings }: VegaVisualizationDependencies) => class VegaVisualization { private readonly dataPlugin = getData(); - private readonly savedObjectsClient = getSavedObjects(); private vegaView: InstanceType | null = null; constructor( @@ -34,39 +33,6 @@ export const createVegaVisualization = ({ getServiceSettings }: VegaVisualizatio private fireEvent: IInterpreterRenderHandlers['event'] ) {} - /** - * Find index pattern by its title, of if not given, gets default - * @param {string} [index] - * @returns {Promise} index id - */ - async findIndex(index: string) { - const { indexPatterns } = this.dataPlugin; - let idxObj; - - if (index) { - // @ts-expect-error - idxObj = indexPatterns.findByTitle(this.savedObjectsClient, index); - if (!idxObj) { - throw new Error( - i18n.translate('visTypeVega.visualization.indexNotFoundErrorMessage', { - defaultMessage: 'Index {index} not found', - values: { index: `"${index}"` }, - }) - ); - } - } else { - idxObj = await indexPatterns.getDefault(); - if (!idxObj) { - throw new Error( - i18n.translate('visTypeVega.visualization.unableToFindDefaultIndexErrorMessage', { - defaultMessage: 'Unable to find default index', - }) - ); - } - } - return idxObj.id; - } - async render(visData: VegaParser) { const { toasts } = getNotifications(); @@ -112,7 +78,6 @@ export const createVegaVisualization = ({ getServiceSettings }: VegaVisualizatio serviceSettings, filterManager, timefilter, - findIndex: this.findIndex.bind(this), }; if (vegaParser.useMap) { diff --git a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap index 03a355c604c4d..3ff0c83961e2a 100644 --- a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap +++ b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap @@ -2,8 +2,6 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipeline calls toExpression on vis_type if it exists 1`] = `"kibana | kibana_context | test"`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles input_control_vis function 1`] = `"input_control_vis visConfig='{\\"some\\":\\"nested\\",\\"data\\":{\\"here\\":true}}' "`; - exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function with buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"bucket\\":1}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}}' "`; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts index 653542bd8837d..57c58a99f09ea 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts @@ -92,15 +92,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => { uiState = {}; }); - it('handles input_control_vis function', () => { - const params = { - some: 'nested', - data: { here: true }, - }; - const actual = buildPipelineVisFunction.input_control_vis(params, schemasDef, uiState); - expect(actual).toMatchSnapshot(); - }); - describe('handles region_map function', () => { it('without buckets', () => { const params = { metric: {} }; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 0c244876ca6a3..29f6ec9b069a7 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -219,9 +219,6 @@ export const prepareDimension = (variable: string, data: any) => { }; export const buildPipelineVisFunction: BuildPipelineVisFunction = { - input_control_vis: (params) => { - return `input_control_vis ${prepareJson('visConfig', params)}`; - }, region_map: (params, schemas) => { const visConfig = { ...params, diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index b207529c456a1..4b32880136146 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -20,7 +20,6 @@ import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; import { AppMountParameters, OverlayRef } from 'kibana/public'; -import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../../kibana_react/public'; import { @@ -31,6 +30,7 @@ import { } from '../types'; import { APP_NAME } from '../visualize_constants'; import { getTopNavConfig } from '../utils'; +import type { IndexPattern } from '../../../../data/public'; interface VisualizeTopNavProps { currentAppState: VisualizeAppState; @@ -118,7 +118,9 @@ const TopNav = ({ stateTransfer, onAppLeave, ]); - const [indexPattern, setIndexPattern] = useState(vis.data.indexPattern); + const [indexPatterns, setIndexPatterns] = useState( + vis.data.indexPattern ? [vis.data.indexPattern] : [] + ); const showDatePicker = () => { // tsvb loads without an indexPattern initially (TODO investigate). // hide timefilter only if timeFieldName is explicitly undefined. @@ -165,14 +167,27 @@ const TopNav = ({ ]); useEffect(() => { - if (!vis.data.indexPattern) { - services.data.indexPatterns.getDefault().then((index) => { - if (index) { - setIndexPattern(index); + const asyncSetIndexPattern = async () => { + let indexes: IndexPattern[] | undefined; + + if (vis.type.getUsedIndexPattern) { + indexes = await vis.type.getUsedIndexPattern(vis.params); + } + if (!indexes || !indexes.length) { + const defaultIndex = await services.data.indexPatterns.getDefault(); + if (defaultIndex) { + indexes = [defaultIndex]; } - }); + } + if (indexes) { + setIndexPatterns(indexes); + } + }; + + if (!vis.data.indexPattern) { + asyncSetIndexPattern(); } - }, [services.data.indexPatterns, vis.data.indexPattern]); + }, [vis.params, vis.type, services.data.indexPatterns, vis.data.indexPattern]); return isChromeVisible ? ( /** @@ -189,7 +204,7 @@ const TopNav = ({ onQuerySubmit={handleRefresh} savedQueryId={currentAppState.savedQuery} onSavedQueryIdChange={stateContainer.transitions.updateSavedQuery} - indexPatterns={indexPattern ? [indexPattern] : undefined} + indexPatterns={indexPatterns} screenTitle={vis.title} showAutoRefreshOnly={!showDatePicker()} showDatePicker={showDatePicker()} @@ -207,7 +222,7 @@ const TopNav = ({ { const esClient = getService('es'); - // FLAKY: https://github.com/elastic/kibana/issues/84445 - describe.skip('Kibana index migration', () => { + describe('Kibana index migration', () => { before(() => esClient.indices.delete({ index: '.migrate-*' })); it('Migrates an existing index that has never been migrated before', async () => { @@ -313,7 +312,10 @@ export default ({ getService }: FtrProviderContext) => { result // @ts-expect-error destIndex exists only on MigrationResult status: 'migrated'; .map(({ status, destIndex }) => ({ status, destIndex })) - .sort((a) => (a.destIndex ? 0 : 1)) + .sort(({ destIndex: a }, { destIndex: b }) => + // sort by destIndex in ascending order, keeping falsy values at the end + (a && !b) || a < b ? -1 : (!a && b) || a > b ? 1 : 0 + ) ).to.eql([ { status: 'migrated', destIndex: '.migration-c_2' }, { status: 'skipped', destIndex: undefined }, diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js index b3d34d5910fc3..a025a65779e9c 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.js +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -62,6 +62,8 @@ export default function ({ getService }) { expect(body.length).to.be(1); const stats = body[0]; expect(stats.collection).to.be('local'); + expect(stats.collectionSource).to.be('local'); + expect(stats.license).to.be.undefined; // OSS cannot get the license expect(stats.stack_stats.kibana.count).to.be.a('number'); expect(stats.stack_stats.kibana.indices).to.be.a('number'); expect(stats.stack_stats.kibana.os.platforms[0].platform).to.be.a('string'); diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index 49b160cc70312..20fda144b338e 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -31,8 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }; - // Failing: See https://github.com/elastic/kibana/issues/82445 - describe.skip('discover doc table', function describeIndexTests() { + describe('discover doc table', function describeIndexTests() { const defaultRowsLimit = 50; const rowsHardLimit = 500; diff --git a/x-pack/plugins/alerts/common/builtin_action_groups.ts b/x-pack/plugins/alerts/common/builtin_action_groups.ts index d31f75357d370..d9c5ae613f787 100644 --- a/x-pack/plugins/alerts/common/builtin_action_groups.ts +++ b/x-pack/plugins/alerts/common/builtin_action_groups.ts @@ -6,13 +6,13 @@ import { i18n } from '@kbn/i18n'; import { ActionGroup } from './alert_type'; -export const ResolvedActionGroup: ActionGroup = { - id: 'resolved', - name: i18n.translate('xpack.alerts.builtinActionGroups.resolved', { - defaultMessage: 'Resolved', +export const RecoveredActionGroup: ActionGroup = { + id: 'recovered', + name: i18n.translate('xpack.alerts.builtinActionGroups.recovered', { + defaultMessage: 'Recovered', }), }; export function getBuiltinActionGroups(): ActionGroup[] { - return [ResolvedActionGroup]; + return [RecoveredActionGroup]; } diff --git a/x-pack/plugins/alerts/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts index 8dc387f96addb..b04871a047e4b 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts @@ -105,8 +105,8 @@ describe('register()', () => { name: 'Default', }, { - id: 'resolved', - name: 'Resolved', + id: 'recovered', + name: 'Recovered', }, ], defaultActionGroupId: 'default', @@ -117,7 +117,7 @@ describe('register()', () => { expect(() => registry.register(alertType)).toThrowError( new Error( - `Alert type [id="${alertType.id}"] cannot be registered. Action groups [resolved] are reserved by the framework.` + `Alert type [id="${alertType.id}"] cannot be registered. Action groups [recovered] are reserved by the framework.` ) ); }); @@ -229,8 +229,8 @@ describe('get()', () => { "name": "Default", }, Object { - "id": "resolved", - "name": "Resolved", + "id": "recovered", + "name": "Recovered", }, ], "actionVariables": Object { @@ -287,8 +287,8 @@ describe('list()', () => { "name": "Test Action Group", }, Object { - "id": "resolved", - "name": "Resolved", + "id": "recovered", + "name": "Recovered", }, ], "actionVariables": Object { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts index 9bd61c0fe66d2..0a764ea768591 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -122,7 +122,7 @@ describe('getAlertInstanceSummary()', () => { .addActiveInstance('instance-previously-active', 'action group B') .advanceTime(10000) .addExecute() - .addResolvedInstance('instance-previously-active') + .addRecoveredInstance('instance-previously-active') .addActiveInstance('instance-currently-active', 'action group A') .getEvents(); const eventsResult = { diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts index f9e4a2908d6ce..1d5ebe2b5911e 100644 --- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts +++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts @@ -6,7 +6,7 @@ import { SanitizedAlert, AlertInstanceSummary } from '../types'; import { IValidatedEvent } from '../../../event_log/server'; -import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER } from '../plugin'; +import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin'; import { alertInstanceSummaryFromEventLog } from './alert_instance_summary_from_event_log'; const ONE_HOUR_IN_MILLIS = 60 * 60 * 1000; @@ -189,7 +189,43 @@ describe('alertInstanceSummaryFromEventLog', () => { .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() - .addResolvedInstance('instance-1') + .addRecoveredInstance('instance-1') + .getEvents(); + + const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ + alert, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, instances } = summary; + expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + Object { + "instances": Object { + "instance-1": Object { + "actionGroupId": undefined, + "activeStartDate": undefined, + "muted": false, + "status": "OK", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "OK", + } + `); + }); + + test('legacy alert with currently inactive instance', async () => { + const alert = createAlert({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') + .advanceTime(10000) + .addExecute() + .addLegacyResolvedInstance('instance-1') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -224,7 +260,7 @@ describe('alertInstanceSummaryFromEventLog', () => { .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() - .addResolvedInstance('instance-1') + .addRecoveredInstance('instance-1') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -406,7 +442,7 @@ describe('alertInstanceSummaryFromEventLog', () => { .advanceTime(10000) .addExecute() .addActiveInstance('instance-1', 'action group A') - .addResolvedInstance('instance-2') + .addRecoveredInstance('instance-2') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -451,7 +487,7 @@ describe('alertInstanceSummaryFromEventLog', () => { .advanceTime(10000) .addExecute() .addActiveInstance('instance-1', 'action group A') - .addResolvedInstance('instance-2') + .addRecoveredInstance('instance-2') .advanceTime(10000) .addExecute() .addActiveInstance('instance-1', 'action group B') @@ -561,12 +597,24 @@ export class EventsFactory { return this; } - addResolvedInstance(instanceId: string): EventsFactory { + addRecoveredInstance(instanceId: string): EventsFactory { + this.events.push({ + '@timestamp': this.date, + event: { + provider: EVENT_LOG_PROVIDER, + action: EVENT_LOG_ACTIONS.recoveredInstance, + }, + kibana: { alerting: { instance_id: instanceId } }, + }); + return this; + } + + addLegacyResolvedInstance(instanceId: string): EventsFactory { this.events.push({ '@timestamp': this.date, event: { provider: EVENT_LOG_PROVIDER, - action: EVENT_LOG_ACTIONS.resolvedInstance, + action: LEGACY_EVENT_LOG_ACTIONS.resolvedInstance, }, kibana: { alerting: { instance_id: instanceId } }, }); diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts index 8fed97a74435d..6fed8b4aa4ee6 100644 --- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts +++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts @@ -6,7 +6,7 @@ import { SanitizedAlert, AlertInstanceSummary, AlertInstanceStatus } from '../types'; import { IEvent } from '../../../event_log/server'; -import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER } from '../plugin'; +import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin'; export interface AlertInstanceSummaryFromEventLogParams { alert: SanitizedAlert; @@ -80,7 +80,8 @@ export function alertInstanceSummaryFromEventLog( status.status = 'Active'; status.actionGroupId = event?.kibana?.alerting?.action_group_id; break; - case EVENT_LOG_ACTIONS.resolvedInstance: + case LEGACY_EVENT_LOG_ACTIONS.resolvedInstance: + case EVENT_LOG_ACTIONS.recoveredInstance: status.status = 'OK'; status.activeStartDate = undefined; status.actionGroupId = undefined; diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 4bfb44425544a..bafb89c64076b 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -82,9 +82,12 @@ export const EVENT_LOG_ACTIONS = { execute: 'execute', executeAction: 'execute-action', newInstance: 'new-instance', - resolvedInstance: 'resolved-instance', + recoveredInstance: 'recovered-instance', activeInstance: 'active-instance', }; +export const LEGACY_EVENT_LOG_ACTIONS = { + resolvedInstance: 'resolved-instance', +}; export interface PluginSetupContract { registerType: AlertTypeRegistry['register']; diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 07d08f5837d54..d4c4f746392c3 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -26,12 +26,12 @@ import { alertsMock, alertsClientMock } from '../mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { IEventLogger } from '../../../event_log/server'; import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; -import { Alert, ResolvedActionGroup } from '../../common'; +import { Alert, RecoveredActionGroup } from '../../common'; import { omit } from 'lodash'; const alertType = { id: 'test', name: 'My test alert', - actionGroups: [{ id: 'default', name: 'Default' }, ResolvedActionGroup], + actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup], defaultActionGroupId: 'default', executor: jest.fn(), producer: 'alerts', @@ -114,7 +114,7 @@ describe('Task Runner', () => { }, }, { - group: ResolvedActionGroup.id, + group: RecoveredActionGroup.id, id: '2', actionTypeId: 'action', params: { @@ -517,7 +517,7 @@ describe('Task Runner', () => { `); }); - test('fire resolved actions for execution for the alertInstances which is in the resolved state', async () => { + test('fire recovered actions for execution for the alertInstances which is in the recovered state', async () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); @@ -650,7 +650,7 @@ describe('Task Runner', () => { Array [ Object { "event": Object { - "action": "resolved-instance", + "action": "recovered-instance", }, "kibana": Object { "alerting": Object { @@ -666,7 +666,7 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' resolved instance: '2'", + "message": "test:1: 'alert-name' instance '2' has recovered", }, ], Array [ diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 24d96788c3395..5a7247ac50ea0 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -39,7 +39,7 @@ import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_l import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error'; import { AlertsClient } from '../alerts_client'; import { partiallyUpdateAlert } from '../saved_objects'; -import { ResolvedActionGroup } from '../../common'; +import { RecoveredActionGroup } from '../../common'; const FALLBACK_RETRY_INTERVAL = '5m'; @@ -219,7 +219,7 @@ export class TaskRunner { alertInstance.hasScheduledActions() ); - generateNewAndResolvedInstanceEvents({ + generateNewAndRecoveredInstanceEvents({ eventLogger, originalAlertInstances, currentAlertInstances: instancesWithScheduledActions, @@ -229,7 +229,7 @@ export class TaskRunner { }); if (!muteAll) { - scheduleActionsForResolvedInstances( + scheduleActionsForRecoveredInstances( alertInstances, executionHandler, originalAlertInstances, @@ -436,7 +436,7 @@ export class TaskRunner { } } -interface GenerateNewAndResolvedInstanceEventsParams { +interface GenerateNewAndRecoveredInstanceEventsParams { eventLogger: IEventLogger; originalAlertInstances: Dictionary; currentAlertInstances: Dictionary; @@ -445,18 +445,20 @@ interface GenerateNewAndResolvedInstanceEventsParams { namespace: string | undefined; } -function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInstanceEventsParams) { +function generateNewAndRecoveredInstanceEvents( + params: GenerateNewAndRecoveredInstanceEventsParams +) { const { eventLogger, alertId, namespace, currentAlertInstances, originalAlertInstances } = params; const originalAlertInstanceIds = Object.keys(originalAlertInstances); const currentAlertInstanceIds = Object.keys(currentAlertInstances); const newIds = without(currentAlertInstanceIds, ...originalAlertInstanceIds); - const resolvedIds = without(originalAlertInstanceIds, ...currentAlertInstanceIds); + const recoveredIds = without(originalAlertInstanceIds, ...currentAlertInstanceIds); - for (const id of resolvedIds) { + for (const id of recoveredIds) { const actionGroup = originalAlertInstances[id].getLastScheduledActions()?.group; - const message = `${params.alertLabel} resolved instance: '${id}'`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.resolvedInstance, message, actionGroup); + const message = `${params.alertLabel} instance '${id}' has recovered`; + logInstanceEvent(id, EVENT_LOG_ACTIONS.recoveredInstance, message, actionGroup); } for (const id of newIds) { @@ -496,7 +498,7 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst } } -function scheduleActionsForResolvedInstances( +function scheduleActionsForRecoveredInstances( alertInstancesMap: Record, executionHandler: ReturnType, originalAlertInstances: Record, @@ -505,22 +507,22 @@ function scheduleActionsForResolvedInstances( ) { const currentAlertInstanceIds = Object.keys(currentAlertInstances); const originalAlertInstanceIds = Object.keys(originalAlertInstances); - const resolvedIds = without( + const recoveredIds = without( originalAlertInstanceIds, ...currentAlertInstanceIds, ...mutedInstanceIds ); - for (const id of resolvedIds) { + for (const id of recoveredIds) { const instance = alertInstancesMap[id]; - instance.updateLastScheduledActions(ResolvedActionGroup.id); + instance.updateLastScheduledActions(RecoveredActionGroup.id); instance.unscheduleActions(); executionHandler({ - actionGroup: ResolvedActionGroup.id, + actionGroup: RecoveredActionGroup.id, context: {}, state: {}, alertInstanceId: id, }); - instance.scheduleActions(ResolvedActionGroup.id); + instance.scheduleActions(RecoveredActionGroup.id); } } diff --git a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.test.tsx b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.test.tsx index b90f606d276eb..399a24a8bc165 100644 --- a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.test.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react'; import { MissingJobsAlert } from './anomaly_detection_setup_link'; -import * as hooks from '../../hooks/useFetcher'; +import * as hooks from '../../hooks/use_fetcher'; async function renderTooltipAnchor({ jobs, diff --git a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx index e6fc80ed7c3b7..8a1d73c818944 100644 --- a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx @@ -16,14 +16,14 @@ import { getEnvironmentLabel, } from '../../../common/environment_filter_values'; import { getAPMHref } from '../../components/shared/Links/apm/APMLink'; -import { useApmPluginContext } from '../../hooks/useApmPluginContext'; -import { FETCH_STATUS, useFetcher } from '../../hooks/useFetcher'; -import { useLicense } from '../../hooks/useLicense'; -import { useUrlParams } from '../../hooks/useUrlParams'; +import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher'; +import { useLicenseContext } from '../../context/license/use_license_context'; +import { useUrlParams } from '../../context/url_params_context/use_url_params'; import { APIReturnType } from '../../services/rest/createCallApmApi'; import { units } from '../../style/variables'; -export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection'>; +export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection/jobs'>; const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false }; @@ -32,7 +32,7 @@ export function AnomalyDetectionSetupLink() { const environment = uiFilters.environment; const { core } = useApmPluginContext(); const canGetJobs = !!core.application.capabilities.ml?.canGetJobs; - const license = useLicense(); + const license = useLicenseContext(); const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); const { basePath } = core.http; @@ -57,7 +57,7 @@ export function AnomalyDetectionSetupLink() { export function MissingJobsAlert({ environment }: { environment?: string }) { const { data = DEFAULT_DATA, status } = useFetcher( (callApmApi) => - callApmApi({ endpoint: `GET /api/apm/settings/anomaly-detection` }), + callApmApi({ endpoint: `GET /api/apm/settings/anomaly-detection/jobs` }), [], { preservePreviousData: false, showToastOnError: false } ); diff --git a/x-pack/plugins/apm/public/application/action_menu/index.tsx b/x-pack/plugins/apm/public/application/action_menu/index.tsx index 1713ef61fac1e..438eb2bca7f24 100644 --- a/x-pack/plugins/apm/public/application/action_menu/index.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/index.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import { getAlertingCapabilities } from '../../components/alerting/get_alert_capabilities'; import { getAPMHref } from '../../components/shared/Links/apm/APMLink'; -import { useApmPluginContext } from '../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; import { AlertingPopoverAndFlyout } from './alerting_popover_flyout'; import { AnomalyDetectionSetupLink } from './anomaly_detection_setup_link'; diff --git a/x-pack/plugins/apm/public/application/application.test.tsx b/x-pack/plugins/apm/public/application/application.test.tsx index 75b7835c13151..c5091b1b554cc 100644 --- a/x-pack/plugins/apm/public/application/application.test.tsx +++ b/x-pack/plugins/apm/public/application/application.test.tsx @@ -8,7 +8,7 @@ import { act } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { Observable } from 'rxjs'; import { AppMountParameters, CoreStart, HttpSetup } from 'src/core/public'; -import { mockApmPluginContextValue } from '../context/ApmPluginContext/MockApmPluginContext'; +import { mockApmPluginContextValue } from '../context/apm_plugin/mock_apm_plugin_context'; import { ApmPluginSetupDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; import { renderApp } from './'; diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index 7fcbe7c518cd0..4d16643a83fe9 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -20,8 +20,8 @@ import { APMRouteDefinition } from '../application/routes'; import { renderAsRedirectTo } from '../components/app/Main/route_config'; import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; import { RumHome, UX_LABEL } from '../components/app/RumDashboard/RumHome'; -import { ApmPluginContext } from '../context/ApmPluginContext'; -import { UrlParamsProvider } from '../context/UrlParamsContext'; +import { ApmPluginContext } from '../context/apm_plugin/apm_plugin_context'; +import { UrlParamsProvider } from '../context/url_params_context/url_params_context'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { ConfigSchema } from '../index'; import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index 79c29867cb8e3..9c4413765a500 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -25,9 +25,9 @@ import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPat import { ApmPluginContext, ApmPluginContextValue, -} from '../context/ApmPluginContext'; -import { LicenseProvider } from '../context/LicenseContext'; -import { UrlParamsProvider } from '../context/UrlParamsContext'; +} from '../context/apm_plugin/apm_plugin_context'; +import { LicenseProvider } from '../context/license/license_context'; +import { UrlParamsProvider } from '../context/url_params_context/url_params_context'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { ApmPluginSetupDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; diff --git a/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx index 1a565ab8708bc..f4f2be0a6e889 100644 --- a/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx @@ -7,11 +7,11 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { ErrorCountAlertTrigger } from '.'; -import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, -} from '../../../context/ApmPluginContext/MockApmPluginContext'; +} from '../../../context/apm_plugin/mock_apm_plugin_context'; export default { title: 'app/ErrorCountAlertTrigger', diff --git a/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx index a465b90e7bf05..efa792ff44273 100644 --- a/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx @@ -10,8 +10,8 @@ import { useParams } from 'react-router-dom'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { useEnvironments } from '../../../hooks/useEnvironments'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { EnvironmentField, ServiceField, IsAboveField } from '../fields'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; @@ -34,7 +34,11 @@ export function ErrorCountAlertTrigger(props: Props) { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams } = useUrlParams(); const { start, end } = urlParams; - const { environmentOptions } = useEnvironments({ serviceName, start, end }); + const { environmentOptions } = useEnvironmentsFetcher({ + serviceName, + start, + end, + }); const defaults = { threshold: 25, diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx index d20aae29fb8ce..8b2d4e235ac25 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx @@ -8,12 +8,12 @@ import { cloneDeep, merge } from 'lodash'; import React, { ComponentType } from 'react'; import { MemoryRouter, Route } from 'react-router-dom'; import { TransactionDurationAlertTrigger } from '.'; -import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, -} from '../../../context/ApmPluginContext/MockApmPluginContext'; -import { MockUrlParamsContextProvider } from '../../../context/UrlParamsContext/MockUrlParamsContextProvider'; +} from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; export default { title: 'app/TransactionDurationAlertTrigger', diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx index b7220de8079c9..3566850aa24c4 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx @@ -10,8 +10,8 @@ import { map } from 'lodash'; import React from 'react'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; -import { useEnvironments } from '../../../hooks/useEnvironments'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; @@ -21,7 +21,7 @@ import { TransactionTypeField, IsAboveField, } from '../fields'; -import { useApmService } from '../../../hooks/use_apm_service'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; interface AlertParams { windowSize: number; @@ -63,10 +63,14 @@ interface Props { export function TransactionDurationAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const { transactionTypes } = useApmService(); + const { transactionTypes } = useApmServiceContext(); const { serviceName } = useParams<{ serviceName?: string }>(); const { start, end, transactionType } = urlParams; - const { environmentOptions } = useEnvironments({ serviceName, start, end }); + const { environmentOptions } = useEnvironmentsFetcher({ + serviceName, + start, + end, + }); if (!transactionTypes.length || !serviceName) { return null; diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx index e13ed6c1bcd6f..ff5939c601375 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx @@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { ANOMALY_SEVERITY } from '../../../../../ml/common'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; -import { useEnvironments } from '../../../hooks/useEnvironments'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; import { @@ -23,7 +23,7 @@ import { ServiceField, TransactionTypeField, } from '../fields'; -import { useApmService } from '../../../hooks/use_apm_service'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; interface Params { windowSize: number; @@ -47,10 +47,14 @@ interface Props { export function TransactionDurationAnomalyAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const { transactionTypes } = useApmService(); + const { transactionTypes } = useApmServiceContext(); const { serviceName } = useParams<{ serviceName?: string }>(); const { start, end, transactionType } = urlParams; - const { environmentOptions } = useEnvironments({ serviceName, start, end }); + const { environmentOptions } = useEnvironmentsFetcher({ + serviceName, + start, + end, + }); if (serviceName && !transactionTypes.length) { return null; diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx index 464409ed332e8..f723febde389d 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx @@ -7,8 +7,8 @@ import { useParams } from 'react-router-dom'; import React from 'react'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types'; -import { useEnvironments } from '../../../hooks/useEnvironments'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; @@ -18,7 +18,7 @@ import { EnvironmentField, IsAboveField, } from '../fields'; -import { useApmService } from '../../../hooks/use_apm_service'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; interface AlertParams { windowSize: number; @@ -38,10 +38,14 @@ interface Props { export function TransactionErrorRateAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const { transactionTypes } = useApmService(); + const { transactionTypes } = useApmServiceContext(); const { serviceName } = useParams<{ serviceName?: string }>(); const { start, end, transactionType } = urlParams; - const { environmentOptions } = useEnvironments({ serviceName, start, end }); + const { environmentOptions } = useEnvironmentsFetcher({ + serviceName, + start, + end, + }); if (serviceName && !transactionTypes.length) { return null; diff --git a/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx index 3ad71b52b6037..07ab89afd4108 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx @@ -17,8 +17,8 @@ import { import React, { useState } from 'react'; import { useParams } from 'react-router-dom'; import { EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType, callApmApi, diff --git a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx index 4364731501b89..30659cf3f9319 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx @@ -19,8 +19,8 @@ import React, { useState } from 'react'; import { useParams } from 'react-router-dom'; import { EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { getDurationFormatter } from '../../../../common/utils/formatters'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType, callApmApi, diff --git a/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx b/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx index b74517902f89b..350f64367b766 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx @@ -10,7 +10,7 @@ import { useHistory } from 'react-router-dom'; import { EuiBasicTable } from '@elastic/eui'; import { asPercent, asInteger } from '../../../../common/utils/formatters'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { createHref } from '../../shared/Links/url_helpers'; type CorrelationsApiResponse = diff --git a/x-pack/plugins/apm/public/components/app/Correlations/index.tsx b/x-pack/plugins/apm/public/components/app/Correlations/index.tsx index b0f6b83485e39..16a21e28fc08d 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/index.tsx @@ -19,10 +19,10 @@ import { } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { enableCorrelations } from '../../../../common/ui_settings_keys'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { LatencyCorrelations } from './LatencyCorrelations'; import { ErrorCorrelations } from './ErrorCorrelations'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { createHref } from '../../shared/Links/url_helpers'; export function Correlations() { diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index 643064b2f3176..c0ce2ed388a12 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -22,7 +22,7 @@ import { useHistory } from 'react-router-dom'; import styled from 'styled-components'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import type { IUrlParams } from '../../../../context/url_params_context/types'; import { px, unit, units } from '../../../../style/variables'; import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; import { DiscoverErrorLink } from '../../../shared/Links/DiscoverLinks/DiscoverErrorLink'; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 159f111bee04c..ab99c6ffa8da0 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -20,7 +20,7 @@ import d3 from 'd3'; import React from 'react'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters'; -import { useTheme } from '../../../../hooks/useTheme'; +import { useTheme } from '../../../../hooks/use_theme'; type ErrorDistributionAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/errors/distribution'>; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index dc97642dec357..95ebd5d4036de 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -21,14 +21,15 @@ import { RouteComponentProps } from 'react-router-dom'; import styled from 'styled-components'; import { useTrackPageview } from '../../../../../observability/public'; import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { callApmApi } from '../../../services/rest/createCallApmApi'; import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables'; import { ApmHeader } from '../../shared/ApmHeader'; import { SearchBar } from '../../shared/search_bar'; import { DetailView } from './DetailView'; import { ErrorDistribution } from './Distribution'; +import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher'; const Titles = styled.div` margin-bottom: ${px(units.plus)}; @@ -88,24 +89,10 @@ export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) { } }, [serviceName, start, end, groupId, uiFilters]); - const { data: errorDistributionData } = useFetcher(() => { - if (start && end) { - return callApmApi({ - endpoint: 'GET /api/apm/services/{serviceName}/errors/distribution', - params: { - path: { - serviceName, - }, - query: { - start, - end, - groupId, - uiFilters: JSON.stringify(uiFilters), - }, - }, - }); - } - }, [serviceName, start, end, groupId, uiFilters]); + const { errorDistributionData } = useErrorGroupDistributionFetcher({ + serviceName, + groupId, + }); useTrackPageview({ app: 'apm', path: 'error_group_details' }); useTrackPageview({ app: 'apm', path: 'error_group_details', delay: 15000 }); diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx index 84b72b62248b0..4022caedadaab 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx @@ -6,8 +6,8 @@ import { mount } from 'enzyme'; import React from 'react'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; -import { MockUrlParamsContextProvider } from '../../../../../context/UrlParamsContext/MockUrlParamsContextProvider'; +import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../../../context/url_params_context/mock_url_params_context_provider'; import { mockMoment, toJson } from '../../../../../utils/testHelpers'; import { ErrorGroupList } from '../index'; import props from './props.json'; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx index be1078ea860c3..200a5f467491b 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx @@ -19,7 +19,7 @@ import { truncate, unit, } from '../../../../style/variables'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { ManagedTable } from '../../../shared/ManagedTable'; import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; import { TimestampTooltip } from '../../../shared/TimestampTooltip'; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index e2a02a2f3e7ae..71cb8e0e01602 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -16,13 +16,14 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { callApmApi } from '../../../services/rest/createCallApmApi'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { SearchBar } from '../../shared/search_bar'; import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; import { ErrorGroupList } from './List'; +import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher'; interface ErrorGroupOverviewProps { serviceName: string; @@ -30,26 +31,11 @@ interface ErrorGroupOverviewProps { function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { const { urlParams, uiFilters } = useUrlParams(); - const { start, end, sortField, sortDirection } = urlParams; - - const { data: errorDistributionData } = useFetcher(() => { - if (start && end) { - return callApmApi({ - endpoint: 'GET /api/apm/services/{serviceName}/errors/distribution', - params: { - path: { - serviceName, - }, - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters), - }, - }, - }); - } - }, [serviceName, start, end, uiFilters]); + const { errorDistributionData } = useErrorGroupDistributionFetcher({ + serviceName, + groupId: undefined, + }); const { data: errorGroupListData } = useFetcher(() => { const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; diff --git a/x-pack/plugins/apm/public/components/app/Home/Home.test.tsx b/x-pack/plugins/apm/public/components/app/Home/Home.test.tsx index ab4ca1dfbb49d..148e0733b93ca 100644 --- a/x-pack/plugins/apm/public/components/app/Home/Home.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/Home.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import { Home } from '../Home'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; describe('Home component', () => { it('should render services', () => { diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index ce8f2b0ba611a..839c087305bd8 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; -import { ApmServiceContextProvider } from '../../../../context/apm_service_context'; +import { ApmServiceContextProvider } from '../../../../context/apm_service/apm_service_context'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n'; import { SERVICE_NODE_NAME_MISSING } from '../../../../../common/service_nodes'; import { APMRouteDefinition } from '../../../../application/routes'; diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx index ac1668a54ab95..10c8417223c77 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { useFetcher } from '../../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../../hooks/use_fetcher'; import { toQuery } from '../../../../shared/Links/url_helpers'; import { Settings } from '../../../Settings'; import { AgentConfigurationCreateEdit } from '../../../Settings/AgentConfigurations/AgentConfigurationCreateEdit'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx index 3787202f5dee4..6a56dbf40b33b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx @@ -27,7 +27,7 @@ import moment from 'moment'; import React from 'react'; import { useHistory } from 'react-router-dom'; import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { ChartWrapper } from '../ChartWrapper'; import { I18LABELS } from '../translations'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index 237d33a6a89a3..9fdb34935fee5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -14,7 +14,7 @@ import { EuiToolTip, EuiIconTip, } from '@elastic/eui'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { I18LABELS } from '../translations'; import { useUxQuery } from '../hooks/useUxQuery'; import { formatToSec } from '../UXMetrics/KeyUXMetrics'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx index 4c4f7110cafb9..2e6c5c8e23ee5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx @@ -16,8 +16,8 @@ import { } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { I18LABELS } from '../translations'; import { CsmSharedContext } from '../CsmSharedContext'; import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index 4b94b98704da7..d7bc94e6564f1 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -6,8 +6,8 @@ import React, { useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { I18LABELS } from '../translations'; import { BreakdownFilter } from '../Breakdowns/BreakdownFilter'; import { PageLoadDistChart } from '../Charts/PageLoadDistChart'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts index c3f4ab44179fe..5c545a63d6d05 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useFetcher } from '../../../../hooks/useFetcher'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useFetcher } from '../../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { PercentileRange } from './index'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index 84668f4b06d77..b339cc7774d75 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -6,8 +6,8 @@ import React, { useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { I18LABELS } from '../translations'; import { BreakdownFilter } from '../Breakdowns/BreakdownFilter'; import { PageViewsChart } from '../Charts/PageViewsChart'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx index 6c7e2e22a9893..8d759d80352d7 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx @@ -8,9 +8,9 @@ import React from 'react'; import { EuiFlexItem } from '@elastic/eui'; import { EnvironmentFilter } from '../../../shared/EnvironmentFilter'; import { ServiceNameFilter } from '../URLFilter/ServiceNameFilter'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { RUM_AGENT_NAMES } from '../../../../../common/agent_name'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { UserPercentile } from '../UserPercentile'; export function MainFilters() { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index e4e9109f007e7..c810bd3e7c489 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -20,7 +20,7 @@ import { PageLoadAndViews } from './Panels/PageLoadAndViews'; import { VisitorBreakdownsPanel } from './Panels/VisitorBreakdowns'; import { useBreakPoints } from './hooks/useBreakPoints'; import { getPercentileLabel } from './UXMetrics/translations'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; export function RumDashboard() { const { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx index b70621b1e4cbc..756014004cc9b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx @@ -8,7 +8,7 @@ import { EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; -import { useUrlParams } from '../../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx index abafdf089748b..a492938deffab 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { createMemoryHistory } from 'history'; -import * as fetcherHook from '../../../../../../hooks/useFetcher'; +import * as fetcherHook from '../../../../../../hooks/use_fetcher'; import { SelectableUrlList } from '../SelectableUrlList'; import { render } from '../../../utils/test_helper'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx index 67692a9a8554b..61f75a430706c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx @@ -8,8 +8,8 @@ import useDebounce from 'react-use/lib/useDebounce'; import React, { useEffect, useState, FormEvent, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import { EuiTitle } from '@elastic/eui'; -import { useUrlParams } from '../../../../../hooks/useUrlParams'; -import { useFetcher } from '../../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../../context/url_params_context/use_url_params'; +import { useFetcher } from '../../../../../hooks/use_fetcher'; import { I18LABELS } from '../../translations'; import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; import { formatToSec } from '../../UXMetrics/KeyUXMetrics'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx index ef829ebf7f0cf..655cdaaca933b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx @@ -10,9 +10,9 @@ import { useHistory } from 'react-router-dom'; import { omit } from 'lodash'; import { URLSearch } from './URLSearch'; import { UrlList } from './UrlList'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; -import { removeUndefinedProps } from '../../../../context/UrlParamsContext/helpers'; +import { removeUndefinedProps } from '../../../../context/url_params_context/helpers'; import { LocalUIFilterName } from '../../../../../common/ui_filter'; export function URLFilter() { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx index 2ded35deb58f2..690595caa6c0e 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx @@ -20,7 +20,7 @@ import { TBT_LABEL, TBT_TOOLTIP, } from './translations'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { useUxQuery } from '../hooks/useUxQuery'; import { UXMetrics } from '../../../../../../observability/public'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx index 3a6323a747a70..baa9cb7dd74f9 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { render } from '@testing-library/react'; -import * as fetcherHook from '../../../../../hooks/useFetcher'; +import * as fetcherHook from '../../../../../hooks/use_fetcher'; import { KeyUXMetrics } from '../KeyUXMetrics'; describe('KeyUXMetrics', () => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx index 95a42ce3018f1..392b42cba12e5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx @@ -15,11 +15,11 @@ import { } from '@elastic/eui'; import { I18LABELS } from '../translations'; import { KeyUXMetrics } from './KeyUXMetrics'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { useUxQuery } from '../hooks/useUxQuery'; import { CoreVitals } from '../../../../../../observability/public'; import { CsmSharedContext } from '../CsmSharedContext'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { getPercentileLabel } from './translations'; export function UXMetrics() { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx index 04c7e3cc00287..260c775c7129b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useEffect } from 'react'; import { EuiSelect } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { I18LABELS } from '../translations'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx index ce9485690b930..77d5697c31750 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui'; import { VisitorBreakdownChart } from '../Charts/VisitorBreakdownChart'; import { I18LABELS, VisitorBreakdownLabel } from '../translations'; -import { useFetcher } from '../../../../hooks/useFetcher'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useFetcher } from '../../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; export function VisitorBreakdown() { const { urlParams, uiFilters } = useUrlParams(); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx index 3a5c3d80ca7d1..eff03c58e9991 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx @@ -21,7 +21,7 @@ import { isErrorEmbeddable, } from '../../../../../../../../src/plugins/embeddable/public'; import { useLayerList } from './useLayerList'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { RenderTooltipContentParams } from '../../../../../../maps/public'; import { MapToolTip } from './MapToolTip'; import { useMapFilters } from './useMapFilters'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts index a1cdf7bb646e5..54bfa81f26add 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts @@ -22,7 +22,7 @@ import { } from '../../../../../../maps/common/constants'; import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { SERVICE_NAME, TRANSACTION_TYPE, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts index 774ac23d23196..c5cf081311f66 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts @@ -5,7 +5,7 @@ */ import { useMemo } from 'react'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FieldFilter as Filter } from '../../../../../../../../src/plugins/data/common'; import { CLIENT_GEO_COUNTRY_ISO_CODE, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useUxQuery.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useUxQuery.ts index 16396dc9fc15b..c8cd2c2c64da8 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useUxQuery.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useUxQuery.ts @@ -5,7 +5,7 @@ */ import { useMemo } from 'react'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; export function useUxQuery() { const { urlParams, uiFilters } = useUrlParams(); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx index 5522cad5690bc..d5b8cd83d437c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx @@ -13,7 +13,7 @@ import { Router } from 'react-router-dom'; import { MemoryHistory } from 'history'; import { EuiThemeProvider } from '../../../../../../observability/public'; import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; -import { UrlParamsProvider } from '../../../../context/UrlParamsContext'; +import { UrlParamsProvider } from '../../../../context/url_params_context/url_params_context'; export const core = ({ http: { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.test.tsx index 1187b71dff825..659f9f63d0cfa 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.test.tsx @@ -9,7 +9,7 @@ import { render } from '@testing-library/react'; import cytoscape from 'cytoscape'; import React, { ReactNode } from 'react'; import { ThemeContext } from 'styled-components'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { Controls } from './Controls'; import { CytoscapeContext } from './Cytoscape'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx index b4408e20c04d2..a23fa72314aed 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx @@ -8,9 +8,9 @@ import { EuiButtonIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { useTheme } from '../../../hooks/useTheme'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { useTheme } from '../../../hooks/use_theme'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; import { APMQueryParams } from '../../shared/Links/url_helpers'; import { CytoscapeContext } from './Cytoscape'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index 8a76c5f7bd8f1..1dea95d369966 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -16,7 +16,7 @@ import React, { useRef, useState, } from 'react'; -import { useTheme } from '../../../hooks/useTheme'; +import { useTheme } from '../../../hooks/use_theme'; import { getCytoscapeOptions } from './cytoscape_options'; import { useCytoscapeEventHandlers } from './use_cytoscape_event_handlers'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx index 63a9cf985959f..07e88294caadb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { CytoscapeContext } from './Cytoscape'; -import { useTheme } from '../../../hooks/useTheme'; +import { useTheme } from '../../../hooks/use_theme'; const EmptyBannerContainer = styled.div` margin: ${({ theme }) => theme.eui.gutterTypes.gutterSmall}; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx index 788e5f25b6310..36c0c9f37f9c7 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx @@ -18,7 +18,7 @@ import { getServiceHealthStatus, getServiceHealthStatusColor, } from '../../../../../common/service_health_status'; -import { useTheme } from '../../../../hooks/useTheme'; +import { useTheme } from '../../../../hooks/use_theme'; import { fontSize, px } from '../../../../style/variables'; import { asInteger, asDuration } from '../../../../../common/utils/formatters'; import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.test.tsx index f98a7a1b33dd8..d9f33b8fca4cd 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.test.tsx @@ -7,7 +7,7 @@ import React, { ReactNode } from 'react'; import { Buttons } from './Buttons'; import { render } from '@testing-library/react'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; function Wrapper({ children }: { children?: ReactNode }) { return {children}; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx index 8670cf623c253..56110a89ed888 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx @@ -9,8 +9,8 @@ import { EuiButton, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { MouseEvent } from 'react'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { getAPMHref } from '../../../shared/Links/apm/APMLink'; import { APMQueryParams } from '../../../shared/Links/url_helpers'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index 70eb5eaf8e576..313b262508c61 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -8,8 +8,8 @@ import cytoscape from 'cytoscape'; import { HttpSetup } from 'kibana/public'; import React, { ComponentType } from 'react'; import { EuiThemeProvider } from '../../../../../../observability/public'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; -import { MockUrlParamsContextProvider } from '../../../../context/UrlParamsContext/MockUrlParamsContextProvider'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider'; import { createCallApmApi } from '../../../../services/rest/createCallApmApi'; import { CytoscapeContext } from '../Cytoscape'; import { Popover } from './'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx index be8c5cf8cd435..3b737c6fa4170 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx @@ -15,8 +15,8 @@ import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import { ServiceNodeStats } from '../../../../../common/service_map'; import { ServiceStatsList } from './ServiceStatsList'; -import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { AnomalyDetection } from './AnomalyDetection'; import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx index 7b7e3b46bb317..036d02531f794 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx @@ -15,7 +15,7 @@ import React, { } from 'react'; import { EuiPopover } from '@elastic/eui'; import cytoscape from 'cytoscape'; -import { useTheme } from '../../../../hooks/useTheme'; +import { useTheme } from '../../../../hooks/use_theme'; import { SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; import { CytoscapeContext } from '../Cytoscape'; import { getAnimationOptions } from '../cytoscape_options'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts index d8a8a3c8e9ab4..e2a54f6048682 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts @@ -15,7 +15,7 @@ import { getServiceHealthStatusColor, ServiceHealthStatus, } from '../../../../common/service_health_status'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { defaultIcon, iconForNode } from './icons'; export const popoverWidth = 280; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/empty_banner.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/empty_banner.test.tsx index ae27d4d3baf75..a1fb7e7077add 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/empty_banner.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/empty_banner.test.tsx @@ -7,7 +7,7 @@ import { act, waitFor } from '@testing-library/react'; import cytoscape from 'cytoscape'; import React, { ReactNode } from 'react'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { renderWithTheme } from '../../../utils/testHelpers'; import { CytoscapeContext } from './Cytoscape'; import { EmptyBanner } from './EmptyBanner'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx index 2a5b4ce44ff46..97e507d7cc871 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx @@ -11,9 +11,9 @@ import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; import { License } from '../../../../../licensing/common/license'; import { EuiThemeProvider } from '../../../../../observability/public'; import { FETCH_STATUS } from '../../../../../observability/public/hooks/use_fetcher'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; -import { LicenseContext } from '../../../context/LicenseContext'; -import * as useFetcherModule from '../../../hooks/useFetcher'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { LicenseContext } from '../../../context/license/license_context'; +import * as useFetcherModule from '../../../hooks/use_fetcher'; import { ServiceMap } from './'; const KibanaReactContext = createKibanaReactContext({ diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 1731d3f9430d4..48a7f8f77ab84 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -13,10 +13,10 @@ import { isActivePlatinumLicense, SERVICE_MAP_TIMEOUT_ERROR, } from '../../../../common/service_map'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; -import { useLicense } from '../../../hooks/useLicense'; -import { useTheme } from '../../../hooks/useTheme'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useLicenseContext } from '../../../context/license/use_license_context'; +import { useTheme } from '../../../hooks/use_theme'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { callApmApi } from '../../../services/rest/createCallApmApi'; import { DatePicker } from '../../shared/DatePicker'; import { LicensePrompt } from '../../shared/LicensePrompt'; @@ -70,7 +70,7 @@ export function ServiceMap({ serviceName, }: PropsWithChildren) { const theme = useTheme(); - const license = useLicense(); + const license = useLicenseContext(); const { urlParams } = useUrlParams(); const { data = { elements: [] }, status, error } = useFetcher(() => { diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx index 74e7b652d0ebe..c4c227deb6918 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -21,8 +21,8 @@ import { asInteger, asPercent, } from '../../../../common/utils/formatters'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { px, truncate, unit } from '../../../style/variables'; import { ServiceNodeMetricOverviewLink } from '../../shared/Links/apm/ServiceNodeMetricOverviewLink'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx index 7c0869afe0cd1..18067a43861bd 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx @@ -21,7 +21,7 @@ import { omitAllOption, getOptionLabel, } from '../../../../../../../common/agent_configuration/all_option'; -import { useFetcher, FETCH_STATUS } from '../../../../../../hooks/useFetcher'; +import { useFetcher, FETCH_STATUS } from '../../../../../../hooks/use_fetcher'; import { FormRowSelect } from './FormRowSelect'; import { APMLink } from '../../../../../shared/Links/apm/APMLink'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx index 54440559070ad..7e1146596dd87 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx @@ -32,8 +32,8 @@ import { validateSetting, } from '../../../../../../../common/agent_configuration/setting_definitions'; import { AgentName } from '../../../../../../../typings/es_schemas/ui/fields/agent'; -import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; -import { FETCH_STATUS } from '../../../../../../hooks/useFetcher'; +import { useApmPluginContext } from '../../../../../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS } from '../../../../../../hooks/use_fetcher'; import { saveConfig } from './saveConfig'; import { SettingFormRow } from './SettingFormRow'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx index db3f2c374a1ae..5ca643428e49c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx @@ -14,13 +14,13 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; import { HttpSetup } from 'kibana/public'; import { AgentConfiguration } from '../../../../../../common/agent_configuration/configuration_types'; -import { FETCH_STATUS } from '../../../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../../../hooks/use_fetcher'; import { createCallApmApi } from '../../../../../services/rest/createCallApmApi'; import { AgentConfigurationCreateEdit } from './index'; import { ApmPluginContext, ApmPluginContextValue, -} from '../../../../../context/ApmPluginContext'; +} from '../../../../../context/apm_plugin/apm_plugin_context'; import { EuiThemeProvider } from '../../../../../../../observability/public'; storiesOf( diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx index 4f94f255a4e4c..998175c895557 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx @@ -14,7 +14,7 @@ import { AgentConfiguration, AgentConfigurationIntake, } from '../../../../../../common/agent_configuration/configuration_types'; -import { FetcherResult } from '../../../../../hooks/useFetcher'; +import { FetcherResult } from '../../../../../hooks/use_fetcher'; import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; import { ServicePage } from './ServicePage/ServicePage'; import { SettingsPage } from './SettingsPage/SettingsPage'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx index adae50db85ada..958aafa8159df 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx @@ -13,7 +13,7 @@ import { APIReturnType, callApmApi, } from '../../../../../services/rest/createCallApmApi'; -import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>[0]; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx index 81079d78a148a..be4edbe2ea270 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx @@ -18,9 +18,9 @@ import React, { useState } from 'react'; import { useLocation } from 'react-router-dom'; import { APIReturnType } from '../../../../../services/rest/createCallApmApi'; import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option'; -import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; -import { FETCH_STATUS } from '../../../../../hooks/useFetcher'; -import { useTheme } from '../../../../../hooks/useTheme'; +import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS } from '../../../../../hooks/use_fetcher'; +import { useTheme } from '../../../../../hooks/use_theme'; import { px, units } from '../../../../../style/variables'; import { createAgentConfigurationHref, diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx index 12c63f8702f25..c408d5e960cf3 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -16,8 +16,8 @@ import { isEmpty } from 'lodash'; import React from 'react'; import { useLocation } from 'react-router-dom'; import { useTrackPageview } from '../../../../../../observability/public'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { createAgentConfigurationHref } from '../../../shared/Links/apm/agentConfigurationLinks'; import { AgentConfigurationList } from './List'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx index 53794ca9965ff..2adf85181886c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx @@ -7,8 +7,8 @@ import { render } from '@testing-library/react'; import React from 'react'; import { ApmIndices } from '.'; -import * as hooks from '../../../../hooks/useFetcher'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import * as hooks from '../../../../hooks/use_fetcher'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; describe('ApmIndices', () => { it('should not get stuck in infinite loop', () => { diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx index a1ef9ddd87271..5a5d20cde9ade 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx @@ -19,10 +19,10 @@ import { EuiButton, EuiButtonEmpty, } from '@elastic/eui'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; import { clearCache } from '../../../../services/rest/callApi'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; const APM_INDEX_LABELS = [ { diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.tsx index 5014584c3928a..ffcb85384642a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.tsx @@ -10,7 +10,7 @@ import { NotificationsStart } from 'kibana/public'; import React, { useState } from 'react'; import { px, unit } from '../../../../../../style/variables'; import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; -import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../../../context/apm_plugin/use_apm_plugin_context'; interface Props { onDelete: () => void; diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx index c6566af3a8b61..f9c5aa17e411a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx @@ -15,7 +15,7 @@ import { import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { Filter } from '../../../../../../../common/custom_link/custom_link_types'; -import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../../../context/apm_plugin/use_apm_plugin_context'; import { FiltersSection } from './FiltersSection'; import { FlyoutFooter } from './FlyoutFooter'; import { LinkSection } from './LinkSection'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx index 96a634828f669..1da7d415b5660 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx @@ -14,15 +14,15 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import * as apmApi from '../../../../../services/rest/createCallApmApi'; import { License } from '../../../../../../../licensing/common/license'; -import * as hooks from '../../../../../hooks/useFetcher'; -import { LicenseContext } from '../../../../../context/LicenseContext'; +import * as hooks from '../../../../../hooks/use_fetcher'; +import { LicenseContext } from '../../../../../context/license/license_context'; import { CustomLinkOverview } from '.'; import { expectTextsInDocument, expectTextsNotInDocument, } from '../../../../../utils/testHelpers'; import * as saveCustomLink from './CreateEditCustomLinkFlyout/saveCustomLink'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; const data = [ { diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index 771a8c6154dc0..6b5c7d583ee8a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -17,8 +17,8 @@ import { isEmpty } from 'lodash'; import React, { useEffect, useState } from 'react'; import { INVALID_LICENSE } from '../../../../../../common/custom_link'; import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; -import { FETCH_STATUS, useFetcher } from '../../../../../hooks/useFetcher'; -import { useLicense } from '../../../../../hooks/useLicense'; +import { FETCH_STATUS, useFetcher } from '../../../../../hooks/use_fetcher'; +import { useLicenseContext } from '../../../../../context/license/use_license_context'; import { LicensePrompt } from '../../../../shared/LicensePrompt'; import { CreateCustomLinkButton } from './CreateCustomLinkButton'; import { CreateEditCustomLinkFlyout } from './CreateEditCustomLinkFlyout'; @@ -26,7 +26,7 @@ import { CustomLinkTable } from './CustomLinkTable'; import { EmptyPrompt } from './EmptyPrompt'; export function CustomLinkOverview() { - const license = useLicense(); + const license = useLicenseContext(); const hasValidLicense = license?.isActive && license?.hasAtLeast('gold'); const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); diff --git a/x-pack/plugins/apm/public/components/app/Settings/Settings.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/Settings.test.tsx index 21da12477b024..cfef7ca937f66 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/Settings.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/Settings.test.tsx @@ -5,7 +5,7 @@ */ import { render } from '@testing-library/react'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import React, { ReactNode } from 'react'; import { Settings } from './'; import { createMemoryHistory } from 'history'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx index ccc1778e9fbde..e709c7e104472 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx @@ -21,8 +21,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ML_ERRORS } from '../../../../../common/anomaly_detection'; -import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { createJobs } from './create_jobs'; import { getEnvironmentLabel } from '../../../../../common/environment_filter_values'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index 2cda5fcf85909..addfd64a9ef62 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -9,15 +9,15 @@ import { EuiTitle, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiPanel, EuiEmptyPrompt } from '@elastic/eui'; import { ML_ERRORS } from '../../../../../common/anomaly_detection'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { JobsList } from './jobs_list'; import { AddEnvironments } from './add_environments'; -import { useFetcher } from '../../../../hooks/useFetcher'; +import { useFetcher } from '../../../../hooks/use_fetcher'; import { LicensePrompt } from '../../../shared/LicensePrompt'; -import { useLicense } from '../../../../hooks/useLicense'; +import { useLicenseContext } from '../../../../context/license/use_license_context'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection'>; +export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection/jobs'>; const DEFAULT_VALUE: AnomalyDetectionApiResponse = { jobs: [], @@ -27,7 +27,7 @@ const DEFAULT_VALUE: AnomalyDetectionApiResponse = { export function AnomalyDetection() { const plugin = useApmPluginContext(); const canGetJobs = !!plugin.core.application.capabilities.ml?.canGetJobs; - const license = useLicense(); + const license = useLicenseContext(); const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); const [viewAddEnvironments, setViewAddEnvironments] = useState(false); @@ -36,7 +36,7 @@ export function AnomalyDetection() { (callApmApi) => { if (canGetJobs) { return callApmApi({ - endpoint: `GET /api/apm/settings/anomaly-detection`, + endpoint: `GET /api/apm/settings/anomaly-detection/jobs`, }); } }, diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 137dcfcdbb4f0..8d6a0740a8a08 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx index 1844e5754cfba..9e21eb2ffc870 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx @@ -7,7 +7,7 @@ import { EuiCallOut, EuiButton } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useMlHref } from '../../../../../../ml/public'; export function LegacyJobsCallout() { diff --git a/x-pack/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index c9c577285ee80..e974f05fbe994 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -16,7 +16,7 @@ import React, { ReactNode } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { HeaderMenuPortal } from '../../../../../observability/public'; import { ActionMenu } from '../../../application/action_menu'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; import { HomeLink } from '../../shared/Links/apm/HomeLink'; diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx index 1a41ffe1f606f..3f325f17af82d 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx @@ -8,8 +8,8 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; import styled from 'styled-components'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { getRedirectToTransactionDetailPageUrl } from './get_redirect_to_transaction_detail_page_url'; import { getRedirectToTracePageUrl } from './get_redirect_to_trace_page_url'; @@ -27,7 +27,7 @@ export function TraceLink({ match }: RouteComponentProps<{ traceId: string }>) { (callApmApi) => { if (traceId) { return callApmApi({ - endpoint: 'GET /api/apm/transaction/{traceId}', + endpoint: 'GET /api/apm/traces/{traceId}/root_transaction', params: { path: { traceId, diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx index e7c0400290dcb..c07e00ef387c9 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx @@ -8,13 +8,13 @@ import { shallow } from 'enzyme'; import React, { ReactNode } from 'react'; import { MemoryRouter, RouteComponentProps } from 'react-router-dom'; import { TraceLink } from './'; -import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, -} from '../../../context/ApmPluginContext/MockApmPluginContext'; -import * as hooks from '../../../hooks/useFetcher'; -import * as urlParamsHooks from '../../../hooks/useUrlParams'; +} from '../../../context/apm_plugin/mock_apm_plugin_context'; +import * as hooks from '../../../hooks/use_fetcher'; +import * as urlParamsHooks from '../../../context/url_params_context/use_url_params'; function Wrapper({ children }: { children?: ReactNode }) { return ( diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx index cbab2c44132f3..ab10d6b4f46a0 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -8,8 +8,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel } from '@elastic/eui'; import React, { useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { SearchBar } from '../../shared/search_bar'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index 003f2ed05b09e..bebd5bdabbae3 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -27,8 +27,8 @@ import { ValuesType } from 'utility-types'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { useTheme } from '../../../../../../observability/public'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import type { IUrlParams } from '../../../../context/url_params_context/types'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { unit } from '../../../../style/variables'; import { ChartContainer } from '../../../shared/charts/chart_container'; import { EmptyMessage } from '../../../shared/EmptyMessage'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx index 48413d6207ee3..43732c23aea64 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx @@ -10,7 +10,7 @@ import { Location } from 'history'; import React from 'react'; import { useHistory } from 'react-router-dom'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import type { IUrlParams } from '../../../../context/url_params_context/types'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { TransactionMetadata } from '../../../shared/MetadataTable/TransactionMetadata'; import { WaterfallContainer } from './WaterfallContainer'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx index e3ba02ce42c2e..5217c2abb11dc 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx @@ -9,7 +9,7 @@ import { MemoryRouter } from 'react-router-dom'; import { EuiThemeProvider } from '../../../../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TraceAPIResponse } from '../../../../../../server/lib/traces/get_trace'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { WaterfallContainer } from './index'; import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; import { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx index 501ca6d33d5af..806350679df55 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx @@ -6,7 +6,7 @@ import { Location } from 'history'; import React from 'react'; -import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; +import { IUrlParams } from '../../../../../context/url_params_context/types'; import { ServiceLegends } from './ServiceLegends'; import { IWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; import { Waterfall } from './Waterfall'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts index f78fe39120d8d..0f6bebd9037cc 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts @@ -5,7 +5,7 @@ */ import { Location } from 'history'; -import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; +import { IUrlParams } from '../../../../../context/url_params_context/types'; export const location = { pathname: '/services/opbeans-go/transactions/view', diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx index c9420dbb81cb9..d90fe393c94a4 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -18,7 +18,7 @@ import { Location } from 'history'; import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import type { IUrlParams } from '../../../../context/url_params_context/types'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { TransactionSummary } from '../../../shared/Summary/TransactionSummary'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index 8f335ddc71c72..c491b9f0e1eff 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -17,19 +17,19 @@ import React, { useMemo } from 'react'; import { isEmpty, flatten } from 'lodash'; import { useHistory } from 'react-router-dom'; import { RouteComponentProps } from 'react-router-dom'; -import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; -import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution'; -import { useWaterfall } from '../../../hooks/useWaterfall'; +import { useTransactionChartsFetcher } from '../../../hooks/use_transaction_charts_fetcher'; +import { useTransactionDistributionFetcher } from '../../../hooks/use_transaction_distribution_fetcher'; +import { useWaterfallFetcher } from './use_waterfall_fetcher'; import { ApmHeader } from '../../shared/ApmHeader'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { TransactionDistribution } from './Distribution'; import { WaterfallWithSummmary } from './WaterfallWithSummmary'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; -import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event_context'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { HeightRetainer } from '../../shared/HeightRetainer'; import { Correlations } from '../Correlations'; @@ -50,18 +50,20 @@ export function TransactionDetails({ const { urlParams } = useUrlParams(); const history = useHistory(); const { - data: distributionData, - status: distributionStatus, - } = useTransactionDistribution(urlParams); + distributionData, + distributionStatus, + } = useTransactionDistributionFetcher(); const { - data: transactionChartsData, - status: transactionChartsStatus, - } = useTransactionCharts(); + transactionChartsData, + transactionChartsStatus, + } = useTransactionChartsFetcher(); - const { waterfall, exceedsMax, status: waterfallStatus } = useWaterfall( - urlParams - ); + const { + waterfall, + exceedsMax, + status: waterfallStatus, + } = useWaterfallFetcher(); const { transactionName, transactionType } = urlParams; useTrackPageview({ app: 'apm', path: 'transaction_details' }); diff --git a/x-pack/plugins/apm/public/hooks/useWaterfall.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/use_waterfall_fetcher.ts similarity index 75% rename from x-pack/plugins/apm/public/hooks/useWaterfall.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/use_waterfall_fetcher.ts index 6264ec45088a2..7458fa79bd1f3 100644 --- a/x-pack/plugins/apm/public/hooks/useWaterfall.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/use_waterfall_fetcher.ts @@ -5,9 +5,9 @@ */ import { useMemo } from 'react'; -import { IUrlParams } from '../context/UrlParamsContext/types'; -import { useFetcher } from './useFetcher'; -import { getWaterfall } from '../components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { getWaterfall } from './WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; const INITIAL_DATA = { root: undefined, @@ -15,7 +15,8 @@ const INITIAL_DATA = { errorsPerTransaction: {}, }; -export function useWaterfall(urlParams: IUrlParams) { +export function useWaterfallFetcher() { + const { urlParams } = useUrlParams(); const { traceId, start, end, transactionId } = urlParams; const { data = INITIAL_DATA, status, error } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx index 003bd6ba4c122..ae0dd85b6a8b5 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React, { ReactNode } from 'react'; import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name'; import { enableServiceOverview } from '../../../../common/ui_settings_keys'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useErrorOverviewHref } from '../../shared/Links/apm/ErrorOverviewLink'; import { useMetricOverviewHref } from '../../shared/Links/apm/MetricOverviewLink'; import { useServiceMapHref } from '../../shared/Links/apm/ServiceMapLink'; @@ -23,7 +23,7 @@ import { ServiceMetrics } from '../service_metrics'; import { ServiceNodeOverview } from '../ServiceNodeOverview'; import { ServiceOverview } from '../service_overview'; import { TransactionOverview } from '../transaction_overview'; -import { useApmService } from '../../../hooks/use_apm_service'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; interface Tab { key: string; @@ -44,7 +44,7 @@ interface Props { } export function ServiceDetailTabs({ serviceName, tab }: Props) { - const { agentName } = useApmService(); + const { agentName } = useApmServiceContext(); const { uiSettings } = useApmPluginContext().core; const overviewTab = { diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/HealthBadge.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/HealthBadge.tsx index e8ad3e65b1a47..3b97849b07790 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/HealthBadge.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/HealthBadge.tsx @@ -10,7 +10,7 @@ import { getServiceHealthStatusLabel, ServiceHealthStatus, } from '../../../../../common/service_health_status'; -import { useTheme } from '../../../../hooks/useTheme'; +import { useTheme } from '../../../../hooks/use_theme'; export function HealthBadge({ healthStatus, diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx index 39cb73d2a0dd9..1c6fa9fe0447e 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx @@ -7,7 +7,7 @@ import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { ServiceHealthStatus } from '../../../../../common/service_health_status'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { mockMoment, renderWithTheme } from '../../../../utils/testHelpers'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { ServiceList, SERVICE_COLUMNS } from './'; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 3c84b3982642d..b1d725bba0ca9 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -17,17 +17,17 @@ import url from 'url'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; -import { useAnomalyDetectionJobs } from '../../../hooks/useAnomalyDetectionJobs'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { SearchBar } from '../../shared/search_bar'; import { Correlations } from '../Correlations'; import { NoServicesMessage } from './no_services_message'; import { ServiceList } from './ServiceList'; import { MLCallout } from './ServiceList/MLCallout'; +import { useAnomalyDetectionJobsFetcher } from './use_anomaly_detection_jobs_fetcher'; const initialData = { items: [], @@ -37,12 +37,10 @@ const initialData = { let hasDisplayedToast = false; -export function ServiceInventory() { +function useServicesFetcher() { + const { urlParams, uiFilters } = useUrlParams(); const { core } = useApmPluginContext(); - const { - urlParams: { start, end }, - uiFilters, - } = useUrlParams(); + const { start, end } = urlParams; const { data = initialData, status } = useFetcher( (callApmApi) => { if (start && end) { @@ -92,6 +90,13 @@ export function ServiceInventory() { } }, [data.hasLegacyData, core.http.basePath, core.notifications.toasts]); + return { servicesData: data, servicesStatus: status }; +} + +export function ServiceInventory() { + const { core } = useApmPluginContext(); + const { servicesData, servicesStatus } = useServicesFetcher(); + // The page is called "service inventory" to avoid confusion with the // "service overview", but this is tracked in some dashboards because it's the // initial landing page for APM, so it stays as "services_overview" (plural.) @@ -110,9 +115,9 @@ export function ServiceInventory() { ); const { - data: anomalyDetectionJobsData, - status: anomalyDetectionJobsStatus, - } = useAnomalyDetectionJobs(); + anomalyDetectionJobsData, + anomalyDetectionJobsStatus, + } = useAnomalyDetectionJobsFetcher(); const [userHasDismissedCallout, setUserHasDismissedCallout] = useLocalStorage( 'apm.userHasDismissedServiceInventoryMlCallout', @@ -148,11 +153,11 @@ export function ServiceInventory() { } /> diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.test.tsx index 0fc2a2b4cdcef..cf1ccfbd36aaf 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.test.tsx @@ -6,8 +6,8 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { NoServicesMessage } from './no_services_message'; function Wrapper({ children }: { children?: ReactNode }) { diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.tsx index d2763c6632c65..b20efc440312c 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { KibanaLink } from '../../shared/Links/KibanaLink'; import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink'; import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { ErrorStatePrompt } from '../../shared/ErrorStatePrompt'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx index de5e92664a769..1c838a01d05c7 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx @@ -13,17 +13,17 @@ import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; import { ServiceHealthStatus } from '../../../../common/service_health_status'; import { ServiceInventory } from '.'; import { EuiThemeProvider } from '../../../../../observability/public'; -import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, -} from '../../../context/ApmPluginContext/MockApmPluginContext'; -import * as useAnomalyDetectionJobs from '../../../hooks/useAnomalyDetectionJobs'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +} from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import * as useLocalUIFilters from '../../../hooks/useLocalUIFilters'; -import * as useDynamicIndexPatternHooks from '../../../hooks/useDynamicIndexPattern'; +import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_pattern'; import { SessionStorageMock } from '../../../services/__test__/SessionStorageMock'; -import { MockUrlParamsContextProvider } from '../../../context/UrlParamsContext/MockUrlParamsContextProvider'; +import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; +import * as hook from './use_anomaly_detection_jobs_fetcher'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiStats: () => {} }, @@ -80,19 +80,13 @@ describe('ServiceInventory', () => { status: FETCH_STATUS.SUCCESS, }); - jest - .spyOn(useAnomalyDetectionJobs, 'useAnomalyDetectionJobs') - .mockReturnValue({ - status: FETCH_STATUS.SUCCESS, - data: { - jobs: [], - hasLegacyJobs: false, - }, - refetch: () => undefined, - }); + jest.spyOn(hook, 'useAnomalyDetectionJobsFetcher').mockReturnValue({ + anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS, + anomalyDetectionJobsData: { jobs: [], hasLegacyJobs: false }, + }); jest - .spyOn(useDynamicIndexPatternHooks, 'useDynamicIndexPattern') + .spyOn(useDynamicIndexPatternHooks, 'useDynamicIndexPatternFetcher') .mockReturnValue({ indexPattern: undefined, status: FETCH_STATUS.SUCCESS, diff --git a/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts b/x-pack/plugins/apm/public/components/app/service_inventory/use_anomaly_detection_jobs_fetcher.ts similarity index 50% rename from x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts rename to x-pack/plugins/apm/public/components/app/service_inventory/use_anomaly_detection_jobs_fetcher.ts index 5bb36720e7b9b..901841ac4d593 100644 --- a/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts +++ b/x-pack/plugins/apm/public/components/app/service_inventory/use_anomaly_detection_jobs_fetcher.ts @@ -3,16 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { useFetcher } from '../../../hooks/use_fetcher'; -import { useFetcher } from './useFetcher'; - -export function useAnomalyDetectionJobs() { - return useFetcher( +export function useAnomalyDetectionJobsFetcher() { + const { data, status } = useFetcher( (callApmApi) => - callApmApi({ - endpoint: `GET /api/apm/settings/anomaly-detection`, - }), + callApmApi({ endpoint: `GET /api/apm/settings/anomaly-detection/jobs` }), [], { showToastOnError: false } ); + + return { anomalyDetectionJobsData: data, anomalyDetectionJobsStatus: status }; } diff --git a/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx index d0f8fc1e61332..bf99f5c87fa6a 100644 --- a/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx @@ -13,10 +13,10 @@ import { EuiFlexGroup, } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; +import { useServiceMetricChartsFetcher } from '../../../hooks/use_service_metric_charts_fetcher'; import { MetricsChart } from '../../shared/charts/metrics_chart'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { SearchBar } from '../../shared/search_bar'; @@ -31,7 +31,9 @@ export function ServiceMetrics({ serviceName, }: ServiceMetricsProps) { const { urlParams } = useUrlParams(); - const { data, status } = useServiceMetricCharts(urlParams, agentName); + const { data, status } = useServiceMetricChartsFetcher({ + serviceNodeName: undefined, + }); const { start, end } = urlParams; const localFiltersConfig: React.ComponentProps< diff --git a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.test.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.test.tsx index c6f7e68e4f4d0..0ba45fae15fef 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ServiceNodeMetrics } from '.'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { RouteComponentProps } from 'react-router-dom'; describe('ServiceNodeMetrics', () => { diff --git a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx index 11de40b47ff86..aa1d9cccbdfa6 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx @@ -22,11 +22,11 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import styled from 'styled-components'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; -import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event_context'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; -import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useApmService } from '../../../hooks/use_apm_service'; +import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useServiceMetricChartsFetcher } from '../../../hooks/use_service_metric_charts_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { px, truncate, unit } from '../../../style/variables'; import { ApmHeader } from '../../shared/ApmHeader'; import { MetricsChart } from '../../shared/charts/metrics_chart'; @@ -58,12 +58,8 @@ type ServiceNodeMetricsProps = RouteComponentProps<{ export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) { const { urlParams, uiFilters } = useUrlParams(); const { serviceName, serviceNodeName } = match.params; - const { agentName } = useApmService(); - const { data } = useServiceMetricCharts( - urlParams, - agentName, - serviceNodeName - ); + const { agentName } = useApmServiceContext(); + const { data } = useServiceMetricChartsFetcher({ serviceNodeName }); const { start, end } = urlParams; const { data: { host, containerId } = INITIAL_DATA, status } = useFetcher( diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 15125128d9781..dcb407d27e690 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { isRumAgentName } from '../../../../common/agent_name'; -import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event_context'; +import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { TransactionBreakdownChart } from '../../shared/charts/transaction_breakdown_chart'; import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart'; import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index b364f027538a6..949f5cce0a64f 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -8,17 +8,17 @@ import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { CoreStart } from 'src/core/public'; import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; -import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, -} from '../../../context/ApmPluginContext/MockApmPluginContext'; -import { MockUrlParamsContextProvider } from '../../../context/UrlParamsContext/MockUrlParamsContextProvider'; -import * as useDynamicIndexPatternHooks from '../../../hooks/useDynamicIndexPattern'; -import * as useFetcherHooks from '../../../hooks/useFetcher'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; -import * as useAnnotationsHooks from '../../../hooks/use_annotations'; -import * as useTransactionBreakdownHooks from '../../../hooks/use_transaction_breakdown'; +} from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; +import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_pattern'; +import * as useFetcherHooks from '../../../hooks/use_fetcher'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import * as useAnnotationsHooks from '../../../context/annotations/use_annotations_context'; +import * as useTransactionBreakdownHooks from '../../shared/charts/transaction_breakdown_chart/use_transaction_breakdown'; import { renderWithTheme } from '../../../utils/testHelpers'; import { ServiceOverview } from './'; @@ -56,10 +56,10 @@ function Wrapper({ children }: { children?: ReactNode }) { describe('ServiceOverview', () => { it('renders', () => { jest - .spyOn(useAnnotationsHooks, 'useAnnotations') + .spyOn(useAnnotationsHooks, 'useAnnotationsContext') .mockReturnValue({ annotations: [] }); jest - .spyOn(useDynamicIndexPatternHooks, 'useDynamicIndexPattern') + .spyOn(useDynamicIndexPatternHooks, 'useDynamicIndexPatternFetcher') .mockReturnValue({ indexPattern: undefined, status: FETCH_STATUS.SUCCESS, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index b4228878dd9f5..6e183924a80a7 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -14,8 +14,8 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import styled from 'styled-components'; import { asInteger } from '../../../../../common/utils/formatters'; -import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; import { px, truncate, unit } from '../../../../style/variables'; import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx index 94d92bfbe89dd..1662f44d1e421 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx @@ -9,10 +9,10 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { useParams } from 'react-router-dom'; import { asTransactionRate } from '../../../../common/utils/formatters'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { useTheme } from '../../../hooks/useTheme'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useApmService } from '../../../hooks/use_apm_service'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { useTheme } from '../../../hooks/use_theme'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { callApmApi } from '../../../services/rest/createCallApmApi'; import { TimeseriesChart } from '../../shared/charts/timeseries_chart'; @@ -24,7 +24,7 @@ export function ServiceOverviewThroughputChart({ const theme = useTheme(); const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams, uiFilters } = useUrlParams(); - const { transactionType } = useApmService(); + const { transactionType } = useApmServiceContext(); const { start, end } = urlParams; const { data, status } = useFetcher(() => { diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx index e241bc2fed05a..6b02a44dcc2f4 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx @@ -21,8 +21,8 @@ import { asTransactionRate, } from '../../../../../common/utils/formatters'; import { px, truncate, unit } from '../../../../style/variables'; -import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APIReturnType, callApmApi, diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/TransactionList.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/TransactionList.stories.tsx index 953397b9f3d5f..c14c31afe0445 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/TransactionList.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/TransactionList.stories.tsx @@ -7,7 +7,7 @@ import React, { ComponentType } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { TransactionList } from './'; type TransactionGroup = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups'>['items'][0]; diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 28a27c034265a..9ff4ad916b174 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -23,10 +23,10 @@ import { useLocation } from 'react-router-dom'; import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; -import { IUrlParams } from '../../../context/UrlParamsContext/types'; -import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; -import { useTransactionList } from '../../../hooks/useTransactionList'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { IUrlParams } from '../../../context/url_params_context/types'; +import { useTransactionChartsFetcher } from '../../../hooks/use_transaction_charts_fetcher'; +import { useTransactionListFetcher } from './use_transaction_list'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; @@ -37,7 +37,7 @@ import { Correlations } from '../Correlations'; import { TransactionList } from './TransactionList'; import { useRedirect } from './useRedirect'; import { UserExperienceCallout } from './user_experience_callout'; -import { useApmService } from '../../../hooks/use_apm_service'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; function getRedirectLocation({ location, @@ -68,22 +68,22 @@ interface TransactionOverviewProps { export function TransactionOverview({ serviceName }: TransactionOverviewProps) { const location = useLocation(); const { urlParams } = useUrlParams(); - const { transactionType, transactionTypes } = useApmService(); + const { transactionType, transactionTypes } = useApmServiceContext(); // redirect to first transaction type useRedirect(getRedirectLocation({ location, transactionType, urlParams })); const { - data: transactionCharts, - status: transactionChartsStatus, - } = useTransactionCharts(); + transactionChartsData, + transactionChartsStatus, + } = useTransactionChartsFetcher(); useTrackPageview({ app: 'apm', path: 'transaction_overview' }); useTrackPageview({ app: 'apm', path: 'transaction_overview', delay: 15000 }); const { - data: transactionListData, - status: transactionListStatus, - } = useTransactionList(urlParams); + transactionListData, + transactionListStatus, + } = useTransactionListFetcher(); const localFiltersConfig: React.ComponentProps< typeof LocalUIFilters @@ -134,7 +134,7 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { )} diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx index d4a8b3a46991c..93d56ea19024e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx @@ -10,13 +10,13 @@ import { CoreStart } from 'kibana/public'; import React from 'react'; import { Router } from 'react-router-dom'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; -import { ApmServiceContextProvider } from '../../../context/apm_service_context'; -import { UrlParamsProvider } from '../../../context/UrlParamsContext'; -import { IUrlParams } from '../../../context/UrlParamsContext/types'; -import * as useFetcherHook from '../../../hooks/useFetcher'; -import * as useServiceTransactionTypesHook from '../../../hooks/use_service_transaction_types'; -import * as useServiceAgentNameHook from '../../../hooks/use_service_agent_name'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { ApmServiceContextProvider } from '../../../context/apm_service/apm_service_context'; +import { UrlParamsProvider } from '../../../context/url_params_context/url_params_context'; +import { IUrlParams } from '../../../context/url_params_context/types'; +import * as useFetcherHook from '../../../hooks/use_fetcher'; +import * as useServiceTransactionTypesHook from '../../../context/apm_service/use_service_transaction_types_fetcher'; +import * as useServiceAgentNameHook from '../../../context/apm_service/use_service_agent_name_fetcher'; import { disableConsoleWarning, renderWithTheme, @@ -46,15 +46,17 @@ function setup({ // mock transaction types jest - .spyOn(useServiceTransactionTypesHook, 'useServiceTransactionTypes') + .spyOn(useServiceTransactionTypesHook, 'useServiceTransactionTypesFetcher') .mockReturnValue(serviceTransactionTypes); // mock agent - jest.spyOn(useServiceAgentNameHook, 'useServiceAgentName').mockReturnValue({ - agentName: 'nodejs', - error: undefined, - status: useFetcherHook.FETCH_STATUS.SUCCESS, - }); + jest + .spyOn(useServiceAgentNameHook, 'useServiceAgentNameFetcher') + .mockReturnValue({ + agentName: 'nodejs', + error: undefined, + status: useFetcherHook.FETCH_STATUS.SUCCESS, + }); jest.spyOn(useFetcherHook, 'useFetcher').mockReturnValue({} as any); diff --git a/x-pack/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts similarity index 75% rename from x-pack/plugins/apm/public/hooks/useTransactionList.ts rename to x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts index 92b54beb715db..78883ec2cf0d3 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionList.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts @@ -5,10 +5,9 @@ */ import { useParams } from 'react-router-dom'; -import { useUiFilters } from '../context/UrlParamsContext'; -import { IUrlParams } from '../context/UrlParamsContext/types'; -import { APIReturnType } from '../services/rest/createCallApmApi'; -import { useFetcher } from './useFetcher'; +import { APIReturnType } from '../../../services/rest/createCallApmApi'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; type TransactionsAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups'>; @@ -18,10 +17,10 @@ const DEFAULT_RESPONSE: Partial = { bucketSize: 0, }; -export function useTransactionList(urlParams: IUrlParams) { +export function useTransactionListFetcher() { + const { urlParams, uiFilters } = useUrlParams(); const { serviceName } = useParams<{ serviceName?: string }>(); const { transactionType, start, end } = urlParams; - const uiFilters = useUiFilters(urlParams); const { data = DEFAULT_RESPONSE, error, status } = useFetcher( (callApmApi) => { if (serviceName && start && end && transactionType) { @@ -43,8 +42,8 @@ export function useTransactionList(urlParams: IUrlParams) { ); return { - data, - status, - error, + transactionListData: data, + transactionListStatus: status, + transactionListError: error, }; } diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/user_experience_callout.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/user_experience_callout.tsx index 41e84d4acfba5..6e1154a458d6e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/user_experience_callout.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/user_experience_callout.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiButton, EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; export function UserExperienceCallout() { const { core } = useApmPluginContext(); diff --git a/x-pack/plugins/apm/public/components/shared/ApmHeader/apm_header.stories.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/apm_header.stories.tsx index 56501d8c916f4..dd88b1ea7eb73 100644 --- a/x-pack/plugins/apm/public/components/shared/ApmHeader/apm_header.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/ApmHeader/apm_header.stories.tsx @@ -8,8 +8,8 @@ import { EuiTitle } from '@elastic/eui'; import React, { ComponentType } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { HttpSetup } from '../../../../../../../src/core/public'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; -import { MockUrlParamsContextProvider } from '../../../context/UrlParamsContext/MockUrlParamsContextProvider'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; import { createCallApmApi } from '../../../services/rest/createCallApmApi'; import { ApmHeader } from './'; diff --git a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx index a806a3ea60154..04e03cda6a61e 100644 --- a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx @@ -9,7 +9,7 @@ import React, { ReactNode } from 'react'; import styled from 'styled-components'; import { HeaderMenuPortal } from '../../../../../observability/public'; import { ActionMenu } from '../../../application/action_menu'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { EnvironmentFilter } from '../EnvironmentFilter'; const HeaderFlexGroup = styled(EuiFlexGroup)` diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/date_picker.test.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/date_picker.test.tsx index 520cc2f423ddd..222c27cc7ed6d 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/date_picker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/date_picker.test.tsx @@ -10,12 +10,12 @@ import { mount } from 'enzyme'; import { createMemoryHistory } from 'history'; import React, { ReactNode } from 'react'; import { Router } from 'react-router-dom'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { UrlParamsContext, useUiFilters, -} from '../../../context/UrlParamsContext'; -import { IUrlParams } from '../../../context/UrlParamsContext/types'; +} from '../../../context/url_params_context/url_params_context'; +import { IUrlParams } from '../../../context/url_params_context/types'; import { DatePicker } from './'; const history = createMemoryHistory(); diff --git a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx index f35cc06748911..f847ce0b6e96f 100644 --- a/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx @@ -8,8 +8,8 @@ import { EuiSuperDatePicker } from '@elastic/eui'; import React, { useEffect } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { clearCache } from '../../../services/rest/callApi'; import { fromQuery, toQuery } from '../Links/url_helpers'; import { TimePickerQuickRange, TimePickerTimeDefaults } from './typings'; diff --git a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx index cace4c2770f37..4522cfa7195fd 100644 --- a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx @@ -13,8 +13,8 @@ import { ENVIRONMENT_ALL, ENVIRONMENT_NOT_DEFINED, } from '../../../../common/environment_filter_values'; -import { useEnvironments } from '../../../hooks/useEnvironments'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../Links/url_helpers'; function updateEnvironmentUrl( @@ -67,7 +67,7 @@ export function EnvironmentFilter() { const { environment } = uiFilters; const { start, end } = urlParams; - const { environments, status = 'loading' } = useEnvironments({ + const { environments, status = 'loading' } = useEnvironmentsFetcher({ serviceName, start, end, diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts index e7dd03db6b63c..2276704edc342 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts @@ -13,7 +13,7 @@ import { TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; import { UIProcessorEvent } from '../../../../common/processor_event'; -import { IUrlParams } from '../../../context/UrlParamsContext/types'; +import { IUrlParams } from '../../../context/url_params_context/types'; export function getBoolFilter({ groupId, diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index 2ef93fc32200e..5284e3f6aa011 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -14,9 +14,9 @@ import { IIndexPattern, QuerySuggestion, } from '../../../../../../../src/plugins/data/public'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { useDynamicIndexPatternFetcher } from '../../../hooks/use_dynamic_index_pattern'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../Links/url_helpers'; import { getBoolFilter } from './get_bool_filter'; // @ts-expect-error @@ -65,7 +65,7 @@ export function KueryBar() { const example = examples[processorEvent || 'defaults']; - const { indexPattern } = useDynamicIndexPattern(processorEvent); + const { indexPattern } = useDynamicIndexPatternFetcher(processorEvent); const placeholder = i18n.translate('xpack.apm.kueryBar.placeholder', { defaultMessage: `Search {event, select, diff --git a/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx b/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx index 1819e71a49753..bd68e7db77714 100644 --- a/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx @@ -9,7 +9,7 @@ import { LicensePrompt } from '.'; import { ApmPluginContext, ApmPluginContextValue, -} from '../../../context/ApmPluginContext'; +} from '../../../context/apm_plugin/apm_plugin_context'; const contextMock = ({ core: { http: { basePath: { prepend: () => {} } } }, diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx index 93b5672aa54f9..70286655bba88 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx @@ -12,7 +12,7 @@ import { useLocation } from 'react-router-dom'; import rison, { RisonValue } from 'rison-node'; import url from 'url'; import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { getTimepickerRisonData } from '../rison_helpers'; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx index 8c2829a515f83..e2447cc7a67a5 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx @@ -6,7 +6,7 @@ import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; import React from 'react'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; // union type constisting of valid guide sections that we link to type DocsSection = '/apm/get-started' | '/x-pack' | '/apm/server' | '/kibana'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx index 630235e54c9fa..6d4bbbbfc2f80 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx @@ -9,7 +9,7 @@ import { IBasePath } from 'kibana/public'; import React from 'react'; import url from 'url'; import { InfraAppId } from '../../../../../infra/public'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { fromQuery } from './url_helpers'; interface InfraQueryParams { diff --git a/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx index 8aa0d4f5a3354..ab44374f48167 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx @@ -7,7 +7,7 @@ import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; import React from 'react'; import url from 'url'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; interface Props extends EuiLinkAnchorProps { path?: string; diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx index 5fbcd475cb47b..7bf017fb239e3 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx @@ -6,9 +6,9 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useMlHref, ML_PAGES } from '../../../../../../ml/public'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; interface MlRisonData { ml?: { diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts index 0f671fd363c75..eabef034bf3d9 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useMlHref } from '../../../../../../ml/public'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; export function useTimeSeriesExplorerHref({ jobId, diff --git a/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx index 0ff73d91d7c5b..68bee36dbe283 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx @@ -7,7 +7,7 @@ import { EuiButton, EuiButtonEmpty, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; const SETUP_INSTRUCTIONS_LABEL = i18n.translate( 'xpack.apm.setupInstructionsButtonLabel', diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx index 41c932bf9c9f5..98046193e3807 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx @@ -11,8 +11,8 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import url from 'url'; import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams, fromQuery, toQuery } from '../url_helpers'; interface Props extends EuiLinkAnchorProps { diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx index 30b91fe2564f1..dcf21de7dca8d 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx index fbae80203f03b..de7130e878608 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx index 2553ec4353194..afdb177e467d8 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { pickKeys } from '../../../../../common/utils/pick_keys'; interface Props extends APMLinkExtendProps { diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx index 0a9553bcbfe6c..c107b436717c2 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { pickKeys } from '../../../../../common/utils/pick_keys'; import { APMQueryParams } from '../url_helpers'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx index 6aa362707800f..caa1498e6df87 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx @@ -11,7 +11,7 @@ */ import React from 'react'; import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx index c9b26b557512c..ee798e0208c2b 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { pickKeys } from '../../../../../common/utils/pick_keys'; interface Props extends APMLinkExtendProps { diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx index 23e795b026d0c..92ff1b8a68ac0 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { pickKeys } from '../../../../../common/utils/pick_keys'; import { APMQueryParams } from '../url_helpers'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx index 039d9dcb1c0ed..318a1590be77c 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/service_inventory_link.tsx @@ -11,7 +11,7 @@ */ import React from 'react'; import { pickKeys } from '../../../../../common/utils/pick_keys'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx index e6d266091ae52..43f7b089a2965 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx @@ -13,7 +13,7 @@ import { import { i18n } from '@kbn/i18n'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../../Links/url_helpers'; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx index 9db563a0f6ba8..6f62fd24e71ea 100644 --- a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx @@ -8,7 +8,7 @@ import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { orderBy } from 'lodash'; import React, { ReactNode, useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { fromQuery, toQuery } from '../Links/url_helpers'; // TODO: this should really be imported from EUI diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx index e95122f54aff1..8f44d98cecdf7 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx @@ -9,7 +9,7 @@ import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { ErrorMetadata } from '..'; import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, expectTextsNotInDocument, diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx index 1f10d923e351e..c97e506187347 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx @@ -9,7 +9,7 @@ import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { SpanMetadata } from '..'; import { Span } from '../../../../../../typings/es_schemas/ui/span'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, expectTextsNotInDocument, diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx index 8359716fc6966..4080a300ba17f 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx @@ -9,7 +9,7 @@ import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { TransactionMetadata } from '..'; import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, expectTextsNotInDocument, diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx index 8e53aa4aa1089..8a4cd588c8260 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx @@ -8,7 +8,7 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { MetadataTable } from '..'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument } from '../../../../utils/testHelpers'; import { SectionsWithRows } from '../helper'; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx index 1d2ac4d18a2a7..283433fa37bf9 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx @@ -17,7 +17,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React, { useCallback } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { HeightRetainer } from '../HeightRetainer'; import { fromQuery, toQuery } from '../Links/url_helpers'; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx index ed33c59af36f4..83c2acb57e3c7 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { EuiBadge } from '@elastic/eui'; -import { useTheme } from '../../../hooks/useTheme'; +import { useTheme } from '../../../hooks/use_theme'; import { px } from '../../../../public/style/variables'; import { units } from '../../../style/variables'; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx index 0241167aba1fb..777200099976e 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/CustomLinkToolbar.test.tsx @@ -7,7 +7,7 @@ import { act, fireEvent, render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, expectTextsNotInDocument, diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx index db7a284f6adff..c4547595645a2 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx @@ -10,8 +10,8 @@ import { MemoryRouter } from 'react-router-dom'; import { CustomLinkMenuSection } from '.'; import { CustomLink as CustomLinkType } from '../../../../../common/custom_link/custom_link_types'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; -import * as useFetcher from '../../../../hooks/useFetcher'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import * as useFetcher from '../../../../hooks/use_fetcher'; import { expectTextsInDocument, expectTextsNotInDocument, diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx index 2825363b10197..0a67db0f15b32 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx @@ -22,7 +22,7 @@ import { import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { CustomLinkList } from './CustomLinkList'; import { CustomLinkToolbar } from './CustomLinkToolbar'; -import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { LoadingStatePrompt } from '../../LoadingStatePrompt'; import { px } from '../../../../style/variables'; import { CreateEditCustomLinkFlyout } from '../../../app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout'; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index 15a85113406e1..3f74b80bab064 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -18,9 +18,9 @@ import { SectionTitle, } from '../../../../../observability/public'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { useLicense } from '../../../hooks/useLicense'; -import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { useLicenseContext } from '../../../context/license/use_license_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { CustomLinkMenuSection } from './CustomLinkMenuSection'; import { getSections } from './sections'; @@ -39,7 +39,7 @@ function ActionMenuButton({ onClick }: { onClick: () => void }) { } export function TransactionActionMenu({ transaction }: Props) { - const license = useLicense(); + const license = useLicenseContext(); const hasGoldLicense = license?.isActive && license?.hasAtLeast('gold'); const { core } = useApmPluginContext(); diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index 9b5f00f76eeb2..8cb863c8fc385 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { License } from '../../../../../../licensing/common/license'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; -import { LicenseContext } from '../../../../context/LicenseContext'; -import * as hooks from '../../../../hooks/useFetcher'; +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import { LicenseContext } from '../../../../context/license/license_context'; +import * as hooks from '../../../../hooks/use_fetcher'; import * as apmApi from '../../../../services/rest/createCallApmApi'; import { expectTextsInDocument, diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts index 4433865b44991..c77de875dc84f 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts @@ -10,7 +10,7 @@ import { isEmpty, pickBy } from 'lodash'; import moment from 'moment'; import url from 'url'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; -import { IUrlParams } from '../../../context/UrlParamsContext/types'; +import { IUrlParams } from '../../../context/url_params_context/types'; import { getDiscoverHref } from '../Links/DiscoverLinks/DiscoverLink'; import { getDiscoverQuery } from '../Links/DiscoverLinks/DiscoverTransactionLink'; import { getInfraHref } from '../Links/InfraLink'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx index 1a2a90c9fb3c3..eebb9e8d23d98 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx @@ -6,7 +6,7 @@ import React from 'react'; import styled from 'styled-components'; -import { useTheme } from '../../../../hooks/useTheme'; +import { useTheme } from '../../../../hooks/use_theme'; import { fontSizes, px, units } from '../../../../style/variables'; export enum Shape { diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx index 37d3664e98acd..a6b46f4a64691 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiToolTip } from '@elastic/eui'; import styled from 'styled-components'; import { asDuration } from '../../../../../../common/utils/formatters'; -import { useTheme } from '../../../../../hooks/useTheme'; +import { useTheme } from '../../../../../hooks/use_theme'; import { px, units } from '../../../../../style/variables'; import { Legend } from '../../Legend'; import { AgentMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx index abe81185635b5..e69b23cf5f008 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx @@ -8,7 +8,7 @@ import { fireEvent } from '@testing-library/react'; import { act } from '@testing-library/react-hooks'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument, renderWithTheme, diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx index de63e2323ddac..c6847bd5e674d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx @@ -8,12 +8,12 @@ import React, { useState } from 'react'; import { EuiPopover, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { asDuration } from '../../../../../../common/utils/formatters'; -import { useTheme } from '../../../../../hooks/useTheme'; +import { useTheme } from '../../../../../hooks/use_theme'; import { TRACE_ID, TRANSACTION_ID, } from '../../../../../../common/elasticsearch_fieldnames'; -import { useUrlParams } from '../../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../../context/url_params_context/use_url_params'; import { px, unit, units } from '../../../../../style/variables'; import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks'; import { ErrorDetailLink } from '../../../Links/apm/ErrorDetailLink'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx index cb5a44432dcbc..dcdfee22e3cfc 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx @@ -9,7 +9,7 @@ import { inRange } from 'lodash'; import { Sticky } from 'react-sticky'; import { XAxis, XYPlot } from 'react-vis'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; -import { useTheme } from '../../../../hooks/useTheme'; +import { useTheme } from '../../../../hooks/use_theme'; import { px } from '../../../../style/variables'; import { Mark } from './'; import { LastTickValue } from './LastTickValue'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx index 5ea2e4cfedf18..ee1c899123994 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { VerticalGridLines, XYPlot } from 'react-vis'; -import { useTheme } from '../../../../hooks/useTheme'; +import { useTheme } from '../../../../hooks/use_theme'; import { Mark } from '../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks'; import { PlotValues } from './plotUtils'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx index c0e8f869ce647..359eadfc55cff 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx @@ -5,7 +5,7 @@ */ import { render } from '@testing-library/react'; import React from 'react'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { ChartContainer } from './chart_container'; describe('ChartContainer', () => { diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx index b4486f1e9b94a..ef58430e1e31e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx @@ -7,7 +7,7 @@ import { EuiLoadingChart, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; interface Props { hasData: boolean; diff --git a/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx index 9a561571df5a7..506c27051b511 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx @@ -15,7 +15,7 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform_metrics_chart'; import { Maybe } from '../../../../../typings/common'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; import { TimeseriesChart } from '../timeseries_chart'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx index 3819ed30d104a..3bfcba63685b6 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; import { px, unit } from '../../../../../style/variables'; -import { useTheme } from '../../../../../hooks/useTheme'; +import { useTheme } from '../../../../../hooks/use_theme'; import { SparkPlot } from '../'; type Color = diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx index a857707ca0c75..689f80e01247e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx @@ -29,11 +29,11 @@ import { useHistory } from 'react-router-dom'; import { useChartTheme } from '../../../../../observability/public'; import { asAbsoluteDateTime } from '../../../../common/utils/formatters'; import { RectCoordinate, TimeSeries } from '../../../../typings/timeseries'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; -import { useTheme } from '../../../hooks/useTheme'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useAnnotations } from '../../../hooks/use_annotations'; -import { useChartPointerEvent } from '../../../hooks/use_chart_pointer_event'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { useTheme } from '../../../hooks/use_theme'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useAnnotationsContext } from '../../../context/annotations/use_annotations_context'; +import { useChartPointerEventContext } from '../../../context/chart_pointer_event/use_chart_pointer_event_context'; import { AnomalySeries } from '../../../selectors/chart_selectors'; import { unit } from '../../../style/variables'; import { ChartContainer } from './chart_container'; @@ -72,9 +72,9 @@ export function TimeseriesChart({ }: Props) { const history = useHistory(); const chartRef = React.createRef(); - const { annotations } = useAnnotations(); + const { annotations } = useAnnotationsContext(); const chartTheme = useChartTheme(); - const { pointerEvent, setPointerEvent } = useChartPointerEvent(); + const { pointerEvent, setPointerEvent } = useChartPointerEventContext(); const { urlParams } = useUrlParams(); const theme = useTheme(); diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx index 4d9a1637bea76..38a980fbcd90a 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useTransactionBreakdown } from '../../../../hooks/use_transaction_breakdown'; +import { useTransactionBreakdown } from './use_transaction_breakdown'; import { TransactionBreakdownChartContents } from './transaction_breakdown_chart_contents'; export function TransactionBreakdownChart({ diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx index 20056a6831adf..0eda922519f85 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx @@ -28,11 +28,11 @@ import { asPercent, } from '../../../../../common/utils/formatters'; import { TimeSeries } from '../../../../../typings/timeseries'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { useTheme } from '../../../../hooks/useTheme'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { useAnnotations } from '../../../../hooks/use_annotations'; -import { useChartPointerEvent } from '../../../../hooks/use_chart_pointer_event'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { useTheme } from '../../../../hooks/use_theme'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useAnnotationsContext } from '../../../../context/annotations/use_annotations_context'; +import { useChartPointerEventContext } from '../../../../context/chart_pointer_event/use_chart_pointer_event_context'; import { unit } from '../../../../style/variables'; import { ChartContainer } from '../../charts/chart_container'; import { onBrushEnd } from '../../charts/helper/helper'; @@ -52,9 +52,9 @@ export function TransactionBreakdownChartContents({ }: Props) { const history = useHistory(); const chartRef = React.createRef(); - const { annotations } = useAnnotations(); + const { annotations } = useAnnotationsContext(); const chartTheme = useChartTheme(); - const { pointerEvent, setPointerEvent } = useChartPointerEvent(); + const { pointerEvent, setPointerEvent } = useChartPointerEventContext(); const { urlParams } = useUrlParams(); const theme = useTheme(); const { start, end } = urlParams; diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_breakdown.ts b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts similarity index 80% rename from x-pack/plugins/apm/public/hooks/use_transaction_breakdown.ts rename to x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts index f1671ed7aa6d9..ff744d763ecae 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_breakdown.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts @@ -5,15 +5,15 @@ */ import { useParams } from 'react-router-dom'; -import { useFetcher } from './useFetcher'; -import { useUrlParams } from './useUrlParams'; -import { useApmService } from './use_apm_service'; +import { useFetcher } from '../../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; export function useTransactionBreakdown() { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams, uiFilters } = useUrlParams(); const { start, end, transactionName } = urlParams; - const { transactionType } = useApmService(); + const { transactionType } = useApmServiceContext(); const { data = { timeseries: undefined }, error, status } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx index ac117bbbd922a..bb7c0a9104fc7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx @@ -20,11 +20,11 @@ import { TRANSACTION_ROUTE_CHANGE, } from '../../../../../common/transaction_types'; import { asTransactionRate } from '../../../../../common/utils/formatters'; -import { AnnotationsContextProvider } from '../../../../context/annotations_context'; -import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event_context'; -import { LicenseContext } from '../../../../context/LicenseContext'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { AnnotationsContextProvider } from '../../../../context/annotations/annotations_context'; +import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event/chart_pointer_event_context'; +import { LicenseContext } from '../../../../context/license/license_context'; +import type { IUrlParams } from '../../../../context/url_params_context/types'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { ITransactionChartData } from '../../../../selectors/chart_selectors'; import { TimeseriesChart } from '../timeseries_chart'; import { TransactionBreakdownChart } from '../transaction_breakdown_chart'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx index ee5cd8960d335..f0569ea1a0752 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx @@ -10,7 +10,7 @@ import { isEmpty } from 'lodash'; import React from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink'; interface Props { diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 00472df95c4b1..06a5e7baef79b 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -9,9 +9,9 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { useParams } from 'react-router-dom'; import { asPercent } from '../../../../../common/utils/formatters'; -import { useFetcher } from '../../../../hooks/useFetcher'; -import { useTheme } from '../../../../hooks/useTheme'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useFetcher } from '../../../../hooks/use_fetcher'; +import { useTheme } from '../../../../hooks/use_theme'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; import { TimeseriesChart } from '../timeseries_chart'; diff --git a/x-pack/plugins/apm/public/components/shared/table_fetch_wrapper/index.tsx b/x-pack/plugins/apm/public/components/shared/table_fetch_wrapper/index.tsx index e8d62cd8bd85b..9538d46960fc9 100644 --- a/x-pack/plugins/apm/public/components/shared/table_fetch_wrapper/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/table_fetch_wrapper/index.tsx @@ -5,7 +5,7 @@ */ import React, { ReactNode } from 'react'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { ErrorStatePrompt } from '../ErrorStatePrompt'; export function TableFetchWrapper({ diff --git a/x-pack/plugins/apm/public/context/annotations_context.tsx b/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx similarity index 83% rename from x-pack/plugins/apm/public/context/annotations_context.tsx rename to x-pack/plugins/apm/public/context/annotations/annotations_context.tsx index 4e09a3d227b11..77285f976d850 100644 --- a/x-pack/plugins/apm/public/context/annotations_context.tsx +++ b/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx @@ -6,10 +6,10 @@ import React, { createContext } from 'react'; import { useParams } from 'react-router-dom'; -import { Annotation } from '../../common/annotations'; -import { useFetcher } from '../hooks/useFetcher'; -import { useUrlParams } from '../hooks/useUrlParams'; -import { callApmApi } from '../services/rest/createCallApmApi'; +import { Annotation } from '../../../common/annotations'; +import { useFetcher } from '../../hooks/use_fetcher'; +import { useUrlParams } from '../url_params_context/use_url_params'; +import { callApmApi } from '../../services/rest/createCallApmApi'; export const AnnotationsContext = createContext({ annotations: [] } as { annotations: Annotation[]; diff --git a/x-pack/plugins/apm/public/hooks/use_annotations.ts b/x-pack/plugins/apm/public/context/annotations/use_annotations_context.ts similarity index 80% rename from x-pack/plugins/apm/public/hooks/use_annotations.ts rename to x-pack/plugins/apm/public/context/annotations/use_annotations_context.ts index 1cd9a7e65dda2..7fdc602b1916e 100644 --- a/x-pack/plugins/apm/public/hooks/use_annotations.ts +++ b/x-pack/plugins/apm/public/context/annotations/use_annotations_context.ts @@ -5,9 +5,9 @@ */ import { useContext } from 'react'; -import { AnnotationsContext } from '../context/annotations_context'; +import { AnnotationsContext } from './annotations_context'; -export function useAnnotations() { +export function useAnnotationsContext() { const context = useContext(AnnotationsContext); if (!context) { diff --git a/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx b/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx similarity index 94% rename from x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx rename to x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx index 44952e64db59c..0e26db4820ea1 100644 --- a/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx @@ -6,7 +6,7 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import { createContext } from 'react'; -import { ConfigSchema } from '../../'; +import { ConfigSchema } from '../..'; import { ApmPluginSetupDeps } from '../../plugin'; export interface ApmPluginContextValue { diff --git a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx similarity index 97% rename from x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx rename to x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index 25e7f23a00125..7ab46c65c90d9 100644 --- a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -5,7 +5,7 @@ */ import React, { ReactNode } from 'react'; import { Observable, of } from 'rxjs'; -import { ApmPluginContext, ApmPluginContextValue } from '.'; +import { ApmPluginContext, ApmPluginContextValue } from './apm_plugin_context'; import { ConfigSchema } from '../..'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; import { createCallApmApi } from '../../services/rest/createCallApmApi'; diff --git a/x-pack/plugins/apm/public/hooks/useApmPluginContext.ts b/x-pack/plugins/apm/public/context/apm_plugin/use_apm_plugin_context.ts similarity index 84% rename from x-pack/plugins/apm/public/hooks/useApmPluginContext.ts rename to x-pack/plugins/apm/public/context/apm_plugin/use_apm_plugin_context.ts index 80a04edbca858..7c480ea3da275 100644 --- a/x-pack/plugins/apm/public/hooks/useApmPluginContext.ts +++ b/x-pack/plugins/apm/public/context/apm_plugin/use_apm_plugin_context.ts @@ -5,7 +5,7 @@ */ import { useContext } from 'react'; -import { ApmPluginContext } from '../context/ApmPluginContext'; +import { ApmPluginContext } from './apm_plugin_context'; export function useApmPluginContext() { return useContext(ApmPluginContext); diff --git a/x-pack/plugins/apm/public/context/apm_service_context.test.tsx b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/context/apm_service_context.test.tsx rename to x-pack/plugins/apm/public/context/apm_service/apm_service_context.test.tsx diff --git a/x-pack/plugins/apm/public/context/apm_service_context.tsx b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx similarity index 75% rename from x-pack/plugins/apm/public/context/apm_service_context.tsx rename to x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx index 2f1b33dea5aa6..b07763aed7b00 100644 --- a/x-pack/plugins/apm/public/context/apm_service_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx @@ -5,15 +5,15 @@ */ import React, { createContext, ReactNode } from 'react'; -import { isRumAgentName } from '../../common/agent_name'; +import { isRumAgentName } from '../../../common/agent_name'; import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, -} from '../../common/transaction_types'; -import { useServiceTransactionTypes } from '../hooks/use_service_transaction_types'; -import { useUrlParams } from '../hooks/useUrlParams'; -import { useServiceAgentName } from '../hooks/use_service_agent_name'; -import { IUrlParams } from './UrlParamsContext/types'; +} from '../../../common/transaction_types'; +import { useServiceTransactionTypesFetcher } from './use_service_transaction_types_fetcher'; +import { useUrlParams } from '../url_params_context/use_url_params'; +import { useServiceAgentNameFetcher } from './use_service_agent_name_fetcher'; +import { IUrlParams } from '../url_params_context/types'; export const APMServiceContext = createContext<{ agentName?: string; @@ -27,8 +27,8 @@ export function ApmServiceContextProvider({ children: ReactNode; }) { const { urlParams } = useUrlParams(); - const { agentName } = useServiceAgentName(); - const transactionTypes = useServiceTransactionTypes(); + const { agentName } = useServiceAgentNameFetcher(); + const transactionTypes = useServiceTransactionTypesFetcher(); const transactionType = getTransactionType({ urlParams, transactionTypes, diff --git a/x-pack/plugins/apm/public/hooks/use_apm_service.ts b/x-pack/plugins/apm/public/context/apm_service/use_apm_service_context.ts similarity index 75% rename from x-pack/plugins/apm/public/hooks/use_apm_service.ts rename to x-pack/plugins/apm/public/context/apm_service/use_apm_service_context.ts index bc80c3771c39d..85c135f36719f 100644 --- a/x-pack/plugins/apm/public/hooks/use_apm_service.ts +++ b/x-pack/plugins/apm/public/context/apm_service/use_apm_service_context.ts @@ -5,8 +5,8 @@ */ import { useContext } from 'react'; -import { APMServiceContext } from '../context/apm_service_context'; +import { APMServiceContext } from './apm_service_context'; -export function useApmService() { +export function useApmServiceContext() { return useContext(APMServiceContext); } diff --git a/x-pack/plugins/apm/public/hooks/use_service_agent_name.ts b/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts similarity index 83% rename from x-pack/plugins/apm/public/hooks/use_service_agent_name.ts rename to x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts index 199f14532f7b4..9a1d969ec2c33 100644 --- a/x-pack/plugins/apm/public/hooks/use_service_agent_name.ts +++ b/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts @@ -5,10 +5,10 @@ */ import { useParams } from 'react-router-dom'; -import { useFetcher } from './useFetcher'; -import { useUrlParams } from './useUrlParams'; +import { useFetcher } from '../../hooks/use_fetcher'; +import { useUrlParams } from '../url_params_context/use_url_params'; -export function useServiceAgentName() { +export function useServiceAgentNameFetcher() { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams } = useUrlParams(); const { start, end } = urlParams; diff --git a/x-pack/plugins/apm/public/hooks/use_service_transaction_types.tsx b/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx similarity index 83% rename from x-pack/plugins/apm/public/hooks/use_service_transaction_types.tsx rename to x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx index 9d8892ac79b7d..85a10cc273bac 100644 --- a/x-pack/plugins/apm/public/hooks/use_service_transaction_types.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx @@ -5,12 +5,12 @@ */ import { useParams } from 'react-router-dom'; -import { useFetcher } from './useFetcher'; -import { useUrlParams } from './useUrlParams'; +import { useFetcher } from '../../hooks/use_fetcher'; +import { useUrlParams } from '../url_params_context/use_url_params'; const INITIAL_DATA = { transactionTypes: [] }; -export function useServiceTransactionTypes() { +export function useServiceTransactionTypesFetcher() { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams } = useUrlParams(); const { start, end } = urlParams; diff --git a/x-pack/plugins/apm/public/context/chart_pointer_event_context.tsx b/x-pack/plugins/apm/public/context/chart_pointer_event/chart_pointer_event_context.tsx similarity index 100% rename from x-pack/plugins/apm/public/context/chart_pointer_event_context.tsx rename to x-pack/plugins/apm/public/context/chart_pointer_event/chart_pointer_event_context.tsx diff --git a/x-pack/plugins/apm/public/hooks/use_chart_pointer_event.tsx b/x-pack/plugins/apm/public/context/chart_pointer_event/use_chart_pointer_event_context.tsx similarity index 78% rename from x-pack/plugins/apm/public/hooks/use_chart_pointer_event.tsx rename to x-pack/plugins/apm/public/context/chart_pointer_event/use_chart_pointer_event_context.tsx index 058ec594e2d22..bf53273104d60 100644 --- a/x-pack/plugins/apm/public/hooks/use_chart_pointer_event.tsx +++ b/x-pack/plugins/apm/public/context/chart_pointer_event/use_chart_pointer_event_context.tsx @@ -5,9 +5,9 @@ */ import { useContext } from 'react'; -import { ChartPointerEventContext } from '../context/chart_pointer_event_context'; +import { ChartPointerEventContext } from './chart_pointer_event_context'; -export function useChartPointerEvent() { +export function useChartPointerEventContext() { const context = useContext(ChartPointerEventContext); if (!context) { diff --git a/x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx b/x-pack/plugins/apm/public/context/license/Invalid_license_notification.tsx similarity index 100% rename from x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx rename to x-pack/plugins/apm/public/context/license/Invalid_license_notification.tsx diff --git a/x-pack/plugins/apm/public/context/LicenseContext/index.tsx b/x-pack/plugins/apm/public/context/license/license_context.tsx similarity index 87% rename from x-pack/plugins/apm/public/context/LicenseContext/index.tsx rename to x-pack/plugins/apm/public/context/license/license_context.tsx index e6615a2fc98bf..557f135fa4c0e 100644 --- a/x-pack/plugins/apm/public/context/LicenseContext/index.tsx +++ b/x-pack/plugins/apm/public/context/license/license_context.tsx @@ -7,8 +7,8 @@ import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import { ILicense } from '../../../../licensing/public'; -import { useApmPluginContext } from '../../hooks/useApmPluginContext'; -import { InvalidLicenseNotification } from './InvalidLicenseNotification'; +import { useApmPluginContext } from '../apm_plugin/use_apm_plugin_context'; +import { InvalidLicenseNotification } from './Invalid_license_notification'; export const LicenseContext = React.createContext( undefined diff --git a/x-pack/plugins/apm/public/hooks/useLicense.ts b/x-pack/plugins/apm/public/context/license/use_license_context.ts similarity index 77% rename from x-pack/plugins/apm/public/hooks/useLicense.ts rename to x-pack/plugins/apm/public/context/license/use_license_context.ts index ca828e49706a8..e86bb78d127ab 100644 --- a/x-pack/plugins/apm/public/hooks/useLicense.ts +++ b/x-pack/plugins/apm/public/context/license/use_license_context.ts @@ -5,8 +5,8 @@ */ import { useContext } from 'react'; -import { LicenseContext } from '../context/LicenseContext'; +import { LicenseContext } from './license_context'; -export function useLicense() { +export function useLicenseContext() { return useContext(LicenseContext); } diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/constants.ts b/x-pack/plugins/apm/public/context/url_params_context/constants.ts similarity index 100% rename from x-pack/plugins/apm/public/context/UrlParamsContext/constants.ts rename to x-pack/plugins/apm/public/context/url_params_context/constants.ts diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/plugins/apm/public/context/url_params_context/helpers.ts similarity index 100% rename from x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts rename to x-pack/plugins/apm/public/context/url_params_context/helpers.ts diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx b/x-pack/plugins/apm/public/context/url_params_context/mock_url_params_context_provider.tsx similarity index 93% rename from x-pack/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx rename to x-pack/plugins/apm/public/context/url_params_context/mock_url_params_context_provider.tsx index fd01e057ac3de..b593cbd57a9a9 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx +++ b/x-pack/plugins/apm/public/context/url_params_context/mock_url_params_context_provider.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { IUrlParams } from './types'; -import { UrlParamsContext, useUiFilters } from '.'; +import { UrlParamsContext, useUiFilters } from './url_params_context'; const defaultUrlParams = { page: 0, diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts similarity index 100% rename from x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts rename to x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/plugins/apm/public/context/url_params_context/types.ts similarity index 100% rename from x-pack/plugins/apm/public/context/UrlParamsContext/types.ts rename to x-pack/plugins/apm/public/context/url_params_context/types.ts diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/url_params_context.test.tsx b/x-pack/plugins/apm/public/context/url_params_context/url_params_context.test.tsx similarity index 98% rename from x-pack/plugins/apm/public/context/UrlParamsContext/url_params_context.test.tsx rename to x-pack/plugins/apm/public/context/url_params_context/url_params_context.test.tsx index 3a6ccce178cd4..6b57039678e0a 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/url_params_context.test.tsx +++ b/x-pack/plugins/apm/public/context/url_params_context/url_params_context.test.tsx @@ -5,7 +5,7 @@ */ import * as React from 'react'; -import { UrlParamsContext, UrlParamsProvider } from './'; +import { UrlParamsContext, UrlParamsProvider } from './url_params_context'; import { mount } from 'enzyme'; import { Location, History } from 'history'; import { MemoryRouter, Router } from 'react-router-dom'; diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx b/x-pack/plugins/apm/public/context/url_params_context/url_params_context.tsx similarity index 98% rename from x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx rename to x-pack/plugins/apm/public/context/url_params_context/url_params_context.tsx index 5682009019d7f..0a3f8459ff002 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx +++ b/x-pack/plugins/apm/public/context/url_params_context/url_params_context.tsx @@ -15,7 +15,7 @@ import { withRouter } from 'react-router-dom'; import { uniqueId, mapValues } from 'lodash'; import { IUrlParams } from './types'; import { getParsedDate } from './helpers'; -import { resolveUrlParams } from './resolveUrlParams'; +import { resolveUrlParams } from './resolve_url_params'; import { UIFilters } from '../../../typings/ui_filters'; import { localUIFilterNames, diff --git a/x-pack/plugins/apm/public/hooks/useUrlParams.tsx b/x-pack/plugins/apm/public/context/url_params_context/use_url_params.tsx similarity index 84% rename from x-pack/plugins/apm/public/hooks/useUrlParams.tsx rename to x-pack/plugins/apm/public/context/url_params_context/use_url_params.tsx index b9f47046812be..1bf071d9db35e 100644 --- a/x-pack/plugins/apm/public/hooks/useUrlParams.tsx +++ b/x-pack/plugins/apm/public/context/url_params_context/use_url_params.tsx @@ -5,7 +5,7 @@ */ import { useContext } from 'react'; -import { UrlParamsContext } from '../context/UrlParamsContext'; +import { UrlParamsContext } from './url_params_context'; export function useUrlParams() { return useContext(UrlParamsContext); diff --git a/x-pack/plugins/apm/public/hooks/useCallApi.ts b/x-pack/plugins/apm/public/hooks/useCallApi.ts index 3fec36e7fb24b..79e439c3f7e7a 100644 --- a/x-pack/plugins/apm/public/hooks/useCallApi.ts +++ b/x-pack/plugins/apm/public/hooks/useCallApi.ts @@ -6,7 +6,7 @@ import { useMemo } from 'react'; import { callApi } from '../services/rest/callApi'; -import { useApmPluginContext } from './useApmPluginContext'; +import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; import { FetchOptions } from '../../common/fetch_options'; export function useCallApi() { diff --git a/x-pack/plugins/apm/public/hooks/useKibanaUrl.ts b/x-pack/plugins/apm/public/hooks/useKibanaUrl.ts index b4a354c231633..66edb84378a45 100644 --- a/x-pack/plugins/apm/public/hooks/useKibanaUrl.ts +++ b/x-pack/plugins/apm/public/hooks/useKibanaUrl.ts @@ -5,7 +5,7 @@ */ import url from 'url'; -import { useApmPluginContext } from './useApmPluginContext'; +import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; export function useKibanaUrl( /** The path to the plugin */ path: string, diff --git a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts index da1797fd712b1..551e92f8ba034 100644 --- a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts @@ -15,10 +15,10 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/ui_filters/local_ui_filters/config'; import { fromQuery, toQuery } from '../components/shared/Links/url_helpers'; -import { removeUndefinedProps } from '../context/UrlParamsContext/helpers'; +import { removeUndefinedProps } from '../context/url_params_context/helpers'; import { useCallApi } from './useCallApi'; -import { useFetcher } from './useFetcher'; -import { useUrlParams } from './useUrlParams'; +import { useFetcher } from './use_fetcher'; +import { useUrlParams } from '../context/url_params_context/use_url_params'; import { LocalUIFilterName } from '../../common/ui_filter'; const getInitialData = ( diff --git a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx index dcd6ed0ba4934..9127bd3adc69e 100644 --- a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx +++ b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx @@ -9,11 +9,11 @@ import produce from 'immer'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { routes } from '../components/app/Main/route_config'; -import { ApmPluginContextValue } from '../context/ApmPluginContext'; +import { ApmPluginContextValue } from '../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, -} from '../context/ApmPluginContext/MockApmPluginContext'; +} from '../context/apm_plugin/mock_apm_plugin_context'; import { useBreadcrumbs } from './use_breadcrumbs'; function createWrapper(path: string) { diff --git a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts index 640170bf3bff2..089381cbe05b5 100644 --- a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts @@ -16,7 +16,7 @@ import { } from 'react-router-dom'; import { APMRouteDefinition, BreadcrumbTitle } from '../application/routes'; import { getAPMHref } from '../components/shared/Links/apm/APMLink'; -import { useApmPluginContext } from './useApmPluginContext'; +import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; interface BreadcrumbWithoutLink extends ChromeBreadcrumb { match: Match>; diff --git a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts b/x-pack/plugins/apm/public/hooks/use_dynamic_index_pattern.ts similarity index 89% rename from x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts rename to x-pack/plugins/apm/public/hooks/use_dynamic_index_pattern.ts index d0e12d8537846..becdf1f9ecc5e 100644 --- a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts +++ b/x-pack/plugins/apm/public/hooks/use_dynamic_index_pattern.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useFetcher } from './useFetcher'; +import { useFetcher } from './use_fetcher'; import { UIProcessorEvent } from '../../common/processor_event'; -export function useDynamicIndexPattern( +export function useDynamicIndexPatternFetcher( processorEvent: UIProcessorEvent | undefined ) { const { data, status } = useFetcher( diff --git a/x-pack/plugins/apm/public/hooks/useEnvironments.tsx b/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx similarity index 94% rename from x-pack/plugins/apm/public/hooks/useEnvironments.tsx rename to x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx index 05ac780aefbde..1ad151b8c7e90 100644 --- a/x-pack/plugins/apm/public/hooks/useEnvironments.tsx +++ b/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx @@ -5,7 +5,7 @@ */ import { useMemo } from 'react'; -import { useFetcher } from './useFetcher'; +import { useFetcher } from './use_fetcher'; import { ENVIRONMENT_ALL, ENVIRONMENT_NOT_DEFINED, @@ -23,7 +23,7 @@ function getEnvironmentOptions(environments: string[]) { return [ENVIRONMENT_ALL, ...environmentOptions]; } -export function useEnvironments({ +export function useEnvironmentsFetcher({ serviceName, start, end, diff --git a/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx new file mode 100644 index 0000000000000..1c17be884ebf5 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx @@ -0,0 +1,40 @@ +/* + * 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 { useUrlParams } from '../context/url_params_context/use_url_params'; +import { useFetcher } from './use_fetcher'; + +export function useErrorGroupDistributionFetcher({ + serviceName, + groupId, +}: { + serviceName: string; + groupId: string | undefined; +}) { + const { urlParams, uiFilters } = useUrlParams(); + const { start, end } = urlParams; + const { data } = useFetcher( + (callApmApi) => { + if (start && end) { + return callApmApi({ + endpoint: 'GET /api/apm/services/{serviceName}/errors/distribution', + params: { + path: { serviceName }, + query: { + start, + end, + groupId, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + }, + [serviceName, start, end, groupId, uiFilters] + ); + + return { errorDistributionData: data }; +} diff --git a/x-pack/plugins/apm/public/hooks/use_fetcher.integration.test.tsx b/x-pack/plugins/apm/public/hooks/use_fetcher.integration.test.tsx index e837851828d94..e6f3b71af8a85 100644 --- a/x-pack/plugins/apm/public/hooks/use_fetcher.integration.test.tsx +++ b/x-pack/plugins/apm/public/hooks/use_fetcher.integration.test.tsx @@ -7,8 +7,8 @@ import { render, waitFor } from '@testing-library/react'; import React from 'react'; import { delay } from '../utils/testHelpers'; -import { useFetcher } from './useFetcher'; -import { MockApmPluginContextWrapper } from '../context/ApmPluginContext/MockApmPluginContext'; +import { useFetcher } from './use_fetcher'; +import { MockApmPluginContextWrapper } from '../context/apm_plugin/mock_apm_plugin_context'; const wrapper = MockApmPluginContextWrapper; diff --git a/x-pack/plugins/apm/public/hooks/useFetcher.test.tsx b/x-pack/plugins/apm/public/hooks/use_fetcher.test.tsx similarity index 96% rename from x-pack/plugins/apm/public/hooks/useFetcher.test.tsx rename to x-pack/plugins/apm/public/hooks/use_fetcher.test.tsx index 59dd9455c724c..9b4ad6bc9bb51 100644 --- a/x-pack/plugins/apm/public/hooks/useFetcher.test.tsx +++ b/x-pack/plugins/apm/public/hooks/use_fetcher.test.tsx @@ -6,9 +6,9 @@ import { renderHook, RenderHookResult } from '@testing-library/react-hooks'; import { delay } from '../utils/testHelpers'; -import { FetcherResult, useFetcher } from './useFetcher'; -import { MockApmPluginContextWrapper } from '../context/ApmPluginContext/MockApmPluginContext'; -import { ApmPluginContextValue } from '../context/ApmPluginContext'; +import { FetcherResult, useFetcher } from './use_fetcher'; +import { MockApmPluginContextWrapper } from '../context/apm_plugin/mock_apm_plugin_context'; +import { ApmPluginContextValue } from '../context/apm_plugin/apm_plugin_context'; // Wrap the hook with a provider so it can useApmPluginContext const wrapper = MockApmPluginContextWrapper; diff --git a/x-pack/plugins/apm/public/hooks/useFetcher.tsx b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx similarity index 98% rename from x-pack/plugins/apm/public/hooks/useFetcher.tsx rename to x-pack/plugins/apm/public/hooks/use_fetcher.tsx index 6add0e8a2b480..a9a4871dc8707 100644 --- a/x-pack/plugins/apm/public/hooks/useFetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx @@ -9,7 +9,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { IHttpFetchError } from 'src/core/public'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { APMClient, callApmApi } from '../services/rest/createCallApmApi'; -import { useApmPluginContext } from './useApmPluginContext'; +import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; export enum FETCH_STATUS { LOADING = 'loading', diff --git a/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts b/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts similarity index 75% rename from x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts rename to x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts index 7a54c6ffc6dbe..c888c51589563 100644 --- a/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts +++ b/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts @@ -7,22 +7,23 @@ import { useParams } from 'react-router-dom'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { MetricsChartsByAgentAPIResponse } from '../../server/lib/metrics/get_metrics_chart_data_by_agent'; -import { useUiFilters } from '../context/UrlParamsContext'; -import { IUrlParams } from '../context/UrlParamsContext/types'; -import { useFetcher } from './useFetcher'; +import { useUrlParams } from '../context/url_params_context/use_url_params'; +import { useApmServiceContext } from '../context/apm_service/use_apm_service_context'; +import { useFetcher } from './use_fetcher'; const INITIAL_DATA: MetricsChartsByAgentAPIResponse = { charts: [], }; -export function useServiceMetricCharts( - urlParams: IUrlParams, - agentName?: string, - serviceNodeName?: string -) { +export function useServiceMetricChartsFetcher({ + serviceNodeName, +}: { + serviceNodeName: string | undefined; +}) { + const { urlParams, uiFilters } = useUrlParams(); + const { agentName } = useApmServiceContext(); const { serviceName } = useParams<{ serviceName?: string }>(); const { start, end } = urlParams; - const uiFilters = useUiFilters(urlParams); const { data = INITIAL_DATA, error, status } = useFetcher( (callApmApi) => { if (serviceName && start && end && agentName) { diff --git a/x-pack/plugins/apm/public/hooks/useTheme.tsx b/x-pack/plugins/apm/public/hooks/use_theme.tsx similarity index 100% rename from x-pack/plugins/apm/public/hooks/useTheme.tsx rename to x-pack/plugins/apm/public/hooks/use_theme.tsx diff --git a/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts b/x-pack/plugins/apm/public/hooks/use_transaction_charts_fetcher.ts similarity index 82% rename from x-pack/plugins/apm/public/hooks/useTransactionCharts.ts rename to x-pack/plugins/apm/public/hooks/use_transaction_charts_fetcher.ts index c790ac57edc3b..f5105e38b985e 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_charts_fetcher.ts @@ -7,10 +7,10 @@ import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { getTransactionCharts } from '../selectors/chart_selectors'; -import { useFetcher } from './useFetcher'; -import { useUrlParams } from './useUrlParams'; +import { useFetcher } from './use_fetcher'; +import { useUrlParams } from '../context/url_params_context/use_url_params'; -export function useTransactionCharts() { +export function useTransactionChartsFetcher() { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams: { transactionType, start, end, transactionName }, @@ -45,8 +45,8 @@ export function useTransactionCharts() { ); return { - data: memoizedData, - status, - error, + transactionChartsData: memoizedData, + transactionChartsStatus: status, + transactionChartsError: error, }; } diff --git a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts similarity index 89% rename from x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts rename to x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts index 9cbfee37d1253..74222e8ffe038 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts @@ -6,12 +6,11 @@ import { flatten, omit, isEmpty } from 'lodash'; import { useHistory, useParams } from 'react-router-dom'; -import { IUrlParams } from '../context/UrlParamsContext/types'; -import { useFetcher } from './useFetcher'; -import { useUiFilters } from '../context/UrlParamsContext'; +import { useFetcher } from './use_fetcher'; import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; import { maybe } from '../../common/utils/maybe'; import { APIReturnType } from '../services/rest/createCallApmApi'; +import { useUrlParams } from '../context/url_params_context/use_url_params'; type APIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transaction_groups/distribution'>; @@ -21,8 +20,9 @@ const INITIAL_DATA = { bucketSize: 0, }; -export function useTransactionDistribution(urlParams: IUrlParams) { +export function useTransactionDistributionFetcher() { const { serviceName } = useParams<{ serviceName?: string }>(); + const { urlParams, uiFilters } = useUrlParams(); const { start, end, @@ -31,10 +31,8 @@ export function useTransactionDistribution(urlParams: IUrlParams) { traceId, transactionName, } = urlParams; - const uiFilters = useUiFilters(urlParams); const history = useHistory(); - const { data = INITIAL_DATA, status, error } = useFetcher( async (callApmApi) => { if (serviceName && start && end && transactionType && transactionName) { @@ -96,5 +94,9 @@ export function useTransactionDistribution(urlParams: IUrlParams) { [serviceName, start, end, transactionType, transactionName, uiFilters] ); - return { data, status, error }; + return { + distributionData: data, + distributionStatus: status, + distributionError: error, + }; } diff --git a/x-pack/plugins/apm/public/selectors/chart_selectors.ts b/x-pack/plugins/apm/public/selectors/chart_selectors.ts index 2fdcaf9e4e675..37bd04e5d9980 100644 --- a/x-pack/plugins/apm/public/selectors/chart_selectors.ts +++ b/x-pack/plugins/apm/public/selectors/chart_selectors.ts @@ -17,7 +17,7 @@ import { RectCoordinate, TimeSeries, } from '../../typings/timeseries'; -import { IUrlParams } from '../context/UrlParamsContext/types'; +import { IUrlParams } from '../context/url_params_context/types'; import { getEmptySeries } from '../components/shared/charts/helper/get_empty_series'; import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor'; import { asDuration, asTransactionRate } from '../../common/utils/formatters'; diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 87dfeb95b6326..21c87c18be363 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -24,8 +24,8 @@ import { PromiseReturnType } from '../../../observability/typings/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { APMConfig } from '../../server'; import { UIFilters } from '../../typings/ui_filters'; -import { MockApmPluginContextWrapper } from '../context/ApmPluginContext/MockApmPluginContext'; -import { UrlParamsProvider } from '../context/UrlParamsContext'; +import { MockApmPluginContextWrapper } from '../context/apm_plugin/mock_apm_plugin_context'; +import { UrlParamsProvider } from '../context/url_params_context/url_params_context'; const originalConsoleWarn = console.warn; // eslint-disable-line no-console /** diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts index f9266baddaf27..5f53bfa18c468 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/merge_transaction_group_data.ts @@ -46,26 +46,26 @@ export function mergeTransactionGroupData({ const timeseriesBuckets = groupBucket?.timeseries.buckets ?? []; return timeseriesBuckets.reduce( - (prev, point) => { + (acc, point) => { return { - ...prev, + ...acc, latency: { - ...prev.latency, - timeseries: prev.latency.timeseries.concat({ + ...acc.latency, + timeseries: acc.latency.timeseries.concat({ x: point.key, y: point.avg_latency.value, }), }, throughput: { - ...prev.throughput, - timeseries: prev.throughput.timeseries.concat({ + ...acc.throughput, + timeseries: acc.throughput.timeseries.concat({ x: point.key, y: point.transaction_count.value / deltaAsMinutes, }), }, errorRate: { - ...prev.errorRate, - timeseries: prev.errorRate.timeseries.concat({ + ...acc.errorRate, + timeseries: acc.errorRate.timeseries.concat({ x: point.key, y: point.transaction_count.value > 0 diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 9334ce60a3f9e..4f7f6320185bf 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -42,8 +42,11 @@ import { } from './settings/apm_indices'; import { metricsChartsRoute } from './metrics'; import { serviceNodesRoute } from './service_nodes'; -import { tracesRoute, tracesByIdRoute } from './traces'; -import { transactionByTraceIdRoute } from './transaction'; +import { + tracesRoute, + tracesByIdRoute, + rootTransactionByTraceIdRoute, +} from './traces'; import { correlationsForSlowTransactionsRoute, correlationsForFailedTransactionsRoute, @@ -147,6 +150,7 @@ const createApmApi = () => { // Traces .add(tracesRoute) .add(tracesByIdRoute) + .add(rootTransactionByTraceIdRoute) // Transaction groups .add(transactionGroupsBreakdownRoute) @@ -166,9 +170,6 @@ const createApmApi = () => { .add(serviceNodesLocalFiltersRoute) .add(uiFiltersEnvironmentsRoute) - // Transaction - .add(transactionByTraceIdRoute) - // Service map .add(serviceMapRoute) .add(serviceMapServiceNodeRoute) diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index e7405ad16a63e..49708e4edb732 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -19,7 +19,7 @@ import { notifyFeatureUsage } from '../../feature'; // get ML anomaly detection jobs for each environment export const anomalyDetectionJobsRoute = createRoute({ - endpoint: 'GET /api/apm/settings/anomaly-detection', + endpoint: 'GET /api/apm/settings/anomaly-detection/jobs', options: { tags: ['access:apm', 'access:ml:canGetJobs'], }, diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index 0c79d391e1fd7..373dc9b8b6ecd 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -11,6 +11,7 @@ import { getTransactionGroupList } from '../lib/transaction_groups'; import { createRoute } from './create_route'; import { rangeRt, uiFiltersRt } from './default_api_types'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { getRootTransactionByTraceId } from '../lib/transactions/get_transaction_by_trace'; export const tracesRoute = createRoute({ endpoint: 'GET /api/apm/traces', @@ -44,3 +45,18 @@ export const tracesByIdRoute = createRoute({ return getTrace(context.params.path.traceId, setup); }, }); + +export const rootTransactionByTraceIdRoute = createRoute({ + endpoint: 'GET /api/apm/traces/{traceId}/root_transaction', + params: t.type({ + path: t.type({ + traceId: t.string, + }), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const { traceId } = context.params.path; + const setup = await setupRequest(context, request); + return getRootTransactionByTraceId(traceId, setup); + }, +}); diff --git a/x-pack/plugins/apm/server/routes/transaction.ts b/x-pack/plugins/apm/server/routes/transaction.ts deleted file mode 100644 index 3294d2e9a8227..0000000000000 --- a/x-pack/plugins/apm/server/routes/transaction.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as t from 'io-ts'; -import { setupRequest } from '../lib/helpers/setup_request'; -import { getRootTransactionByTraceId } from '../lib/transactions/get_transaction_by_trace'; -import { createRoute } from './create_route'; - -export const transactionByTraceIdRoute = createRoute({ - endpoint: 'GET /api/apm/transaction/{traceId}', - params: t.type({ - path: t.type({ - traceId: t.string, - }), - }), - options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const { traceId } = context.params.path; - const setup = await setupRequest(context, request); - return getRootTransactionByTraceId(traceId, setup); - }, -}); diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_actions_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_actions_provider.js new file mode 100644 index 0000000000000..df3017ebf92a4 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_actions_provider.js @@ -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 { FollowerIndexPauseProvider } from './follower_index_pause_provider'; +import { FollowerIndexResumeProvider } from './follower_index_resume_provider'; +import { FollowerIndexUnfollowProvider } from './follower_index_unfollow_provider'; + +export const FollowerIndexActionsProvider = (props) => { + return ( + + {(pauseFollowerIndex) => ( + + {(resumeFollowerIndex) => ( + + {(unfollowLeaderIndex) => { + const { children } = props; + return children(() => ({ + pauseFollowerIndex, + resumeFollowerIndex, + unfollowLeaderIndex, + })); + }} + + )} + + )} + + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_pause_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js similarity index 95% rename from x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_pause_provider.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js index 9c1e8255d069c..7d1168d831631 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_pause_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js @@ -11,9 +11,9 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; -import { pauseFollowerIndex } from '../store/actions'; -import { arrify } from '../../../common/services/utils'; -import { areAllSettingsDefault } from '../services/follower_index_default_settings'; +import { pauseFollowerIndex } from '../../store/actions'; +import { arrify } from '../../../../common/services/utils'; +import { areAllSettingsDefault } from '../../services/follower_index_default_settings'; class FollowerIndexPauseProviderUi extends PureComponent { static propTypes = { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js similarity index 95% rename from x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js index 101e3df6bf710..86f8c0447e734 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js @@ -10,10 +10,10 @@ import { connect } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiConfirmModal, EuiLink, EuiOverlayMask } from '@elastic/eui'; -import { reactRouterNavigate } from '../../../../../../src/plugins/kibana_react/public'; -import { routing } from '../services/routing'; -import { resumeFollowerIndex } from '../store/actions'; -import { arrify } from '../../../common/services/utils'; +import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; +import { routing } from '../../services/routing'; +import { resumeFollowerIndex } from '../../store/actions'; +import { arrify } from '../../../../common/services/utils'; class FollowerIndexResumeProviderUi extends PureComponent { static propTypes = { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_unfollow_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js similarity index 97% rename from x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_unfollow_provider.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js index 68b6b970ad90b..f9644aa20c2c2 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_unfollow_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js @@ -11,8 +11,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; -import { unfollowLeaderIndex } from '../store/actions'; -import { arrify } from '../../../common/services/utils'; +import { unfollowLeaderIndex } from '../../store/actions'; +import { arrify } from '../../../../common/services/utils'; class FollowerIndexUnfollowProviderUi extends PureComponent { static propTypes = { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/index.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/index.js new file mode 100644 index 0000000000000..fe1a7d82a56a1 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/index.js @@ -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. + */ + +export { FollowerIndexActionsProvider } from './follower_index_actions_provider'; +export { FollowerIndexPauseProvider } from './follower_index_pause_provider'; +export { FollowerIndexResumeProvider } from './follower_index_resume_provider'; +export { FollowerIndexUnfollowProvider } from './follower_index_unfollow_provider'; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/index.js b/x-pack/plugins/cross_cluster_replication/public/app/components/index.js index 6d971bff03981..55609fa85fb11 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/index.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/index.js @@ -12,9 +12,10 @@ export { AutoFollowPatternForm } from './auto_follow_pattern_form'; export { AutoFollowPatternDeleteProvider } from './auto_follow_pattern_delete_provider'; export { AutoFollowPatternPageTitle } from './auto_follow_pattern_page_title'; export { AutoFollowPatternIndicesPreview } from './auto_follow_pattern_indices_preview'; -export { FollowerIndexPauseProvider } from './follower_index_pause_provider'; -export { FollowerIndexResumeProvider } from './follower_index_resume_provider'; -export { FollowerIndexUnfollowProvider } from './follower_index_unfollow_provider'; +export { FollowerIndexPauseProvider } from './follower_index_actions_providers'; +export { FollowerIndexResumeProvider } from './follower_index_actions_providers'; +export { FollowerIndexUnfollowProvider } from './follower_index_actions_providers'; +export { FollowerIndexActionsProvider } from './follower_index_actions_providers'; export { FollowerIndexForm } from './follower_index_form'; export { FollowerIndexPageTitle } from './follower_index_page_title'; export { FormEntryRow } from './form_entry_row'; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js index 2309dece3f92b..dd5fe6f212808 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { PureComponent, Fragment } from 'react'; +import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { @@ -13,7 +13,6 @@ import { EuiLoadingKibana, EuiOverlayMask, EuiHealth, - EuiIcon, } from '@elastic/eui'; import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK } from '../../../../../constants'; import { @@ -23,6 +22,33 @@ import { import { routing } from '../../../../../services/routing'; import { trackUiMetric } from '../../../../../services/track_ui_metric'; +const actionI18nTexts = { + pause: i18n.translate( + 'xpack.crossClusterReplication.autoFollowPatternList.table.actionPauseDescription', + { + defaultMessage: 'Pause replication', + } + ), + resume: i18n.translate( + 'xpack.crossClusterReplication.autoFollowPatternList.table.actionResumeDescription', + { + defaultMessage: 'Resume replication', + } + ), + edit: i18n.translate( + 'xpack.crossClusterReplication.autoFollowPatternList.table.actionEditDescription', + { + defaultMessage: 'Edit auto-follow pattern', + } + ), + delete: i18n.translate( + 'xpack.crossClusterReplication.autoFollowPatternList.table.actionDeleteDescription', + { + defaultMessage: 'Delete auto-follow pattern', + } + ), +}; + const getFilteredPatterns = (autoFollowPatterns, queryText) => { if (queryText) { const normalizedSearchText = queryText.toLowerCase(); @@ -93,7 +119,7 @@ export class AutoFollowPatternTable extends PureComponent { }); }; - getTableColumns() { + getTableColumns(deleteAutoFollowPattern) { const { selectAutoFollowPattern } = this.props; return [ @@ -200,88 +226,34 @@ export class AutoFollowPatternTable extends PureComponent { ), actions: [ { - render: ({ name, active }) => { - const label = active - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPatternList.table.actionPauseDescription', - { - defaultMessage: 'Pause replication', - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPatternList.table.actionResumeDescription', - { - defaultMessage: 'Resume replication', - } - ); - - return ( - { - if (event.stopPropagation) { - event.stopPropagation(); - } - if (active) { - this.props.pauseAutoFollowPattern(name); - } else { - this.props.resumeAutoFollowPattern(name); - } - }} - data-test-subj={active ? 'contextMenuPauseButton' : 'contextMenuResumeButton'} - > - - {label} - - ); - }, + name: actionI18nTexts.pause, + description: actionI18nTexts.pause, + icon: 'pause', + onClick: (item) => this.props.pauseAutoFollowPattern(item.name), + available: (item) => item.active, + 'data-test-subj': 'contextMenuPauseButton', }, { - render: ({ name }) => { - const label = i18n.translate( - 'xpack.crossClusterReplication.autoFollowPatternList.table.actionEditDescription', - { - defaultMessage: 'Edit auto-follow pattern', - } - ); - - return ( - routing.navigate(routing.getAutoFollowPatternPath(name))} - data-test-subj="contextMenuEditButton" - > - - {label} - - ); - }, + name: actionI18nTexts.resume, + description: actionI18nTexts.resume, + icon: 'play', + onClick: (item) => this.props.resumeAutoFollowPattern(item.name), + available: (item) => !item.active, + 'data-test-subj': 'contextMenuResumeButton', }, { - render: ({ name }) => { - const label = i18n.translate( - 'xpack.crossClusterReplication.autoFollowPatternList.table.actionDeleteDescription', - { - defaultMessage: 'Delete auto-follow pattern', - } - ); - - return ( - - {(deleteAutoFollowPattern) => ( - deleteAutoFollowPattern(name)} - data-test-subj="contextMenuDeleteButton" - > - - {label} - - )} - - ); - }, + name: actionI18nTexts.edit, + description: actionI18nTexts.edit, + icon: 'pencil', + onClick: (item) => routing.navigate(routing.getAutoFollowPatternPath(item.name)), + 'data-test-subj': 'contextMenuEditButton', + }, + { + name: actionI18nTexts.delete, + description: actionI18nTexts.delete, + icon: 'trash', + onClick: (item) => deleteAutoFollowPattern(item.name), + 'data-test-subj': 'contextMenuDeleteButton', }, ], width: '100px', @@ -339,26 +311,30 @@ export class AutoFollowPatternTable extends PureComponent { }; return ( - - ({ - 'data-test-subj': 'row', - })} - cellProps={(item, column) => ({ - 'data-test-subj': `cell_${column.field}`, - })} - data-test-subj="autoFollowPatternListTable" - /> - {this.renderLoading()} - + + {(deleteAutoFollowPattern) => ( + <> + ({ + 'data-test-subj': 'row', + })} + cellProps={(item, column) => ({ + 'data-test-subj': `cell_${column.field}`, + })} + data-test-subj="autoFollowPatternListTable" + /> + {this.renderLoading()} + + )} + ); } } diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js index 0c57b3f7330cf..2ea73e272b24e 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { PureComponent, Fragment } from 'react'; +import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiIcon, EuiHealth, EuiInMemoryTable, EuiLink, @@ -17,15 +16,38 @@ import { EuiOverlayMask, } from '@elastic/eui'; import { API_STATUS, UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK } from '../../../../../constants'; -import { - FollowerIndexPauseProvider, - FollowerIndexResumeProvider, - FollowerIndexUnfollowProvider, -} from '../../../../../components'; +import { FollowerIndexActionsProvider } from '../../../../../components'; import { routing } from '../../../../../services/routing'; import { trackUiMetric } from '../../../../../services/track_ui_metric'; import { ContextMenu } from '../context_menu'; +const actionI18nTexts = { + pause: i18n.translate( + 'xpack.crossClusterReplication.followerIndexList.table.actionPauseDescription', + { + defaultMessage: 'Pause replication', + } + ), + resume: i18n.translate( + 'xpack.crossClusterReplication.followerIndexList.table.actionResumeDescription', + { + defaultMessage: 'Resume replication', + } + ), + edit: i18n.translate( + 'xpack.crossClusterReplication.followerIndexList.table.actionEditDescription', + { + defaultMessage: 'Edit follower index', + } + ), + unfollow: i18n.translate( + 'xpack.crossClusterReplication.followerIndexList.table.actionUnfollowDescription', + { + defaultMessage: 'Unfollow leader index', + } + ), +}; + const getFilteredIndices = (followerIndices, queryText) => { if (queryText) { const normalizedSearchText = queryText.toLowerCase(); @@ -101,91 +123,43 @@ export class FollowerIndicesTable extends PureComponent { routing.navigate(uri); }; - getTableColumns() { + getTableColumns(actionHandlers) { const { selectFollowerIndex } = this.props; const actions = [ - /* Pause or resume follower index */ + /* Pause follower index */ { - render: (followerIndex) => { - const { name, isPaused } = followerIndex; - const label = isPaused - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndexList.table.actionResumeDescription', - { - defaultMessage: 'Resume replication', - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndexList.table.actionPauseDescription', - { - defaultMessage: 'Pause replication', - } - ); - - return isPaused ? ( - - {(resumeFollowerIndex) => ( - resumeFollowerIndex(name)} data-test-subj="resumeButton"> - - {label} - - )} - - ) : ( - - {(pauseFollowerIndex) => ( - pauseFollowerIndex(followerIndex)} - data-test-subj="pauseButton" - > - - {label} - - )} - - ); - }, + name: actionI18nTexts.pause, + description: actionI18nTexts.pause, + icon: 'pause', + onClick: (item) => actionHandlers.pauseFollowerIndex(item), + available: (item) => !item.isPaused, + 'data-test-subj': 'pauseButton', + }, + /* Resume follower index */ + { + name: actionI18nTexts.resume, + description: actionI18nTexts.resume, + icon: 'play', + onClick: (item) => actionHandlers.resumeFollowerIndex(item.name), + available: (item) => item.isPaused, + 'data-test-subj': 'resumeButton', }, /* Edit follower index */ { - render: ({ name }) => { - const label = i18n.translate( - 'xpack.crossClusterReplication.followerIndexList.table.actionEditDescription', - { - defaultMessage: 'Edit follower index', - } - ); - - return ( - this.editFollowerIndex(name)} data-test-subj="editButton"> - - {label} - - ); - }, + name: actionI18nTexts.edit, + description: actionI18nTexts.edit, + onClick: (item) => this.editFollowerIndex(item.name), + icon: 'pencil', + 'data-test-subj': 'editButton', }, /* Unfollow leader index */ { - render: ({ name }) => { - const label = i18n.translate( - 'xpack.crossClusterReplication.followerIndexList.table.actionUnfollowDescription', - { - defaultMessage: 'Unfollow leader index', - } - ); - - return ( - - {(unfollowLeaderIndex) => ( - unfollowLeaderIndex(name)} data-test-subj="unfollowButton"> - - {label} - - )} - - ); - }, + name: actionI18nTexts.unfollow, + description: actionI18nTexts.unfollow, + onClick: (item) => actionHandlers.unfollowLeaderIndex(item.name), + icon: 'indexFlush', + 'data-test-subj': 'unfollowButton', }, ]; @@ -321,26 +295,33 @@ export class FollowerIndicesTable extends PureComponent { }; return ( - - ({ - 'data-test-subj': 'row', - })} - cellProps={(item, column) => ({ - 'data-test-subj': `cell-${column.field}`, - })} - data-test-subj="followerIndexListTable" - /> - {this.renderLoading()} - + + {(getActionHandlers) => { + const actionHandlers = getActionHandlers(); + return ( + <> + ({ + 'data-test-subj': 'row', + })} + cellProps={(item, column) => ({ + 'data-test-subj': `cell-${column.field}`, + })} + data-test-subj="followerIndexListTable" + /> + {this.renderLoading()} + + ); + }} + ); } } diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index fa3206446f9fc..a3b37e47287e5 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -68,6 +68,8 @@ export class DataEnhancedPlugin React.createElement( createConnectedBackgroundSessionIndicator({ sessionService: plugins.data.search.session, + application: core.application, + timeFilter: plugins.data.query.timefilter.timefilter, }) ) ), diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index f4d7422d1c7e2..20b55d9688edb 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -9,9 +9,10 @@ import { EnhancedSearchInterceptor } from './search_interceptor'; import { CoreSetup, CoreStart } from 'kibana/public'; import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; import { AbortError } from '../../../../../src/plugins/kibana_utils/public'; -import { SearchTimeoutError } from 'src/plugins/data/public'; +import { ISessionService, SearchTimeoutError, SessionState } from 'src/plugins/data/public'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { bfetchPluginMock } from '../../../../../src/plugins/bfetch/public/mocks'; +import { BehaviorSubject } from 'rxjs'; const timeTravel = (msToRun = 0) => { jest.advanceTimersByTime(msToRun); @@ -43,11 +44,18 @@ function mockFetchImplementation(responses: any[]) { describe('EnhancedSearchInterceptor', () => { let mockUsageCollector: any; + let sessionService: jest.Mocked; + let sessionState$: BehaviorSubject; beforeEach(() => { mockCoreSetup = coreMock.createSetup(); mockCoreStart = coreMock.createStart(); + sessionState$ = new BehaviorSubject(SessionState.None); const dataPluginMockStart = dataPluginMock.createStartContract(); + sessionService = { + ...(dataPluginMockStart.search.session as jest.Mocked), + state$: sessionState$, + }; fetchMock = jest.fn(); mockCoreSetup.uiSettings.get.mockImplementation((name: string) => { @@ -87,7 +95,7 @@ describe('EnhancedSearchInterceptor', () => { http: mockCoreSetup.http, uiSettings: mockCoreSetup.uiSettings, usageCollector: mockUsageCollector, - session: dataPluginMockStart.search.session, + session: sessionService, }); }); @@ -144,6 +152,7 @@ describe('EnhancedSearchInterceptor', () => { }, }, ]; + mockFetchImplementation(responses); const response = searchInterceptor.search({}, { pollInterval: 0 }); @@ -361,6 +370,54 @@ describe('EnhancedSearchInterceptor', () => { expect(fetchMock).toHaveBeenCalledTimes(2); expect(mockCoreSetup.http.delete).toHaveBeenCalled(); }); + + test('should NOT DELETE a running SAVED async search on abort', async () => { + const sessionId = 'sessionId'; + sessionService.getSessionId.mockImplementation(() => sessionId); + const responses = [ + { + time: 10, + value: { + isPartial: true, + isRunning: true, + id: 1, + }, + }, + { + time: 300, + value: { + isPartial: false, + isRunning: false, + id: 1, + }, + }, + ]; + mockFetchImplementation(responses); + + const abortController = new AbortController(); + setTimeout(() => abortController.abort(), 250); + + const response = searchInterceptor.search( + {}, + { abortSignal: abortController.signal, pollInterval: 0, sessionId } + ); + response.subscribe({ next, error }); + + await timeTravel(10); + + expect(next).toHaveBeenCalled(); + expect(error).not.toHaveBeenCalled(); + + sessionState$.next(SessionState.BackgroundLoading); + + await timeTravel(240); + + expect(error).toHaveBeenCalled(); + expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(mockCoreSetup.http.delete).not.toHaveBeenCalled(); + }); }); describe('cancelPending', () => { @@ -395,4 +452,108 @@ describe('EnhancedSearchInterceptor', () => { expect(mockUsageCollector.trackQueriesCancelled).toBeCalledTimes(1); }); }); + + describe('session', () => { + beforeEach(() => { + const responses = [ + { + time: 10, + value: { + isPartial: true, + isRunning: true, + id: 1, + }, + }, + { + time: 300, + value: { + isPartial: false, + isRunning: false, + id: 1, + }, + }, + ]; + + mockFetchImplementation(responses); + }); + + test('should track searches', async () => { + const sessionId = 'sessionId'; + sessionService.getSessionId.mockImplementation(() => sessionId); + + const untrack = jest.fn(); + sessionService.trackSearch.mockImplementation(() => untrack); + + const response = searchInterceptor.search({}, { pollInterval: 0, sessionId }); + response.subscribe({ next, error }); + await timeTravel(10); + expect(sessionService.trackSearch).toBeCalledTimes(1); + expect(untrack).not.toBeCalled(); + await timeTravel(300); + expect(sessionService.trackSearch).toBeCalledTimes(1); + expect(untrack).toBeCalledTimes(1); + }); + + test('session service should be able to cancel search', async () => { + const sessionId = 'sessionId'; + sessionService.getSessionId.mockImplementation(() => sessionId); + + const untrack = jest.fn(); + sessionService.trackSearch.mockImplementation(() => untrack); + + const response = searchInterceptor.search({}, { pollInterval: 0, sessionId }); + response.subscribe({ next, error }); + await timeTravel(10); + expect(sessionService.trackSearch).toBeCalledTimes(1); + + const abort = sessionService.trackSearch.mock.calls[0][0].abort; + expect(abort).toBeInstanceOf(Function); + + abort(); + + await timeTravel(10); + + expect(error).toHaveBeenCalled(); + expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); + }); + + test("don't track non current session searches", async () => { + const sessionId = 'sessionId'; + sessionService.getSessionId.mockImplementation(() => sessionId); + + const untrack = jest.fn(); + sessionService.trackSearch.mockImplementation(() => untrack); + + const response1 = searchInterceptor.search( + {}, + { pollInterval: 0, sessionId: 'something different' } + ); + response1.subscribe({ next, error }); + + const response2 = searchInterceptor.search({}, { pollInterval: 0, sessionId: undefined }); + response2.subscribe({ next, error }); + + await timeTravel(10); + expect(sessionService.trackSearch).toBeCalledTimes(0); + }); + + test("don't track if no current session", async () => { + sessionService.getSessionId.mockImplementation(() => undefined); + + const untrack = jest.fn(); + sessionService.trackSearch.mockImplementation(() => untrack); + + const response1 = searchInterceptor.search( + {}, + { pollInterval: 0, sessionId: 'something different' } + ); + response1.subscribe({ next, error }); + + const response2 = searchInterceptor.search({}, { pollInterval: 0, sessionId: undefined }); + response2.subscribe({ next, error }); + + await timeTravel(10); + expect(sessionService.trackSearch).toBeCalledTimes(0); + }); + }); }); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index 9aa35b460b1e8..0e87c093d2a8d 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -5,13 +5,14 @@ */ import { throwError, Subscription } from 'rxjs'; -import { tap, finalize, catchError } from 'rxjs/operators'; +import { tap, finalize, catchError, filter, take, skip } from 'rxjs/operators'; import { TimeoutErrorMode, SearchInterceptor, SearchInterceptorDeps, UI_SETTINGS, IKibanaSearchRequest, + SessionState, } from '../../../../../src/plugins/data/public'; import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; import { ENHANCED_ES_SEARCH_STRATEGY, IAsyncSearchOptions, pollSearch } from '../../common'; @@ -54,7 +55,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { }; public search({ id, ...request }: IKibanaSearchRequest, options: IAsyncSearchOptions = {}) { - const { combinedSignal, timeoutSignal, cleanup } = this.setupAbortSignal({ + const { combinedSignal, timeoutSignal, cleanup, abort } = this.setupAbortSignal({ abortSignal: options.abortSignal, timeout: this.searchTimeout, }); @@ -63,16 +64,41 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { const search = () => this.runSearch({ id, ...request }, searchOptions); this.pendingCount$.next(this.pendingCount$.getValue() + 1); + const isCurrentSession = () => + !!options.sessionId && options.sessionId === this.deps.session.getSessionId(); + + const untrackSearch = isCurrentSession() && this.deps.session.trackSearch({ abort }); + + // track if this search's session will be send to background + // if yes, then we don't need to cancel this search when it is aborted + let isSavedToBackground = false; + const savedToBackgroundSub = + isCurrentSession() && + this.deps.session.state$ + .pipe( + skip(1), // ignore any state, we are only interested in transition x -> BackgroundLoading + filter((state) => isCurrentSession() && state === SessionState.BackgroundLoading), + take(1) + ) + .subscribe(() => { + isSavedToBackground = true; + }); return pollSearch(search, { ...options, abortSignal: combinedSignal }).pipe( tap((response) => (id = response.id)), catchError((e: AbortError) => { - if (id) this.deps.http.delete(`/internal/search/${strategy}/${id}`); + if (id && !isSavedToBackground) this.deps.http.delete(`/internal/search/${strategy}/${id}`); return throwError(this.handleSearchError(e, timeoutSignal, options)); }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); cleanup(); + if (untrackSearch && isCurrentSession()) { + untrackSearch(); + } + if (savedToBackgroundSub) { + savedToBackgroundSub.unsubscribe(); + } }) ); } diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx index 9cef76c62279c..4a6a852be755b 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.stories.tsx @@ -7,24 +7,33 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { BackgroundSessionIndicator } from './background_session_indicator'; -import { BackgroundSessionViewState } from '../connected_background_session_indicator'; +import { SessionState } from '../../../../../../../src/plugins/data/public'; storiesOf('components/BackgroundSessionIndicator', module).add('default', () => ( <>
- +
- +
- +
- +
- + +
+
+
)); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx index 5b7ab2e4f9b1f..b7d342300f311 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.test.tsx @@ -8,8 +8,8 @@ import React, { ReactNode } from 'react'; import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { BackgroundSessionIndicator } from './background_session_indicator'; -import { BackgroundSessionViewState } from '../connected_background_session_indicator'; import { IntlProvider } from 'react-intl'; +import { SessionState } from '../../../../../../../src/plugins/data/public'; function Container({ children }: { children?: ReactNode }) { return {children}; @@ -19,7 +19,7 @@ test('Loading state', async () => { const onCancel = jest.fn(); render( - + ); @@ -33,10 +33,7 @@ test('Completed state', async () => { const onSave = jest.fn(); render( - + ); @@ -50,10 +47,7 @@ test('Loading in the background state', async () => { const onCancel = jest.fn(); render( - + ); @@ -64,30 +58,26 @@ test('Loading in the background state', async () => { }); test('BackgroundCompleted state', async () => { - const onViewSession = jest.fn(); render( ); await userEvent.click(screen.getByLabelText('Results loaded in the background')); - await userEvent.click(screen.getByText('View background sessions')); - - expect(onViewSession).toBeCalled(); + expect(screen.getByRole('link', { name: 'View background sessions' }).getAttribute('href')).toBe( + '__link__' + ); }); test('Restored state', async () => { const onRefresh = jest.fn(); render( - + ); @@ -96,3 +86,27 @@ test('Restored state', async () => { expect(onRefresh).toBeCalled(); }); + +test('Canceled state', async () => { + const onRefresh = jest.fn(); + render( + + + + ); + + await userEvent.click(screen.getByLabelText('Canceled')); + await userEvent.click(screen.getByText('Refresh')); + + expect(onRefresh).toBeCalled(); +}); + +test('Disabled state', async () => { + render( + + + + ); + + expect(screen.getByTestId('backgroundSessionIndicator').querySelector('button')).toBeDisabled(); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx index b55bd6b655371..ce77686c4f3c1 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/background_session_indicator/background_session_indicator.tsx @@ -19,22 +19,29 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { BackgroundSessionViewState } from '../connected_background_session_indicator'; + import './background_session_indicator.scss'; +import { SessionState } from '../../../../../../../src/plugins/data/public/'; export interface BackgroundSessionIndicatorProps { - state: BackgroundSessionViewState; + state: SessionState; onContinueInBackground?: () => void; onCancel?: () => void; - onViewBackgroundSessions?: () => void; + viewBackgroundSessionsLink?: string; onSaveResults?: () => void; onRefresh?: () => void; + disabled?: boolean; + disabledReasonText?: string; } type ActionButtonProps = BackgroundSessionIndicatorProps & { buttonProps: EuiButtonEmptyProps }; const CancelButton = ({ onCancel = () => {}, buttonProps = {} }: ActionButtonProps) => ( - + {}, buttonProps = {}, }: ActionButtonProps) => ( - + {}, + viewBackgroundSessionsLink = 'management', buttonProps = {}, }: ActionButtonProps) => ( - // TODO: make this a link - + {}, buttonProps = {} }: ActionButtonProps) => ( - + {}, buttonProps = {} }: ActionButtonP ); const SaveButton = ({ onSaveResults = () => {}, buttonProps = {} }: ActionButtonProps) => ( - + {}, buttonProps = {} }: ActionButton ); const backgroundSessionIndicatorViewStateToProps: { - [state in BackgroundSessionViewState]: { - button: Pick & { tooltipText: string }; + [state in SessionState]: { + button: Pick & { + tooltipText: string; + }; popover: { text: string; primaryAction?: React.ComponentType; secondaryAction?: React.ComponentType; }; - }; + } | null; } = { - [BackgroundSessionViewState.Loading]: { + [SessionState.None]: null, + [SessionState.Loading]: { button: { color: 'subdued', iconType: 'clock', @@ -116,7 +141,7 @@ const backgroundSessionIndicatorViewStateToProps: { secondaryAction: ContinueInBackgroundButton, }, }, - [BackgroundSessionViewState.Completed]: { + [SessionState.Completed]: { button: { color: 'subdued', iconType: 'checkInCircleFilled', @@ -141,7 +166,7 @@ const backgroundSessionIndicatorViewStateToProps: { secondaryAction: ViewBackgroundSessionsButton, }, }, - [BackgroundSessionViewState.BackgroundLoading]: { + [SessionState.BackgroundLoading]: { button: { iconType: EuiLoadingSpinner, 'aria-label': i18n.translate( @@ -165,7 +190,7 @@ const backgroundSessionIndicatorViewStateToProps: { secondaryAction: ViewBackgroundSessionsButton, }, }, - [BackgroundSessionViewState.BackgroundCompleted]: { + [SessionState.BackgroundCompleted]: { button: { color: 'success', iconType: 'checkInCircleFilled', @@ -192,7 +217,7 @@ const backgroundSessionIndicatorViewStateToProps: { primaryAction: ViewBackgroundSessionsButton, }, }, - [BackgroundSessionViewState.Restored]: { + [SessionState.Restored]: { button: { color: 'warning', iconType: 'refresh', @@ -217,6 +242,25 @@ const backgroundSessionIndicatorViewStateToProps: { secondaryAction: ViewBackgroundSessionsButton, }, }, + [SessionState.Canceled]: { + button: { + color: 'subdued', + iconType: 'refresh', + 'aria-label': i18n.translate('xpack.data.backgroundSessionIndicator.canceledIconAriaLabel', { + defaultMessage: 'Canceled', + }), + tooltipText: i18n.translate('xpack.data.backgroundSessionIndicator.canceledTooltipText', { + defaultMessage: 'Search was canceled', + }), + }, + popover: { + text: i18n.translate('xpack.data.backgroundSessionIndicator.canceledText', { + defaultMessage: 'Search was canceled', + }), + primaryAction: RefreshButton, + secondaryAction: ViewBackgroundSessionsButton, + }, + }, }; const VerticalDivider: React.FC = () => ( @@ -228,7 +272,9 @@ export const BackgroundSessionIndicator: React.FC setIsPopoverOpen((isOpen) => !isOpen); const closePopover = () => setIsPopoverOpen(false); - const { button, popover } = backgroundSessionIndicatorViewStateToProps[props.state]; + if (!backgroundSessionIndicatorViewStateToProps[props.state]) return null; + + const { button, popover } = backgroundSessionIndicatorViewStateToProps[props.state]!; return ( + } @@ -255,6 +303,7 @@ export const BackgroundSessionIndicator: React.FC diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx index d97d10512783c..e08773c6a8a76 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx @@ -5,17 +5,37 @@ */ import React from 'react'; -import { render, waitFor } from '@testing-library/react'; +import { render, waitFor, screen, act } from '@testing-library/react'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { createConnectedBackgroundSessionIndicator } from './connected_background_session_indicator'; import { BehaviorSubject } from 'rxjs'; -import { ISessionService } from '../../../../../../../src/plugins/data/public'; +import { + ISessionService, + RefreshInterval, + SessionState, + TimefilterContract, +} from '../../../../../../../src/plugins/data/public'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; -const sessionService = dataPluginMock.createStartContract().search - .session as jest.Mocked; +const coreStart = coreMock.createStart(); +const dataStart = dataPluginMock.createStartContract(); +const sessionService = dataStart.search.session as jest.Mocked; + +const refreshInterval$ = new BehaviorSubject({ value: 0, pause: true }); +const timeFilter = dataStart.query.timefilter.timefilter as jest.Mocked; +timeFilter.getRefreshIntervalUpdate$.mockImplementation(() => refreshInterval$); +timeFilter.getRefreshInterval.mockImplementation(() => refreshInterval$.getValue()); + +beforeEach(() => { + refreshInterval$.next({ value: 0, pause: true }); +}); test("shouldn't show indicator in case no active search session", async () => { - const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ sessionService }); + const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ + sessionService, + application: coreStart.application, + timeFilter, + }); const { getByTestId, container } = render(); // make sure `backgroundSessionIndicator` isn't appearing after some time (lazy-loading) @@ -26,11 +46,36 @@ test("shouldn't show indicator in case no active search session", async () => { }); test('should show indicator in case there is an active search session', async () => { - const session$ = new BehaviorSubject('session_id'); - sessionService.getSession$.mockImplementation(() => session$); - sessionService.getSessionId.mockImplementation(() => session$.getValue()); - const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ sessionService }); + const state$ = new BehaviorSubject(SessionState.Loading); + const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ + sessionService: { ...sessionService, state$ }, + application: coreStart.application, + timeFilter, + }); const { getByTestId } = render(); await waitFor(() => getByTestId('backgroundSessionIndicator')); }); + +test('should be disabled during auto-refresh', async () => { + const state$ = new BehaviorSubject(SessionState.Loading); + const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ + sessionService: { ...sessionService, state$ }, + application: coreStart.application, + timeFilter, + }); + + render(); + + await waitFor(() => screen.getByTestId('backgroundSessionIndicator')); + + expect( + screen.getByTestId('backgroundSessionIndicator').querySelector('button') + ).not.toBeDisabled(); + + act(() => { + refreshInterval$.next({ value: 0, pause: false }); + }); + + expect(screen.getByTestId('backgroundSessionIndicator').querySelector('button')).toBeDisabled(); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx index d097a1aecb66a..b80295d87d202 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx @@ -5,28 +5,67 @@ */ import React from 'react'; +import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; import useObservable from 'react-use/lib/useObservable'; -import { distinctUntilChanged, map } from 'rxjs/operators'; +import { i18n } from '@kbn/i18n'; import { BackgroundSessionIndicator } from '../background_session_indicator'; -import { ISessionService } from '../../../../../../../src/plugins/data/public/'; -import { BackgroundSessionViewState } from './background_session_view_state'; +import { ISessionService, TimefilterContract } from '../../../../../../../src/plugins/data/public/'; +import { RedirectAppLinks } from '../../../../../../../src/plugins/kibana_react/public'; +import { ApplicationStart } from '../../../../../../../src/core/public'; export interface BackgroundSessionIndicatorDeps { sessionService: ISessionService; + timeFilter: TimefilterContract; + application: ApplicationStart; } export const createConnectedBackgroundSessionIndicator = ({ sessionService, + application, + timeFilter, }: BackgroundSessionIndicatorDeps): React.FC => { - const sessionId$ = sessionService.getSession$(); - const hasActiveSession$ = sessionId$.pipe( - map((sessionId) => !!sessionId), - distinctUntilChanged() - ); + const isAutoRefreshEnabled = () => !timeFilter.getRefreshInterval().pause; + const isAutoRefreshEnabled$ = timeFilter + .getRefreshIntervalUpdate$() + .pipe(map(isAutoRefreshEnabled), distinctUntilChanged()); return () => { - const isSession = useObservable(hasActiveSession$, !!sessionService.getSessionId()); - if (!isSession) return null; - return ; + const state = useObservable(sessionService.state$.pipe(debounceTime(500))); + const autoRefreshEnabled = useObservable(isAutoRefreshEnabled$, isAutoRefreshEnabled()); + let disabled = false; + let disabledReasonText: string = ''; + + if (autoRefreshEnabled) { + disabled = true; + disabledReasonText = i18n.translate( + 'xpack.data.backgroundSessionIndicator.disabledDueToAutoRefreshMessage', + { + defaultMessage: 'Send to background is not available when auto refresh is enabled.', + } + ); + } + + if (!state) return null; + return ( + + { + sessionService.save(); + }} + onSaveResults={() => { + sessionService.save(); + }} + onRefresh={() => { + sessionService.refresh(); + }} + onCancel={() => { + sessionService.cancel(); + }} + disabled={disabled} + disabledReasonText={disabledReasonText} + /> + + ); }; }; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts index adbb6edbbfcf3..223a0537129df 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/index.ts @@ -8,4 +8,3 @@ export { BackgroundSessionIndicatorDeps, createConnectedBackgroundSessionIndicator, } from './connected_background_session_indicator'; -export { BackgroundSessionViewState } from './background_session_view_state'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/constants.ts new file mode 100644 index 0000000000000..b2b76d5b987b9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/constants.ts @@ -0,0 +1,28 @@ +/* + * 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'; + +export const INDEXING_STATUS_PROGRESS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.indexingStatus.progress.title', + { + defaultMessage: 'Indexing progress', + } +); + +export const INDEXING_STATUS_HAS_ERRORS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.indexingStatus.hasErrors.title', + { + defaultMessage: 'Several documents have field conversion errors.', + } +); + +export const INDEXING_STATUS_HAS_ERRORS_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.indexingStatus.hasErrors.button', + { + defaultMessage: 'View errors', + } +); diff --git a/x-pack/plugins/telemetry_collection_xpack/common/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/index.ts similarity index 68% rename from x-pack/plugins/telemetry_collection_xpack/common/index.ts rename to x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/index.ts index 2b08ebe2e7bbf..4a97f11e8f0ee 100644 --- a/x-pack/plugins/telemetry_collection_xpack/common/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const PLUGIN_ID = 'telemetryCollectionXpack'; -export const PLUGIN_NAME = 'telemetry_collection_xpack'; +export { IndexingStatus } from './indexing_status'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx new file mode 100644 index 0000000000000..097c3bbc8e9ff --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx @@ -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. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiPanel } from '@elastic/eui'; + +import { IndexingStatusContent } from './indexing_status_content'; +import { IndexingStatusErrors } from './indexing_status_errors'; +import { IndexingStatusFetcher } from './indexing_status_fetcher'; +import { IndexingStatus } from './indexing_status'; + +describe('IndexingStatus', () => { + const getItemDetailPath = jest.fn(); + const getStatusPath = jest.fn(); + const onComplete = jest.fn(); + const setGlobalIndexingStatus = jest.fn(); + + const props = { + percentageComplete: 50, + numDocumentsWithErrors: 1, + activeReindexJobId: 12, + viewLinkPath: '/path', + itemId: '1', + getItemDetailPath, + getStatusPath, + onComplete, + setGlobalIndexingStatus, + }; + + it('renders', () => { + const wrapper = shallow(); + const fetcher = wrapper.find(IndexingStatusFetcher).prop('children')( + props.percentageComplete, + props.numDocumentsWithErrors + ); + + expect(shallow(fetcher).find(EuiPanel)).toHaveLength(1); + expect(shallow(fetcher).find(IndexingStatusContent)).toHaveLength(1); + }); + + it('renders errors', () => { + const wrapper = shallow(); + const fetcher = wrapper.find(IndexingStatusFetcher).prop('children')(100, 1); + expect(shallow(fetcher).find(IndexingStatusErrors)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx new file mode 100644 index 0000000000000..beec0babea590 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx @@ -0,0 +1,44 @@ +/* + * 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 { EuiPanel, EuiSpacer } from '@elastic/eui'; + +import { IndexingStatusContent } from './indexing_status_content'; +import { IndexingStatusErrors } from './indexing_status_errors'; +import { IndexingStatusFetcher } from './indexing_status_fetcher'; + +import { IIndexingStatus } from '../types'; + +export interface IIndexingStatusProps extends IIndexingStatus { + viewLinkPath: string; + itemId: string; + getItemDetailPath?(itemId: string): string; + getStatusPath(itemId: string, activeReindexJobId: number): string; + onComplete(numDocumentsWithErrors: number): void; + setGlobalIndexingStatus?(activeReindexJob: IIndexingStatus): void; +} + +export const IndexingStatus: React.FC = (props) => ( + + {(percentageComplete, numDocumentsWithErrors) => ( +
+ {percentageComplete < 100 && ( + + + + )} + {percentageComplete === 100 && numDocumentsWithErrors > 0 && ( + <> + + + + )} +
+ )} +
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx new file mode 100644 index 0000000000000..9fe0e890e6943 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiProgress, EuiTitle } from '@elastic/eui'; + +import { IndexingStatusContent } from './indexing_status_content'; + +describe('IndexingStatusContent', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTitle)).toHaveLength(1); + expect(wrapper.find(EuiProgress)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.tsx new file mode 100644 index 0000000000000..a0c67388621a8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiProgress, EuiSpacer, EuiTitle } from '@elastic/eui'; + +import { INDEXING_STATUS_PROGRESS_TITLE } from './constants'; + +interface IIndexingStatusContentProps { + percentageComplete: number; +} + +export const IndexingStatusContent: React.FC = ({ + percentageComplete, +}) => ( +
+ +

{INDEXING_STATUS_PROGRESS_TITLE}

+
+ + +
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx new file mode 100644 index 0000000000000..fc706aee659a5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx @@ -0,0 +1,25 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { EuiCallOut, EuiButton } from '@elastic/eui'; + +import { EuiLinkTo } from '../react_router_helpers'; + +import { IndexingStatusErrors } from './indexing_status_errors'; + +describe('IndexingStatusErrors', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiButton)).toHaveLength(1); + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + expect(wrapper.find(EuiLinkTo)).toHaveLength(1); + expect(wrapper.find(EuiLinkTo).prop('to')).toEqual('/path'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx new file mode 100644 index 0000000000000..a928400b2338c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.tsx @@ -0,0 +1,31 @@ +/* + * 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 { EuiButton, EuiCallOut } from '@elastic/eui'; + +import { EuiLinkTo } from '../react_router_helpers'; + +import { INDEXING_STATUS_HAS_ERRORS_TITLE, INDEXING_STATUS_HAS_ERRORS_BUTTON } from './constants'; + +interface IIndexingStatusErrorsProps { + viewLinkPath: string; +} + +export const IndexingStatusErrors: React.FC = ({ viewLinkPath }) => ( + +

{INDEXING_STATUS_HAS_ERRORS_TITLE}

+ + {INDEXING_STATUS_HAS_ERRORS_BUTTON} + +
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx new file mode 100644 index 0000000000000..cb7c82f91ed61 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_fetcher.tsx @@ -0,0 +1,64 @@ +/* + * 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, { useEffect, useState, useRef } from 'react'; + +import { HttpLogic } from '../http'; +import { flashAPIErrors } from '../flash_messages'; + +interface IIndexingStatusFetcherProps { + activeReindexJobId: number; + itemId: string; + percentageComplete: number; + numDocumentsWithErrors: number; + onComplete?(numDocumentsWithErrors: number): void; + getStatusPath(itemId: string, activeReindexJobId: number): string; + children(percentageComplete: number, numDocumentsWithErrors: number): JSX.Element; +} + +export const IndexingStatusFetcher: React.FC = ({ + activeReindexJobId, + children, + getStatusPath, + itemId, + numDocumentsWithErrors, + onComplete, + percentageComplete = 0, +}) => { + const [indexingStatus, setIndexingStatus] = useState({ + numDocumentsWithErrors, + percentageComplete, + }); + const pollingInterval = useRef(); + + useEffect(() => { + pollingInterval.current = window.setInterval(async () => { + try { + const response = await HttpLogic.values.http.get(getStatusPath(itemId, activeReindexJobId)); + if (response.percentageComplete >= 100) { + clearInterval(pollingInterval.current); + } + setIndexingStatus({ + percentageComplete: response.percentageComplete, + numDocumentsWithErrors: response.numDocumentsWithErrors, + }); + if (response.percentageComplete >= 100 && onComplete) { + onComplete(response.numDocumentsWithErrors); + } + } catch (e) { + flashAPIErrors(e); + } + }, 3000); + + return () => { + if (pollingInterval.current) { + clearInterval(pollingInterval.current); + } + }; + }, []); + + return children(indexingStatus.percentageComplete, indexingStatus.numDocumentsWithErrors); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts new file mode 100644 index 0000000000000..3866d1a7199e4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -0,0 +1,11 @@ +/* + * 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 IIndexingStatus { + percentageComplete: number; + numDocumentsWithErrors: number; + activeReindexJobId: number; +} diff --git a/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts index 6cf0be9fd1f31..b4b7a489c6ea3 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts @@ -61,7 +61,7 @@ describe('Telemetry helpers', () => { expect(incrementCounterMock).toHaveBeenCalledWith( 'app_search_telemetry', 'app_search_telemetry', - 'ui_clicked.button' + ['ui_clicked.button'] ); expect(response).toEqual({ success: true }); }); diff --git a/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts index cd8ad72bf8358..deba94fc0bd5e 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.ts @@ -55,7 +55,7 @@ export async function incrementUICounter({ await internalRepository.incrementCounter( id, id, - `${uiAction}.${metric}` // e.g., ui_viewed.setup_guide + [`${uiAction}.${metric}`] // e.g., ui_viewed.setup_guide ); return { success: true }; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts index 62f4dceeac363..d97a587e57ff2 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts @@ -19,6 +19,9 @@ import { registerAccountPrepareSourcesRoute, registerAccountSourceSearchableRoute, registerAccountSourceDisplaySettingsConfig, + registerAccountSourceSchemasRoute, + registerAccountSourceReindexJobRoute, + registerAccountSourceReindexJobStatusRoute, registerOrgSourcesRoute, registerOrgSourcesStatusRoute, registerOrgSourceRoute, @@ -31,6 +34,9 @@ import { registerOrgPrepareSourcesRoute, registerOrgSourceSearchableRoute, registerOrgSourceDisplaySettingsConfig, + registerOrgSourceSchemasRoute, + registerOrgSourceReindexJobRoute, + registerOrgSourceReindexJobStatusRoute, registerOrgSourceOauthConfigurationsRoute, registerOrgSourceOauthConfigurationRoute, } from './sources'; @@ -523,6 +529,139 @@ describe('sources routes', () => { }); }); + describe('GET /api/workplace_search/account/sources/{id}/schemas', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/account/sources/{id}/schemas', + payload: 'params', + }); + + registerAccountSourceSchemasRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + id: '123', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources/123/schemas', + }); + }); + }); + + describe('POST /api/workplace_search/account/sources/{id}/schemas', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/workplace_search/account/sources/{id}/schemas', + payload: 'body', + }); + + registerAccountSourceSchemasRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + const mockRequest = { + params: { id: '123' }, + body: {}, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources/123/schemas', + body: mockRequest.body, + }); + }); + }); + + describe('GET /api/workplace_search/account/sources/{source_id}/reindex_job/{job_id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/account/sources/{source_id}/reindex_job/{job_id}', + payload: 'params', + }); + + registerAccountSourceReindexJobRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + source_id: '123', + job_id: '345', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources/123/reindex_job/345', + }); + }); + }); + + describe('GET /api/workplace_search/account/sources/{source_id}/reindex_job/{job_id}/status', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/account/sources/{source_id}/reindex_job/{job_id}/status', + payload: 'params', + }); + + registerAccountSourceReindexJobStatusRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + source_id: '123', + job_id: '345', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources/123/reindex_job/345/status', + }); + }); + }); + describe('GET /api/workplace_search/org/sources', () => { let mockRouter: MockRouter; @@ -1000,6 +1139,139 @@ describe('sources routes', () => { }); }); + describe('GET /api/workplace_search/org/sources/{id}/schemas', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/sources/{id}/schemas', + payload: 'params', + }); + + registerOrgSourceSchemasRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + id: '123', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources/123/schemas', + }); + }); + }); + + describe('POST /api/workplace_search/org/sources/{id}/schemas', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/workplace_search/org/sources/{id}/schemas', + payload: 'body', + }); + + registerOrgSourceSchemasRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + const mockRequest = { + params: { id: '123' }, + body: {}, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources/123/schemas', + body: mockRequest.body, + }); + }); + }); + + describe('GET /api/workplace_search/org/sources/{source_id}/reindex_job/{job_id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/sources/{source_id}/reindex_job/{job_id}', + payload: 'params', + }); + + registerOrgSourceReindexJobRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + source_id: '123', + job_id: '345', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources/123/reindex_job/345', + }); + }); + }); + + describe('GET /api/workplace_search/org/sources/{source_id}/reindex_job/{job_id}/status', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/sources/{source_id}/reindex_job/{job_id}/status', + payload: 'params', + }); + + registerOrgSourceReindexJobStatusRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + source_id: '123', + job_id: '345', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources/123/reindex_job/345/status', + }); + }); + }); + describe('GET /api/workplace_search/org/settings/connectors', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index d43a4252c7e1f..9beac109be510 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -339,6 +339,89 @@ export function registerAccountSourceDisplaySettingsConfig({ ); } +export function registerAccountSourceSchemasRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/account/sources/{id}/schemas', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/sources/${request.params.id}/schemas`, + })(context, request, response); + } + ); + + router.post( + { + path: '/api/workplace_search/account/sources/{id}/schemas', + validate: { + body: schema.object({}), + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/sources/${request.params.id}/schemas`, + body: request.body, + })(context, request, response); + } + ); +} + +export function registerAccountSourceReindexJobRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/account/sources/{source_id}/reindex_job/{job_id}', + validate: { + params: schema.object({ + source_id: schema.string(), + job_id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/sources/${request.params.source_id}/reindex_job/${request.params.job_id}`, + })(context, request, response); + } + ); +} + +export function registerAccountSourceReindexJobStatusRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/account/sources/{source_id}/reindex_job/{job_id}/status', + validate: { + params: schema.object({ + source_id: schema.string(), + job_id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/sources/${request.params.source_id}/reindex_job/${request.params.job_id}/status`, + })(context, request, response); + } + ); +} + export function registerOrgSourcesRoute({ router, enterpriseSearchRequestHandler, @@ -638,6 +721,89 @@ export function registerOrgSourceDisplaySettingsConfig({ ); } +export function registerOrgSourceSchemasRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/sources/{id}/schemas', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/org/sources/${request.params.id}/schemas`, + })(context, request, response); + } + ); + + router.post( + { + path: '/api/workplace_search/org/sources/{id}/schemas', + validate: { + body: schema.object({}), + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/org/sources/${request.params.id}/schemas`, + body: request.body, + })(context, request, response); + } + ); +} + +export function registerOrgSourceReindexJobRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/sources/{source_id}/reindex_job/{job_id}', + validate: { + params: schema.object({ + source_id: schema.string(), + job_id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/org/sources/${request.params.source_id}/reindex_job/${request.params.job_id}`, + })(context, request, response); + } + ); +} + +export function registerOrgSourceReindexJobStatusRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/sources/{source_id}/reindex_job/{job_id}/status', + validate: { + params: schema.object({ + source_id: schema.string(), + job_id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/org/sources/${request.params.source_id}/reindex_job/${request.params.job_id}/status`, + })(context, request, response); + } + ); +} + export function registerOrgSourceOauthConfigurationsRoute({ router, enterpriseSearchRequestHandler, @@ -741,6 +907,9 @@ export const registerSourcesRoutes = (dependencies: RouteDependencies) => { registerAccountPrepareSourcesRoute(dependencies); registerAccountSourceSearchableRoute(dependencies); registerAccountSourceDisplaySettingsConfig(dependencies); + registerAccountSourceSchemasRoute(dependencies); + registerAccountSourceReindexJobRoute(dependencies); + registerAccountSourceReindexJobStatusRoute(dependencies); registerOrgSourcesRoute(dependencies); registerOrgSourcesStatusRoute(dependencies); registerOrgSourceRoute(dependencies); @@ -753,6 +922,9 @@ export const registerSourcesRoutes = (dependencies: RouteDependencies) => { registerOrgPrepareSourcesRoute(dependencies); registerOrgSourceSearchableRoute(dependencies); registerOrgSourceDisplaySettingsConfig(dependencies); + registerOrgSourceSchemasRoute(dependencies); + registerOrgSourceReindexJobRoute(dependencies); + registerOrgSourceReindexJobStatusRoute(dependencies); registerOrgSourceOauthConfigurationsRoute(dependencies); registerOrgSourceOauthConfigurationRoute(dependencies); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index 766ad961674af..ed91c1cb1479c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -186,16 +186,9 @@ export const FleetAppContext: React.FC<{ /** For testing purposes only */ routerHistory?: History; }> = memo( - ({ - children, - startServices, - config, - history, - kibanaVersion, - extensions, - routerHistory = createHashHistory(), - }) => { + ({ children, startServices, config, history, kibanaVersion, extensions, routerHistory }) => { const isDarkMode = useObservable(startServices.uiSettings.get$('theme:darkMode')); + const [routerHistoryInstance] = useState(routerHistory || createHashHistory()); return ( @@ -207,7 +200,7 @@ export const FleetAppContext: React.FC<{ - + {children} diff --git a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts index ecd4227a54b65..2fce7f8f5e825 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts @@ -78,8 +78,8 @@ export const pagePathGetters: { `/policies/${policyId}/edit-integration/${packagePolicyId}`, fleet: () => '/fleet', fleet_agent_list: ({ kuery }) => `/fleet/agents${kuery ? `?kuery=${kuery}` : ''}`, - fleet_agent_details: ({ agentId, tabId }) => - `/fleet/agents/${agentId}${tabId ? `/${tabId}` : ''}`, + fleet_agent_details: ({ agentId, tabId, logQuery }) => + `/fleet/agents/${agentId}${tabId ? `/${tabId}` : ''}${logQuery ? `?_q=${logQuery}` : ''}`, fleet_enrollment_tokens: () => '/fleet/enrollment-tokens', data_streams: () => '/data-streams', }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx index 487eac6779dd5..2b1eb8e1ce984 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/actions_menu.tsx @@ -73,7 +73,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{ )} = memo(({ agent, agentPolicy }) => { - const { getHref } = useLink(); - const kibanaVersion = useKibanaVersion(); - return ( - - {[ - { - title: i18n.translate('xpack.fleet.agentDetails.hostNameLabel', { - defaultMessage: 'Host name', - }), - description: - typeof agent.local_metadata.host === 'object' && - typeof agent.local_metadata.host.hostname === 'string' - ? agent.local_metadata.host.hostname - : '-', - }, - { - title: i18n.translate('xpack.fleet.agentDetails.hostIdLabel', { - defaultMessage: 'Agent ID', - }), - description: agent.id, - }, - { - title: i18n.translate('xpack.fleet.agentDetails.statusLabel', { - defaultMessage: 'Status', - }), - description: , - }, - { - title: i18n.translate('xpack.fleet.agentDetails.agentPolicyLabel', { - defaultMessage: 'Agent policy', - }), - description: agentPolicy ? ( - - {agentPolicy.name || agent.policy_id} - - ) : ( - agent.policy_id || '-' - ), - }, - { - title: i18n.translate('xpack.fleet.agentDetails.versionLabel', { - defaultMessage: 'Agent version', - }), - description: - typeof agent.local_metadata.elastic === 'object' && - typeof agent.local_metadata.elastic.agent === 'object' && - typeof agent.local_metadata.elastic.agent.version === 'string' ? ( - - - {agent.local_metadata.elastic.agent.version} - - {isAgentUpgradeable(agent, kibanaVersion) ? ( - - - -   - - - - ) : null} - - ) : ( - '-' - ), - }, - { - title: i18n.translate('xpack.fleet.agentDetails.releaseLabel', { - defaultMessage: 'Agent release', - }), - description: - typeof agent.local_metadata.elastic === 'object' && - typeof agent.local_metadata.elastic.agent === 'object' && - typeof agent.local_metadata.elastic.agent.snapshot === 'boolean' - ? agent.local_metadata.elastic.agent.snapshot === true - ? 'snapshot' - : 'stable' - : '-', - }, - { - title: i18n.translate('xpack.fleet.agentDetails.logLevel', { - defaultMessage: 'Log level', - }), - description: - typeof agent.local_metadata.elastic === 'object' && - typeof agent.local_metadata.elastic.agent === 'object' && - typeof agent.local_metadata.elastic.agent.log_level === 'string' - ? agent.local_metadata.elastic.agent.log_level - : '-', - }, - { - title: i18n.translate('xpack.fleet.agentDetails.platformLabel', { - defaultMessage: 'Platform', - }), - description: - typeof agent.local_metadata.os === 'object' && - typeof agent.local_metadata.os.platform === 'string' - ? agent.local_metadata.os.platform - : '-', - }, - ].map(({ title, description }) => { - return ( - - - {title} - - - {description} - - - ); - })} - - ); -}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx new file mode 100644 index 0000000000000..0cad0b4d487d0 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx @@ -0,0 +1,159 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiAccordion, + EuiTitle, + EuiPanel, + EuiButtonIcon, + EuiBasicTable, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +import { Agent, AgentPolicy, PackagePolicy, PackagePolicyInput } from '../../../../../types'; +import { useLink } from '../../../../../hooks'; +import { PackageIcon } from '../../../../../components'; +import { displayInputType, getLogsQueryByInputType } from './input_type_utils'; + +const StyledEuiAccordion = styled(EuiAccordion)` + .ingest-integration-title-button { + padding: ${(props) => props.theme.eui.paddingSizes.m} + ${(props) => props.theme.eui.paddingSizes.m}; + border-bottom: 1px solid ${(props) => props.theme.eui.euiColorLightShade}; + } +`; + +const CollapsablePanel: React.FC<{ id: string; title: React.ReactNode }> = ({ + id, + title, + children, +}) => { + return ( + + + {children} + + + ); +}; + +export const AgentDetailsIntegration: React.FunctionComponent<{ + agent: Agent; + agentPolicy: AgentPolicy; + packagePolicy: PackagePolicy; +}> = memo(({ agent, agentPolicy, packagePolicy }) => { + const { getHref } = useLink(); + + const inputs = useMemo(() => { + return packagePolicy.inputs.filter((input) => input.enabled); + }, [packagePolicy.inputs]); + + const columns = [ + { + field: 'type', + width: '100%', + name: i18n.translate('xpack.fleet.agentDetailsIntegrations.inputTypeLabel', { + defaultMessage: 'Input', + }), + render: (inputType: string) => { + return displayInputType(inputType); + }, + }, + { + name: i18n.translate('xpack.fleet.agentDetailsIntegrations.actionsLabel', { + defaultMessage: 'Actions', + }), + field: 'type', + width: 'auto', + render: (inputType: string) => { + return ( + + ); + }, + }, + ]; + + return ( + +

+ + + {packagePolicy.package ? ( + + ) : ( + + )} + + + + {packagePolicy.name} + + + +

+ + } + > + tableLayout="auto" items={inputs} columns={columns} /> +
+ ); +}); + +export const AgentDetailsIntegrationsSection: React.FunctionComponent<{ + agent: Agent; + agentPolicy?: AgentPolicy; +}> = memo(({ agent, agentPolicy }) => { + if (!agentPolicy || !agentPolicy.package_policies) { + return null; + } + + return ( + + {(agentPolicy.package_policies as PackagePolicy[]).map((packagePolicy) => { + return ( + + + + ); + })} + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx new file mode 100644 index 0000000000000..a19f6658ef93f --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx @@ -0,0 +1,185 @@ +/* + * 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, { memo } from 'react'; +import { + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { EuiIcon } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Agent, AgentPolicy } from '../../../../../types'; +import { useKibanaVersion, useLink } from '../../../../../hooks'; +import { isAgentUpgradeable } from '../../../../../services'; +import { AgentPolicyPackageBadges } from '../../../components/agent_policy_package_badges'; +import { LinkAndRevision } from '../../../../../components'; + +export const AgentDetailsOverviewSection: React.FunctionComponent<{ + agent: Agent; + agentPolicy?: AgentPolicy; +}> = memo(({ agent, agentPolicy }) => { + const { getHref } = useLink(); + const kibanaVersion = useKibanaVersion(); + return ( + + + {[ + { + title: i18n.translate('xpack.fleet.agentDetails.hostIdLabel', { + defaultMessage: 'Agent ID', + }), + description: agent.id, + }, + { + title: i18n.translate('xpack.fleet.agentDetails.agentPolicyLabel', { + defaultMessage: 'Agent policy', + }), + description: agentPolicy ? ( + + {agentPolicy.name || agentPolicy.id} + + ) : ( + agent.policy_id || '-' + ), + }, + { + title: i18n.translate('xpack.fleet.agentDetails.versionLabel', { + defaultMessage: 'Agent version', + }), + description: + typeof agent.local_metadata?.elastic?.agent?.version === 'string' ? ( + + + {agent.local_metadata.elastic.agent.version} + + {isAgentUpgradeable(agent, kibanaVersion) ? ( + + + +   + + + + ) : null} + + ) : ( + '-' + ), + }, + { + title: i18n.translate('xpack.fleet.agentDetails.enrollmentTokenLabel', { + defaultMessage: 'Enrollment token', + }), + description: '-', // Fixme when we have the enrollment tokenhttps://github.com/elastic/kibana/issues/61269 + }, + { + title: i18n.translate('xpack.fleet.agentDetails.integrationsLabel', { + defaultMessage: 'Integrations', + }), + description: agent.policy_id ? ( + + ) : null, + }, + { + title: i18n.translate('xpack.fleet.agentDetails.hostNameLabel', { + defaultMessage: 'Host name', + }), + description: + typeof agent.local_metadata?.host?.hostname === 'string' + ? agent.local_metadata.host.hostname + : '-', + }, + { + title: i18n.translate('xpack.fleet.agentDetails.logLevel', { + defaultMessage: 'Logging level', + }), + description: + typeof agent.local_metadata?.elastic?.agent?.log_level === 'string' + ? agent.local_metadata.elastic.agent.log_level + : '-', + }, + { + title: i18n.translate('xpack.fleet.agentDetails.releaseLabel', { + defaultMessage: 'Agent release', + }), + description: + typeof agent.local_metadata?.elastic?.agent?.snapshot === 'boolean' + ? agent.local_metadata.elastic.agent.snapshot === true + ? 'snapshot' + : 'stable' + : '-', + }, + { + title: i18n.translate('xpack.fleet.agentDetails.platformLabel', { + defaultMessage: 'Platform', + }), + description: + typeof agent.local_metadata?.os?.platform === 'string' + ? agent.local_metadata.os.platform + : '-', + }, + { + title: i18n.translate('xpack.fleet.agentDetails.monitorLogsLabel', { + defaultMessage: 'Monitor logs', + }), + description: agentPolicy?.monitoring_enabled?.includes('logs') ? ( + + ) : ( + + ), + }, + { + title: i18n.translate('xpack.fleet.agentDetails.monitorMetricsLabel', { + defaultMessage: 'Monitor metrics', + }), + description: agentPolicy?.monitoring_enabled?.includes('metrics') ? ( + + ) : ( + + ), + }, + ].map(({ title, description }) => { + return ( + + + + {title} + + + {description} + + + + ); + })} + + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/index.tsx new file mode 100644 index 0000000000000..0b83fb4cc64e1 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/index.tsx @@ -0,0 +1,47 @@ +/* + * 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, { memo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Agent, AgentPolicy } from '../../../../../types'; +import { AgentDetailsOverviewSection } from './agent_details_overview'; +import { AgentDetailsIntegrationsSection } from './agent_details_integrations'; + +export const AgentDetailsContent: React.FunctionComponent<{ + agent: Agent; + agentPolicy?: AgentPolicy; +}> = memo(({ agent, agentPolicy }) => { + return ( + <> + + + +

+ +

+
+ + +
+ + +

+ +

+
+ + +
+
+ + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/input_type_utils.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/input_type_utils.ts new file mode 100644 index 0000000000000..62b7a294e1750 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/input_type_utils.ts @@ -0,0 +1,43 @@ +/* + * 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 { + STATE_DATASET_FIELD, + AGENT_DATASET_FILEBEAT, + AGENT_DATASET_METRICBEAT, +} from '../agent_logs/constants'; + +export function displayInputType(inputType: string): string { + if (inputType === 'logfile') { + return i18n.translate('xpack.fleet.agentDetailsIntegrations.inputTypeLogText', { + defaultMessage: 'Logs', + }); + } + if (inputType === 'endpoint') { + return i18n.translate('xpack.fleet.agentDetailsIntegrations.inputTypeEndpointText', { + defaultMessage: 'Endpoint', + }); + } + if (inputType.match(/\/metrics$/)) { + return i18n.translate('xpack.fleet.agentDetailsIntegrations.inputTypeMetricsText', { + defaultMessage: 'Metrics', + }); + } + + return inputType; +} + +export function getLogsQueryByInputType(inputType: string) { + if (inputType === 'logfile') { + return `(${STATE_DATASET_FIELD}:!(${AGENT_DATASET_FILEBEAT}))`; + } + if (inputType.match(/\/metrics$/)) { + return `(${STATE_DATASET_FIELD}:!(${AGENT_DATASET_METRICBEAT}))`; + } + + return ''; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx index 89fe1a916605d..4ee1618a38584 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/constants.tsx @@ -7,6 +7,8 @@ import { AgentLogsState } from './agent_logs'; export const AGENT_LOG_INDEX_PATTERN = 'logs-elastic_agent-*,logs-elastic_agent.*-*'; export const AGENT_DATASET = 'elastic_agent'; +export const AGENT_DATASET_FILEBEAT = 'elastic_agent.filebeat'; +export const AGENT_DATASET_METRICBEAT = 'elastic_agent.metricbeat'; export const AGENT_DATASET_PATTERN = 'elastic_agent.*'; export const AGENT_ID_FIELD = { name: 'elastic_agent.id', @@ -34,6 +36,8 @@ export const DEFAULT_LOGS_STATE: AgentLogsState = { query: '', }; +export const STATE_DATASET_FIELD = 'datasets'; + export const AGENT_LOG_LEVELS = { ERROR: 'error', WARNING: 'warning', diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx index f3714bbb53223..34893dccd93a4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx @@ -5,7 +5,6 @@ */ import React, { useMemo, useCallback } from 'react'; import { useRouteMatch, Switch, Route, useLocation } from 'react-router-dom'; -import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, @@ -17,7 +16,7 @@ import { EuiDescriptionListDescription, } from '@elastic/eui'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiIconTip } from '@elastic/eui'; import { Agent, AgentPolicy, AgentDetailsReassignPolicyAction } from '../../../types'; @@ -38,12 +37,6 @@ import { AgentLogs, AgentDetailsActionMenu, AgentDetailsContent } from './compon import { useIntraAppState } from '../../../hooks/use_intra_app_state'; import { isAgentUpgradeable } from '../../../services'; -const Divider = styled.div` - width: 0; - height: 100%; - border-left: ${(props) => props.theme.eui.euiBorderThin}; -`; - export const AgentDetailsPage: React.FunctionComponent = () => { const { params: { agentId, tabId = '' }, @@ -78,6 +71,8 @@ export const AgentDetailsPage: React.FunctionComponent = () => { } }, [routeState, navigateToApp]); + const host = agentData?.item?.local_metadata?.host; + const headerLeftContent = useMemo( () => ( @@ -99,9 +94,8 @@ export const AgentDetailsPage: React.FunctionComponent = () => {

{isLoading && isInitialRequest ? ( - ) : typeof agentData?.item?.local_metadata?.host === 'object' && - typeof agentData?.item?.local_metadata?.host?.hostname === 'string' ? ( - agentData.item.local_metadata.host.hostname + ) : typeof host === 'object' && typeof host?.hostname === 'string' ? ( + host.hostname ) : ( { ), - [agentData?.item?.local_metadata?.host, agentId, getHref, isInitialRequest, isLoading] + [host, agentId, getHref, isInitialRequest, isLoading] ); const headerRightContent = useMemo( () => agentData && agentData.item ? ( - + {[ { label: i18n.translate('xpack.fleet.agentDetails.statusLabel', { @@ -130,7 +124,16 @@ export const AgentDetailsPage: React.FunctionComponent = () => { }), content: , }, - { isDivider: true }, + { + label: i18n.translate('xpack.fleet.agentDetails.lastActivityLabel', { + defaultMessage: 'Last activity', + }), + content: agentData.item.last_checkin ? ( + + ) : ( + '-' + ), + }, { label: i18n.translate('xpack.fleet.agentDetails.policyLabel', { defaultMessage: 'Policy', @@ -148,7 +151,6 @@ export const AgentDetailsPage: React.FunctionComponent = () => { agentData.item.policy_id || '-' ), }, - { isDivider: true }, { label: i18n.translate('xpack.fleet.agentDetails.agentVersionLabel', { defaultMessage: 'Agent version', @@ -187,7 +189,6 @@ export const AgentDetailsPage: React.FunctionComponent = () => { '-' ), }, - { isDivider: true }, { content: ( { }, ].map((item, index) => ( - {item.isDivider ?? false ? ( - - ) : item.label ? ( - + {item.label ? ( + {item.label} {item.content} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx index 45017ac8532da..40d91f13db659 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; -import { EuiHealth, EuiToolTip } from '@elastic/eui'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { Agent } from '../../../types'; interface Props { @@ -13,79 +13,52 @@ interface Props { } const Status = { - Online: ( - - - + Healthy: ( + + + ), Offline: ( - + - + ), Inactive: ( - - - - ), - Warning: ( - - - - ), - Error: ( - - - - ), - Degraded: ( - - - - ), - Enrolling: ( - - - + + + ), - Unenrolling: ( - + Unhealthy: ( + - + ), - Upgrading: ( - + Updating: ( + - + ), }; function getStatusComponent(agent: Agent): React.ReactElement { switch (agent.status) { + case 'warning': case 'error': - return Status.Error; case 'degraded': - return Status.Degraded; + return Status.Unhealthy; case 'inactive': return Status.Inactive; case 'offline': return Status.Offline; - case 'warning': - return Status.Warning; case 'unenrolling': - return Status.Unenrolling; case 'enrolling': - return Status.Enrolling; case 'updating': - return Status.Upgrading; + return Status.Updating; default: - return Status.Online; + return Status.Healthy; } } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_policy_package_badges.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_policy_package_badges.tsx index 08835cc872b82..ff8e4868b1fdf 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_policy_package_badges.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_policy_package_badges.tsx @@ -3,53 +3,74 @@ * 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 React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, EuiText, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; -import { PackagePolicy } from '../../../types'; +import { PackagePolicy, PackagePolicyPackage } from '../../../types'; import { useGetOneAgentPolicy } from '../../../hooks'; import { PackageIcon } from '../../../components/package_icon'; interface Props { agentPolicyId: string; + hideTitle?: boolean; } -export const AgentPolicyPackageBadges: React.FunctionComponent = ({ agentPolicyId }) => { +export const AgentPolicyPackageBadges: React.FunctionComponent = ({ + agentPolicyId, + hideTitle, +}) => { const agentPolicyRequest = useGetOneAgentPolicy(agentPolicyId); const agentPolicy = agentPolicyRequest.data ? agentPolicyRequest.data.item : null; - if (!agentPolicy) { + const packages = useMemo(() => { + if (!agentPolicy) { + return; + } + + const uniquePackages = new Map(); + + (agentPolicy.package_policies as PackagePolicy[]).forEach(({ package: pkg }) => { + if (!pkg) { + return; + } + + if (!uniquePackages.has(pkg.name) || uniquePackages.get(pkg.name)!.version < pkg.version) { + uniquePackages.set(pkg.name, pkg); + } + }); + + return [...uniquePackages.values()]; + }, [agentPolicy]); + + if (!agentPolicy || !packages) { return null; } + return ( <> - - {agentPolicy.package_policies.length}, - }} - /> - - - {(agentPolicy.package_policies as PackagePolicy[]).map((packagePolicy, idx) => { - if (!packagePolicy.package) { - return null; - } + {!hideTitle && ( + <> + + {packages.length}, + }} + /> + + + + )} + {packages.map((pkg, idx) => { return ( - + - {packagePolicy.package.title} + {pkg.title} ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx index ed607e361bd6e..d9aeba2372672 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx @@ -46,6 +46,9 @@ function useCreateApiKeyForm( policy_id: policyIdInput.value, }), }); + if (res.error) { + throw res.error; + } policyIdInput.clear(); apiKeyNameInput.clear(); setIsLoading(false); diff --git a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts index ded1447954aff..dd80c1ad77b85 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts @@ -22,6 +22,7 @@ export { NewPackagePolicyInputStream, PackagePolicyConfigRecord, PackagePolicyConfigRecordEntry, + PackagePolicyPackage, Output, DataStream, // API schema - misc setup, status diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index e3ca6a9b48dcf..d6fa79a2baeba 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -5,7 +5,11 @@ */ /* eslint-disable max-classes-per-file */ -export { defaultIngestErrorHandler, ingestErrorToResponseOptions } from './handlers'; +export { + defaultIngestErrorHandler, + ingestErrorToResponseOptions, + isLegacyESClientError, +} from './handlers'; export class IngestManagerError extends Error { constructor(message?: string) { @@ -24,3 +28,4 @@ export class PackageUnsupportedMediaTypeError extends IngestManagerError {} export class PackageInvalidArchiveError extends IngestManagerError {} export class PackageCacheError extends IngestManagerError {} export class PackageOperationNotSupportedError extends IngestManagerError {} +export class FleetAdminUserInvalidError extends IngestManagerError {} diff --git a/x-pack/plugins/fleet/server/services/api_keys/security.ts b/x-pack/plugins/fleet/server/services/api_keys/security.ts index dfd53d55fbbf5..5fdf8626a9fb2 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/security.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/security.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, FakeRequest, SavedObjectsClientContract } from 'src/core/server'; +import type { Request } from '@hapi/hapi'; +import { KibanaRequest, SavedObjectsClientContract } from '../../../../../../src/core/server'; +import { FleetAdminUserInvalidError, isLegacyESClientError } from '../../errors'; import { CallESAsCurrentUser } from '../../types'; import { appContextService } from '../app_context'; import { outputService } from '../output'; @@ -18,22 +20,38 @@ export async function createAPIKey( if (!adminUser) { throw new Error('No admin user configured'); } - const request: FakeRequest = { + const request = KibanaRequest.from(({ + path: '/', + route: { settings: {} }, + url: { href: '/' }, + raw: { req: { url: '/' } }, headers: { authorization: `Basic ${Buffer.from(`${adminUser.username}:${adminUser.password}`).toString( 'base64' )}`, }, - }; + } as unknown) as Request); const security = appContextService.getSecurity(); if (!security) { throw new Error('Missing security plugin'); } - return security.authc.createAPIKey(request as KibanaRequest, { - name, - role_descriptors: roleDescriptors, - }); + try { + const key = await security.authc.createAPIKey(request, { + name, + role_descriptors: roleDescriptors, + }); + + return key; + } catch (err) { + if (isLegacyESClientError(err) && err.statusCode === 401) { + // Clear Fleet admin user cache as the user is probably not valid anymore + outputService.invalidateCache(); + throw new FleetAdminUserInvalidError(`Fleet Admin user is invalid: ${err.message}`); + } + + throw err; + } } export async function authenticate(callCluster: CallESAsCurrentUser) { try { @@ -51,20 +69,36 @@ export async function invalidateAPIKey(soClient: SavedObjectsClientContract, id: if (!adminUser) { throw new Error('No admin user configured'); } - const request: FakeRequest = { + const request = KibanaRequest.from(({ + path: '/', + route: { settings: {} }, + url: { href: '/' }, + raw: { req: { url: '/' } }, headers: { authorization: `Basic ${Buffer.from(`${adminUser.username}:${adminUser.password}`).toString( 'base64' )}`, }, - }; + } as unknown) as Request); const security = appContextService.getSecurity(); if (!security) { throw new Error('Missing security plugin'); } - return security.authc.invalidateAPIKey(request as KibanaRequest, { - id, - }); + try { + const res = await security.authc.invalidateAPIKey(request, { + id, + }); + + return res; + } catch (err) { + if (isLegacyESClientError(err) && err.statusCode === 401) { + // Clear Fleet admin user cache as the user is probably not valid anymore + outputService.invalidateCache(); + throw new FleetAdminUserInvalidError(`Fleet Admin user is invalid: ${err.message}`); + } + + throw err; + } } diff --git a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts index dc7a91e08799c..b93a9119cd4df 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts @@ -5,7 +5,7 @@ */ import yaml from 'js-yaml'; -import { uniq } from 'lodash'; +import { pick, uniq } from 'lodash'; import { ArchivePackage, RegistryPolicyTemplate, @@ -21,6 +21,42 @@ import { pkgToPkgKey } from '../registry'; const MANIFESTS: Record = {}; const MANIFEST_NAME = 'manifest.yml'; +// not sure these are 100% correct but they do the job here +// keeping them local until others need them +type OptionalPropertyOf = Exclude< + { + [K in keyof T]: T extends Record ? never : K; + }[keyof T], + undefined +>; +type RequiredPropertyOf = Exclude>; + +type RequiredPackageProp = RequiredPropertyOf; +type OptionalPackageProp = OptionalPropertyOf; +// pro: guarantee only supplying known values. these keys must be in ArchivePackage. no typos or new values +// pro: any values added to these lists will be passed through by default +// pro & con: values do need to be shadowed / repeated from ArchivePackage, but perhaps we want to limit values +const requiredArchivePackageProps: readonly RequiredPackageProp[] = [ + 'name', + 'version', + 'description', + 'type', + 'categories', + 'format_version', +] as const; + +const optionalArchivePackageProps: readonly OptionalPackageProp[] = [ + 'title', + 'release', + 'readme', + 'screenshots', + 'icons', + 'assets', + 'internal', + 'data_streams', + 'policy_templates', +] as const; + // TODO: everything below performs verification of manifest.yml files, and hence duplicates functionality already implemented in the // package registry. At some point this should probably be replaced (or enhanced) with verification based on // https://github.com/elastic/package-spec/ @@ -58,43 +94,43 @@ function parseAndVerifyArchive(paths: string[]): ArchivePackage { } // ... which must be valid YAML - let manifest; + let manifest: ArchivePackage; try { manifest = yaml.load(manifestBuffer.toString()); } catch (error) { throw new PackageInvalidArchiveError(`Could not parse top-level package manifest: ${error}.`); } - // Package name and version from the manifest must match those from the toplevel directory - const pkgKey = pkgToPkgKey({ name: manifest.name, version: manifest.version }); - if (toplevelDir !== pkgKey) { + // must have mandatory fields + const reqGiven = pick(manifest, requiredArchivePackageProps); + const requiredKeysMatch = + Object.keys(reqGiven).toString() === requiredArchivePackageProps.toString(); + if (!requiredKeysMatch) { + const list = requiredArchivePackageProps.join(', '); throw new PackageInvalidArchiveError( - `Name ${manifest.name} and version ${manifest.version} do not match top-level directory ${toplevelDir}` + `Invalid top-level package manifest: one or more fields missing of ${list}` ); } - const { name, version, description, type, categories, format_version: formatVersion } = manifest; - // check for mandatory fields - if (!(name && version && description && type && categories && formatVersion)) { + // at least have all required properties + // get optional values and combine into one object for the remaining operations + const optGiven = pick(manifest, optionalArchivePackageProps); + const parsed: ArchivePackage = { ...reqGiven, ...optGiven }; + + // Package name and version from the manifest must match those from the toplevel directory + const pkgKey = pkgToPkgKey({ name: parsed.name, version: parsed.version }); + if (toplevelDir !== pkgKey) { throw new PackageInvalidArchiveError( - 'Invalid top-level package manifest: one or more fields missing of name, version, description, type, categories, format_version' + `Name ${parsed.name} and version ${parsed.version} do not match top-level directory ${toplevelDir}` ); } - const dataStreams = parseAndVerifyDataStreams(paths, name, version); - const policyTemplates = parseAndVerifyPolicyTemplates(manifest); + parsed.data_streams = parseAndVerifyDataStreams(paths, parsed.name, parsed.version); + parsed.policy_templates = parseAndVerifyPolicyTemplates(manifest); - return { - name, - version, - description, - type, - categories, - format_version: formatVersion, - data_streams: dataStreams, - policy_templates: policyTemplates, - }; + return parsed; } + function parseAndVerifyDataStreams( paths: string[], pkgName: string, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 05f552b558205..1af7ce149dfc0 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -5,7 +5,7 @@ */ import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; -import { InstallablePackage, InstallSource } from '../../../../common'; +import { InstallablePackage, InstallSource, MAX_TIME_COMPLETE_INSTALL } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, @@ -46,15 +46,29 @@ export async function _installPackage({ installSource: InstallSource; }): Promise { const { name: pkgName, version: pkgVersion } = packageInfo; - // add the package installation to the saved object. - // if some installation already exists, just update install info + // if some installation already exists if (installedPkg) { - await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - install_version: pkgVersion, - install_status: 'installing', - install_started_at: new Date().toISOString(), - install_source: installSource, - }); + // if the installation is currently running, don't try to install + // instead, only return already installed assets + if ( + installedPkg.attributes.install_status === 'installing' && + Date.now() - Date.parse(installedPkg.attributes.install_started_at) < + MAX_TIME_COMPLETE_INSTALL + ) { + let assets: AssetReference[] = []; + assets = assets.concat(installedPkg.attributes.installed_es); + assets = assets.concat(installedPkg.attributes.installed_kibana); + return assets; + } else { + // if no installation is running, or the installation has been running longer than MAX_TIME_COMPLETE_INSTALL + // (it might be stuck) update the saved object and proceed + await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + install_version: pkgVersion, + install_status: 'installing', + install_started_at: new Date().toISOString(), + install_source: installSource, + }); + } } else { await createInstallation({ savedObjectsClient, diff --git a/x-pack/plugins/graph/public/angular/graph_client_workspace.js b/x-pack/plugins/graph/public/angular/graph_client_workspace.js index 5cc06bad4c423..785e221b79865 100644 --- a/x-pack/plugins/graph/public/angular/graph_client_workspace.js +++ b/x-pack/plugins/graph/public/angular/graph_client_workspace.js @@ -1187,7 +1187,7 @@ function GraphWorkspace(options) { // Search for connections between the selected nodes. searcher(self.options.indexName, searchReq, function (data) { - const numDocsMatched = data.hits.total; + const numDocsMatched = data.hits.total.value; const buckets = data.aggregations.matrix.buckets; const vertices = nodesForLinking.map(function (existingNode) { return { diff --git a/x-pack/plugins/graph/server/routes/search.ts b/x-pack/plugins/graph/server/routes/search.ts index 1bd2861687281..7d05f9ab6888c 100644 --- a/x-pack/plugins/graph/server/routes/search.ts +++ b/x-pack/plugins/graph/server/routes/search.ts @@ -47,7 +47,7 @@ export function registerSearchRoute({ await esClient.asCurrentUser.search({ index: request.body.index, body: request.body.body, - rest_total_hits_as_int: true, + track_total_hits: true, ignore_throttled: !includeFrozen, }) ).body, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 14785f64cffac..1941ec6326ddb 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; import { AlertStates, InventoryMetricConditions } from './types'; -import { ResolvedActionGroup } from '../../../../../alerts/common'; +import { RecoveredActionGroup } from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; import { InfraBackendLibs } from '../../infra_types'; @@ -103,7 +103,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = } if (reason) { const actionGroupId = - nextState === AlertStates.OK ? ResolvedActionGroup.id : FIRED_ACTIONS.id; + nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS.id; alertInstance.scheduleActions(actionGroupId, { group: item, alertState: stateToAlertMessage[nextState], diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index b31afba8ac9cc..a1d6428f3b52b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -6,7 +6,7 @@ import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; import { Comparator, AlertStates } from './types'; import * as mocks from './test_mocks'; -import { ResolvedActionGroup } from '../../../../../alerts/common'; +import { RecoveredActionGroup } from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { alertsMock, @@ -367,7 +367,7 @@ describe('The metric threshold alert type', () => { expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute([2]); - expect(mostRecentAction(instanceID).id).toBe(ResolvedActionGroup.id); + expect(mostRecentAction(instanceID).id).toBe(RecoveredActionGroup.id); expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('does not continue to send a recovery alert if the metric is still OK', async () => { @@ -383,7 +383,7 @@ describe('The metric threshold alert type', () => { expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); await execute([2]); - expect(mostRecentAction(instanceID).id).toBe(ResolvedActionGroup.id); + expect(mostRecentAction(instanceID).id).toBe(RecoveredActionGroup.id); expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 7c3918c37ebbf..60790648d9a9b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -6,7 +6,7 @@ import { first, last } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; -import { ResolvedActionGroup } from '../../../../../alerts/common'; +import { RecoveredActionGroup } from '../../../../../alerts/common'; import { AlertExecutorOptions } from '../../../../../alerts/server'; import { InfraBackendLibs } from '../../infra_types'; import { @@ -89,7 +89,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => const firstResult = first(alertResults); const timestamp = (firstResult && firstResult[group].timestamp) ?? moment().toISOString(); const actionGroupId = - nextState === AlertStates.OK ? ResolvedActionGroup.id : FIRED_ACTIONS.id; + nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS.id; alertInstance.scheduleActions(actionGroupId, { group, alertState: stateToAlertMessage[nextState], diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index 93b4a4e3bea20..3d453cd078b7f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -88,7 +88,7 @@ function LayerPanels( const layerIds = activeVisualization.getLayerIds(visualizationState); return ( - + {layerIds.map((layerId, index) => ( setActiveDimension(initialActiveDimensionState)} + handleClose={() => { + if (layerDatasource.updateStateOnCloseDimension) { + const newState = layerDatasource.updateStateOnCloseDimension({ + state: layerDatasourceState, + layerId, + columnId: activeId!, + }); + props.updateDatasource(datasourceId, newState); + } + setActiveDimension(initialActiveDimensionState); + }} panel={ <> {activeGroup && activeId && (

-

+

{title || i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })}

diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index ad5509dd88bc9..5121714050c68 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -563,7 +563,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ '{availableFields} available {availableFields, plural, one {field} other {fields}}. {emptyFields} empty {emptyFields, plural, one {field} other {fields}}. {metaFields} meta {metaFields, plural, one {field} other {fields}}.', values: { availableFields: fieldGroups.AvailableFields.fields.length, - emptyFields: fieldGroups.EmptyFields.fields.length, + // empty fields can be undefined if there is no existence information to be fetched + emptyFields: fieldGroups.EmptyFields?.fields.length || 0, metaFields: fieldGroups.MetaFields.fields.length, }, })} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index b4231dadf54c0..576825e9c960a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { IndexPatternDimensionEditorProps } from './dimension_panel'; import { OperationSupportMatrix } from './operation_support'; -import { IndexPatternColumn, OperationType } from '../indexpattern'; +import { IndexPatternColumn } from '../indexpattern'; import { operationDefinitionMap, getOperationDisplay, @@ -27,6 +27,8 @@ import { replaceColumn, deleteColumn, updateColumnParam, + resetIncomplete, + FieldBasedIndexPatternColumn, } from '../operations'; import { mergeLayer } from '../state_helpers'; import { FieldSelect } from './field_select'; @@ -107,14 +109,14 @@ export function DimensionEditor(props: DimensionEditorProps) { hideGrouping, } = props; const { fieldByOperation, operationWithoutField } = operationSupportMatrix; - const [ - incompatibleSelectedOperationType, - setInvalidOperationType, - ] = useState(null); const selectedOperationDefinition = selectedColumn && operationDefinitionMap[selectedColumn.operationType]; + const incompleteInfo = (state.layers[layerId].incompleteColumns ?? {})[columnId]; + const incompleteOperation = incompleteInfo?.operationType; + const incompleteField = incompleteInfo?.sourceField ?? null; + const ParamEditor = selectedOperationDefinition?.paramEditor; const possibleOperations = useMemo(() => { @@ -139,7 +141,7 @@ export function DimensionEditor(props: DimensionEditorProps) { hasField(selectedColumn) && definition.input === 'field' && fieldByOperation[operationType]?.has(selectedColumn.sourceField)) || - (selectedColumn && !hasField(selectedColumn) && definition.input !== 'field'), + (selectedColumn && !hasField(selectedColumn) && definition.input === 'none'), disabledStatus: definition.getDisabledStatus && definition.getDisabledStatus(state.indexPatterns[state.currentIndexPatternId]), @@ -154,10 +156,8 @@ export function DimensionEditor(props: DimensionEditorProps) { const sideNavItems: EuiListGroupItemProps[] = operationsWithCompatibility.map( ({ operationType, compatibleWithCurrentField, disabledStatus }) => { const isActive = Boolean( - incompatibleSelectedOperationType === operationType || - (!incompatibleSelectedOperationType && - selectedColumn && - selectedColumn.operationType === operationType) + incompleteOperation === operationType || + (!incompleteOperation && selectedColumn && selectedColumn.operationType === operationType) ); let color: EuiListGroupItemProps['color'] = 'primary'; @@ -191,9 +191,17 @@ export function DimensionEditor(props: DimensionEditorProps) { }`, onClick() { if (operationDefinitionMap[operationType].input === 'none') { - // Clear invalid state because we are creating a valid column - setInvalidOperationType(null); if (selectedColumn?.operationType === operationType) { + // Clear invalid state because we are reseting to a valid column + if (incompleteInfo) { + setState( + mergeLayer({ + state, + layerId, + newLayer: resetIncomplete(state.layers[layerId], columnId), + }) + ); + } return; } const newLayer = insertOrReplaceColumn({ @@ -223,15 +231,34 @@ export function DimensionEditor(props: DimensionEditorProps) { }) ); } else { - setInvalidOperationType(operationType); + setState( + mergeLayer({ + state, + layerId, + newLayer: insertOrReplaceColumn({ + layer: props.state.layers[props.layerId], + indexPattern: currentIndexPattern, + columnId, + op: operationType, + field: undefined, + }), + }) + ); } trackUiEvent(`indexpattern_dimension_operation_${operationType}`); return; } - setInvalidOperationType(null); - if (selectedColumn.operationType === operationType) { + if (incompleteInfo) { + setState( + mergeLayer({ + state, + layerId, + newLayer: resetIncomplete(state.layers[layerId], columnId), + }) + ); + } return; } @@ -274,18 +301,17 @@ export function DimensionEditor(props: DimensionEditorProps) {
{!selectedColumn || selectedOperationDefinition?.input === 'field' || - (incompatibleSelectedOperationType && - operationDefinitionMap[incompatibleSelectedOperationType].input === 'field') ? ( + (incompleteOperation && operationDefinitionMap[incompleteOperation].input === 'field') ? ( { setState( mergeLayer({ @@ -310,53 +342,25 @@ export function DimensionEditor(props: DimensionEditorProps) { ); }} onChoose={(choice) => { - let newLayer: IndexPatternLayer; - if ( - !incompatibleSelectedOperationType && - selectedColumn && - 'field' in choice && - choice.operationType === selectedColumn.operationType - ) { - // Replaces just the field - newLayer = replaceColumn({ - layer: state.layers[layerId], - columnId, - indexPattern: currentIndexPattern, - op: choice.operationType, - field: currentIndexPattern.getFieldByName(choice.field)!, - }); - } else { - // Finds a new operation - const compatibleOperations = - ('field' in choice && operationSupportMatrix.operationByField[choice.field]) || - new Set(); - let operation; - if (compatibleOperations.size > 0) { - operation = - incompatibleSelectedOperationType && - compatibleOperations.has(incompatibleSelectedOperationType) - ? incompatibleSelectedOperationType - : compatibleOperations.values().next().value; - } else if ('field' in choice) { - operation = choice.operationType; - } - newLayer = insertOrReplaceColumn({ - layer: state.layers[layerId], - columnId, - field: currentIndexPattern.getFieldByName(choice.field), - indexPattern: currentIndexPattern, - op: operation as OperationType, - }); - } - - setState(mergeLayer({ state, layerId, newLayer })); - setInvalidOperationType(null); + setState( + mergeLayer({ + state, + layerId, + newLayer: insertOrReplaceColumn({ + layer: state.layers[layerId], + columnId, + indexPattern: currentIndexPattern, + op: choice.operationType, + field: currentIndexPattern.getFieldByName(choice.field), + }), + }) + ); }} /> ) : null} - {!currentFieldIsInvalid && !incompatibleSelectedOperationType && selectedColumn && ( + {!currentFieldIsInvalid && !incompleteInfo && selectedColumn && ( )} - {!currentFieldIsInvalid && - !incompatibleSelectedOperationType && - selectedColumn && - ParamEditor && ( - <> - - - )} + {!currentFieldIsInvalid && !incompleteInfo && selectedColumn && ParamEditor && ( + <> + + + )}
{!currentFieldIsInvalid && (
- {!incompatibleSelectedOperationType && selectedColumn && ( + {!incompleteInfo && selectedColumn && ( { @@ -417,7 +418,7 @@ export function DimensionEditor(props: DimensionEditorProps) { /> )} - {!incompatibleSelectedOperationType && !hideGrouping && ( + {!incompleteInfo && !hideGrouping && ( { columns: { col1: { label: 'Date histogram of timestamp', + customLabel: true, dataType: 'date', isBucketed: true, @@ -153,11 +154,16 @@ describe('IndexPatternDimensionEditorPanel', () => { sourceField: 'timestamp', }, }, + incompleteColumns: {}, }, }, }; - setState = jest.fn(); + setState = jest.fn().mockImplementation((newState) => { + if (wrapper instanceof ReactWrapper) { + wrapper.setProps({ state: newState }); + } + }); defaultProps = { state, @@ -544,7 +550,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }); describe('transient invalid state', () => { - it('should not set the state if selecting an operation incompatible with the current field', () => { + it('should set the state if selecting an operation incompatible with the current field', () => { wrapper = mount(); act(() => { @@ -553,7 +559,20 @@ describe('IndexPatternDimensionEditorPanel', () => { .simulate('click'); }); - expect(setState).not.toHaveBeenCalled(); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + }, + incompleteColumns: { + col1: { operationType: 'terms' }, + }, + }, + }, + }); }); it('should show error message in invalid state', () => { @@ -566,8 +585,6 @@ describe('IndexPatternDimensionEditorPanel', () => { expect( wrapper.find('[data-test-subj="indexPattern-field-selection-row"]').first().prop('error') ).toBeDefined(); - - expect(setState).not.toHaveBeenCalled(); }); it('should leave error state if a compatible operation is selected', () => { @@ -664,6 +681,17 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper = mount(); wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + incompleteColumns: { + col2: { operationType: 'avg' }, + }, + }, + }, + }); const comboBox = wrapper .find(EuiComboBox) @@ -675,7 +703,7 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([options![1].options![2]]); }); - expect(setState).toHaveBeenCalledWith({ + expect(setState).toHaveBeenLastCalledWith({ ...state, layers: { first: { @@ -759,11 +787,9 @@ describe('IndexPatternDimensionEditorPanel', () => { it('should set datasource state if compatible field is selected for operation', () => { wrapper = mount(); - act(() => { - wrapper - .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') - .simulate('click'); - }); + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]') + .simulate('click'); const comboBox = wrapper .find(EuiComboBox) @@ -774,7 +800,7 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState).toHaveBeenCalledWith({ + expect(setState).toHaveBeenLastCalledWith({ ...state, layers: { first: { @@ -1046,6 +1072,20 @@ describe('IndexPatternDimensionEditorPanel', () => { wrapper.find('button[data-test-subj="lns-indexPatternDimension-avg"]').simulate('click'); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + incompleteColumns: { + col2: { + operationType: 'avg', + }, + }, + }, + }, + }); + const comboBox = wrapper .find(EuiComboBox) .filter('[data-test-subj="indexPattern-dimension-field"]'); @@ -1212,6 +1252,9 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('should add a column on selection of a field', () => { + // Prevents field format from being loaded + setState.mockImplementation(() => {}); + wrapper = mount(); const comboBox = wrapper @@ -1231,6 +1274,7 @@ describe('IndexPatternDimensionEditorPanel', () => { columns: { ...state.layers.first.columns, col2: expect.objectContaining({ + operationType: 'range', sourceField: 'bytes', // Other parts of this don't matter for this test }), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts index 54da245bc8e1a..6be03a92a445e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts @@ -104,6 +104,7 @@ describe('IndexPatternDimensionEditorPanel', () => { columns: { col1: { label: 'Date histogram of timestamp', + customLabel: true, dataType: 'date', isBucketed: true, @@ -115,6 +116,7 @@ describe('IndexPatternDimensionEditorPanel', () => { sourceField: 'timestamp', }, }, + incompleteColumns: {}, }, }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index 9bc3e52822cf4..135c5dcf37db9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -28,14 +28,14 @@ import { fieldExists } from '../pure_helpers'; export interface FieldChoice { type: 'field'; field: string; - operationType?: OperationType; + operationType: OperationType; } export interface FieldSelectProps extends EuiComboBoxProps<{}> { currentIndexPattern: IndexPattern; - incompatibleSelectedOperationType: OperationType | null; - selectedColumnOperationType?: OperationType; - selectedColumnSourceField?: string; + selectedOperationType?: OperationType; + selectedField?: string; + incompleteOperation?: OperationType; operationSupportMatrix: OperationSupportMatrix; onChoose: (choice: FieldChoice) => void; onDeleteColumn: () => void; @@ -45,9 +45,9 @@ export interface FieldSelectProps extends EuiComboBoxProps<{}> { export function FieldSelect({ currentIndexPattern, - incompatibleSelectedOperationType, - selectedColumnOperationType, - selectedColumnSourceField, + incompleteOperation, + selectedOperationType, + selectedField, operationSupportMatrix, onChoose, onDeleteColumn, @@ -59,14 +59,10 @@ export function FieldSelect({ const memoizedFieldOptions = useMemo(() => { const fields = Object.keys(operationByField).sort(); + const currentOperationType = incompleteOperation ?? selectedOperationType; + function isCompatibleWithCurrentOperation(fieldName: string) { - if (incompatibleSelectedOperationType) { - return operationByField[fieldName]!.has(incompatibleSelectedOperationType); - } - return ( - !selectedColumnOperationType || - operationByField[fieldName]!.has(selectedColumnOperationType) - ); + return !currentOperationType || operationByField[fieldName]!.has(currentOperationType); } const [specialFields, normalFields] = _.partition( @@ -81,20 +77,25 @@ export function FieldSelect({ function fieldNamesToOptions(items: string[]) { return items .filter((field) => currentIndexPattern.getFieldByName(field)?.displayName) - .map((field) => ({ - label: currentIndexPattern.getFieldByName(field)?.displayName, - value: { - type: 'field', - field, - dataType: currentIndexPattern.getFieldByName(field)?.type, - operationType: - selectedColumnOperationType && isCompatibleWithCurrentOperation(field) - ? selectedColumnOperationType - : undefined, - }, - exists: containsData(field), - compatible: isCompatibleWithCurrentOperation(field), - })) + .map((field) => { + return { + label: currentIndexPattern.getFieldByName(field)?.displayName, + value: { + type: 'field', + field, + dataType: currentIndexPattern.getFieldByName(field)?.type, + // Use the operation directly, or choose the first compatible operation. + // All fields are guaranteed to have at least one operation because they + // won't appear in the list otherwise + operationType: + currentOperationType && isCompatibleWithCurrentOperation(field) + ? currentOperationType + : operationByField[field]!.values().next().value, + }, + exists: containsData(field), + compatible: isCompatibleWithCurrentOperation(field), + }; + }) .sort((a, b) => { if (a.compatible && !b.compatible) { return -1; @@ -157,8 +158,8 @@ export function FieldSelect({ metaFieldsOptions, ].filter(Boolean); }, [ - incompatibleSelectedOperationType, - selectedColumnOperationType, + incompleteOperation, + selectedOperationType, currentIndexPattern, operationByField, existingFields, @@ -174,15 +175,15 @@ export function FieldSelect({ defaultMessage: 'Field', })} options={(memoizedFieldOptions as unknown) as EuiComboBoxOptionOption[]} - isInvalid={Boolean(incompatibleSelectedOperationType || fieldIsInvalid)} + isInvalid={Boolean(incompleteOperation || fieldIsInvalid)} selectedOptions={ - ((selectedColumnOperationType && selectedColumnSourceField + ((selectedOperationType && selectedField ? [ { label: fieldIsInvalid - ? selectedColumnSourceField - : currentIndexPattern.getFieldByName(selectedColumnSourceField)?.displayName, - value: { type: 'field', field: selectedColumnSourceField }, + ? selectedField + : currentIndexPattern.getFieldByName(selectedField)?.displayName, + value: { type: 'field', field: selectedField }, }, ] : []) as unknown) as EuiComboBoxOptionOption[] diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx index eb7730677d52a..9e89468200e2c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx @@ -14,13 +14,6 @@ import { IndexPatternField } from './types'; import { FieldItemSharedProps, FieldsAccordion } from './fields_accordion'; const PAGINATION_SIZE = 50; -export interface FieldsGroup { - specialFields: IndexPatternField[]; - availableFields: IndexPatternField[]; - emptyFields: IndexPatternField[]; - metaFields: IndexPatternField[]; -} - export type FieldGroups = Record< string, { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 3ee0f5035caf4..20f71cfd3ce17 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -988,4 +988,44 @@ describe('IndexPattern Data Source', () => { expect(getErrorMessages).toHaveBeenCalledTimes(1); }); }); + + describe('#updateStateOnCloseDimension', () => { + it('should clear the incomplete column', () => { + const state = { + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + incompleteColumns: { + col1: { operationType: 'avg' as const }, + col2: { operationType: 'sum' as const }, + }, + }, + }, + currentIndexPatternId: '1', + }; + expect( + indexPatternDatasource.updateStateOnCloseDimension!({ + state, + layerId: 'first', + columnId: 'col1', + }) + ).toEqual({ + ...state, + layers: { + first: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + incompleteColumns: { col2: { operationType: 'sum' } }, + }, + }, + }); + }); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 3824aa1089d60..a639ea2c00ac0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -46,7 +46,7 @@ import { normalizeOperationDataType, } from './utils'; import { LayerPanel } from './layerpanel'; -import { IndexPatternColumn, getErrorMessages } from './operations'; +import { IndexPatternColumn, getErrorMessages, IncompleteColumn } from './operations'; import { IndexPatternField, IndexPatternPrivateState, IndexPatternPersistedState } from './types'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; @@ -319,6 +319,20 @@ export function getIndexPatternDatasource({ canHandleDrop, onDrop, + // Reset the temporary invalid state when closing the editor + updateStateOnCloseDimension: ({ state, layerId, columnId }) => { + const layer = { ...state.layers[layerId] }; + const newIncomplete: Record = { + ...(state.layers[layerId].incompleteColumns || {}), + }; + delete newIncomplete[columnId]; + return mergeLayer({ + state, + layerId, + newLayer: { ...layer, incompleteColumns: newIncomplete }, + }); + }, + getPublicAPI({ state, layerId }: PublicAPIProps) { const columnLabelMap = indexPatternDatasource.uniqueLabels(state); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts index bbe90c5923ed0..ff900134df9a1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts @@ -41,6 +41,7 @@ export const { isColumnTransferable, getErrorMessages, isReferenced, + resetIncomplete, } = actualHelpers; export const { adjustTimeScaleLabelSuffix, DEFAULT_TIME_SCALE } = actualTimeScaleUtils; 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 d0a0fb4b28588..de3f158cca620 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 @@ -6,6 +6,7 @@ import type { Operation } from '../../../types'; import { TimeScaleUnit } from '../../time_scale'; +import type { OperationType } from '../definitions'; export interface BaseIndexPatternColumn extends Operation { // Private @@ -39,6 +40,6 @@ export interface ReferenceBasedIndexPatternColumn // Used to store the temporary invalid state export interface IncompleteColumn { - operationType?: string; + operationType?: OperationType; sourceField?: string; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 48dc43b85a3ed..93447053a6029 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -228,16 +228,22 @@ describe('state_helpers', () => { ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] })); }); - it('should throw if the aggregation does not support the field', () => { - expect(() => { + it('should insert both incomplete states if the aggregation does not support the field', () => { + expect( insertNewColumn({ layer: { indexPatternId: '1', columnOrder: [], columns: {} }, columnId: 'col1', indexPattern, op: 'terms', field: indexPattern.fields[0], - }); - }).toThrow(); + }) + ).toEqual( + expect.objectContaining({ + incompleteColumns: { + col1: { operationType: 'terms', sourceField: 'timestamp' }, + }, + }) + ); }); it('should put the terms agg ahead of the date histogram', () => { @@ -531,8 +537,8 @@ describe('state_helpers', () => { }).toThrow(); }); - it('should throw if switching to a field-based operation without providing a field', () => { - expect(() => { + it('should set incompleteColumns when switching to a field-based operation without providing a field', () => { + expect( replaceColumn({ layer: { indexPatternId: '1', @@ -554,12 +560,19 @@ describe('state_helpers', () => { }, columnId: 'col1', indexPattern, - op: 'date_histogram', - }); - }).toThrow(); + op: 'terms', + }) + ).toEqual( + expect.objectContaining({ + columns: { col1: expect.objectContaining({ operationType: 'date_histogram' }) }, + incompleteColumns: { + col1: { operationType: 'terms' }, + }, + }) + ); }); - it('should carry over params from old column if the switching fields', () => { + it('should carry over params from old column if switching fields', () => { expect( replaceColumn({ layer: { @@ -592,7 +605,7 @@ describe('state_helpers', () => { ); }); - it('should transition from field-based to fieldless operation', () => { + it('should transition from field-based to fieldless operation, clearing incomplete', () => { expect( replaceColumn({ layer: { @@ -612,14 +625,20 @@ describe('state_helpers', () => { }, }, }, + incompleteColumns: { + col1: { operationType: 'terms' }, + }, }, indexPattern, columnId: 'col1', op: 'filters', - }).columns.col1 + }) ).toEqual( expect.objectContaining({ - operationType: 'filters', + columns: { + col1: expect.objectContaining({ operationType: 'filters' }), + }, + incompleteColumns: {}, }) ); }); @@ -944,6 +963,7 @@ describe('state_helpers', () => { isTransferable: jest.fn(), toExpression: jest.fn().mockReturnValue([]), getPossibleOperation: jest.fn().mockReturnValue({ dataType: 'number', isBucketed: false }), + getDefaultLabel: () => 'Test reference', }; const layer: IndexPatternLayer = { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 260ed180da921..b16418d44ba33 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -153,15 +153,50 @@ export function insertNewColumn({ } } - if (!field) { - throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`); + const invalidFieldName = (layer.incompleteColumns ?? {})[columnId]?.sourceField; + const invalidField = invalidFieldName ? indexPattern.getFieldByName(invalidFieldName) : undefined; + + if (!field && invalidField) { + const possibleOperation = operationDefinition.getPossibleOperationForField(invalidField); + if (!possibleOperation) { + throw new Error( + `Tried to create an invalid operation ${operationDefinition.type} using previously selected field ${invalidField.name}` + ); + } + const isBucketed = Boolean(possibleOperation.isBucketed); + if (isBucketed) { + return addBucket( + layer, + operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }), + columnId + ); + } else { + return addMetric( + layer, + operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }), + columnId + ); + } + } else if (!field) { + // Labels don't need to be updated because it's incomplete + return { + ...layer, + incompleteColumns: { + ...(layer.incompleteColumns ?? {}), + [columnId]: { operationType: op }, + }, + }; } const possibleOperation = operationDefinition.getPossibleOperationForField(field); if (!possibleOperation) { - throw new Error( - `Tried to create an invalid operation ${operationDefinition.type} on ${field.name}` - ); + return { + ...layer, + incompleteColumns: { + ...(layer.incompleteColumns ?? {}), + [columnId]: { operationType: op, sourceField: field.name }, + }, + }; } const isBucketed = Boolean(possibleOperation.isBucketed); if (isBucketed) { @@ -208,6 +243,8 @@ export function replaceColumn({ if (isNewOperation) { let tempLayer = { ...layer }; + tempLayer = resetIncomplete(tempLayer, columnId); + if (previousDefinition.input === 'fullReference') { (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { tempLayer = deleteColumn({ layer: tempLayer, columnId: id }); @@ -217,8 +254,6 @@ export function replaceColumn({ if (operationDefinition.input === 'fullReference') { const referenceIds = operationDefinition.requiredReferences.map(() => generateId()); - const incompleteColumns = { ...(tempLayer.incompleteColumns || {}) }; - delete incompleteColumns[columnId]; const newColumns = { ...tempLayer.columns, [columnId]: operationDefinition.buildColumn({ @@ -232,7 +267,6 @@ export function replaceColumn({ ...tempLayer, columnOrder: getColumnOrder({ ...tempLayer, columns: newColumns }), columns: newColumns, - incompleteColumns, }; } @@ -249,7 +283,13 @@ export function replaceColumn({ } if (!field) { - throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`); + return { + ...tempLayer, + incompleteColumns: { + ...(tempLayer.incompleteColumns ?? {}), + [columnId]: { operationType: op }, + }, + }; } let newColumn = operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer, field }); @@ -296,7 +336,7 @@ function addBucket( column: IndexPatternColumn, addedColumnId: string ): IndexPatternLayer { - const [buckets, metrics] = separateBucketColumns(layer); + const [buckets, metrics, references] = getExistingColumnGroups(layer); const oldDateHistogramIndex = layer.columnOrder.findIndex( (columnId) => layer.columns[columnId].operationType === 'date_histogram' @@ -310,17 +350,19 @@ function addBucket( addedColumnId, ...buckets.slice(oldDateHistogramIndex, buckets.length), ...metrics, + ...references, ]; } else { // Insert the new bucket after existing buckets. Users will see the same data // they already had, with an extra level of detail. - updatedColumnOrder = [...buckets, addedColumnId, ...metrics]; + updatedColumnOrder = [...buckets, addedColumnId, ...metrics, ...references]; } - return { - ...layer, + const tempLayer = { + ...resetIncomplete(layer, addedColumnId), columns: { ...layer.columns, [addedColumnId]: column }, columnOrder: updatedColumnOrder, }; + return { ...tempLayer, columnOrder: getColumnOrder(tempLayer) }; } function addMetric( @@ -328,18 +370,15 @@ function addMetric( column: IndexPatternColumn, addedColumnId: string ): IndexPatternLayer { - return { - ...layer, + const tempLayer = { + ...resetIncomplete(layer, addedColumnId), columns: { ...layer.columns, [addedColumnId]: column, }, columnOrder: [...layer.columnOrder, addedColumnId], }; -} - -function separateBucketColumns(layer: IndexPatternLayer) { - return partition(layer.columnOrder, (columnId) => layer.columns[columnId]?.isBucketed); + return { ...tempLayer, columnOrder: getColumnOrder(tempLayer) }; } export function getMetricOperationTypes(field: IndexPatternField) { @@ -442,9 +481,24 @@ export function deleteColumn({ return { ...newLayer, columnOrder: getColumnOrder(newLayer), incompleteColumns: newIncomplete }; } +// Derives column order from column object, respects existing columnOrder +// when possible, but also allows new columns to be added to the order export function getColumnOrder(layer: IndexPatternLayer): string[] { + const entries = Object.entries(layer.columns); + entries.sort(([idA], [idB]) => { + const indexA = layer.columnOrder.indexOf(idA); + const indexB = layer.columnOrder.indexOf(idB); + if (indexA > -1 && indexB > -1) { + return indexA - indexB; + } else if (indexA > -1) { + return -1; + } else { + return 1; + } + }); + const [direct, referenceBased] = _.partition( - Object.entries(layer.columns), + entries, ([id, col]) => operationDefinitionMap[col.operationType].input !== 'fullReference' ); // If a reference has another reference as input, put it last in sort order @@ -465,6 +519,15 @@ export function getColumnOrder(layer: IndexPatternLayer): string[] { .concat(referenceBased.map(([id]) => id)); } +// Splits existing columnOrder into the three categories +function getExistingColumnGroups(layer: IndexPatternLayer): [string[], string[], string[]] { + const [direct, referenced] = partition( + layer.columnOrder, + (columnId) => layer.columns[columnId] && !('references' in layer.columns[columnId]) + ); + return [...partition(direct, (columnId) => layer.columns[columnId]?.isBucketed), referenced]; +} + /** * Returns true if the given column can be applied to the given index pattern */ @@ -601,3 +664,9 @@ function isOperationAllowedAsReference({ hasValidMetadata ); } + +export function resetIncomplete(layer: IndexPatternLayer, columnId: string): IndexPatternLayer { + const incompleteColumns = { ...(layer.incompleteColumns ?? {}) }; + delete incompleteColumns[columnId]; + return { ...layer, incompleteColumns }; +} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 0fbc427e2188a..ba459a73ea0ee 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -167,6 +167,7 @@ export interface Datasource { renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; canHandleDrop: (props: DatasourceDimensionDropProps) => boolean; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; + updateStateOnCloseDimension?: (props: { layerId: string; columnId: string; state: T }) => T; toExpression: (state: T, layerId: string) => Ast | string | null; diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts index 68c47e11acfc0..e5764eaf0e8c0 100644 --- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts +++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts @@ -5,9 +5,11 @@ */ import { uniq, mapValues } from 'lodash'; -import { PaletteOutput } from 'src/plugins/charts/public'; +import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; import { Datatable } from 'src/plugins/expressions'; -import { FormatFactory } from '../types'; +import { AccessorConfig, FormatFactory, FramePublicAPI } from '../types'; +import { getColumnToLabelMap } from './state_helpers'; +import { LayerConfig } from './types'; const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object'; @@ -87,3 +89,48 @@ export function getColorAssignments( }; }); } + +export function getAccessorColorConfig( + colorAssignments: ColorAssignments, + frame: FramePublicAPI, + layer: LayerConfig, + sortedAccessors: string[], + paletteService: PaletteRegistry +): AccessorConfig[] { + const layerContainsSplits = Boolean(layer.splitAccessor); + const currentPalette: PaletteOutput = layer.palette || { type: 'palette', name: 'default' }; + const totalSeriesCount = colorAssignments[currentPalette.name].totalSeriesCount; + return sortedAccessors.map((accessor) => { + const currentYConfig = layer.yConfig?.find((yConfig) => yConfig.forAccessor === accessor); + if (layerContainsSplits) { + return { + columnId: accessor as string, + triggerIcon: 'disabled', + }; + } + const columnToLabel = getColumnToLabelMap(layer, frame.datasourceLayers[layer.layerId]); + const rank = colorAssignments[currentPalette.name].getRank( + layer, + columnToLabel[accessor] || accessor, + accessor + ); + const customColor = + currentYConfig?.color || + paletteService.get(currentPalette.name).getColor( + [ + { + name: columnToLabel[accessor] || accessor, + rankAtDepth: rank, + totalSeriesAtDepth: totalSeriesCount, + }, + ], + { maxDepth: 1, totalSeries: totalSeriesCount }, + currentPalette.params + ); + return { + columnId: accessor as string, + triggerIcon: customColor ? 'color' : 'disabled', + color: customColor ? customColor : undefined, + }; + }); +} diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 906c0eda9cfcd..ebf80c61e0cd1 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -10,24 +10,19 @@ import { render } from 'react-dom'; import { Position } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; +import { PaletteRegistry } from 'src/plugins/charts/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { getSuggestions } from './xy_suggestions'; import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel'; -import { - Visualization, - OperationMetadata, - VisualizationType, - AccessorConfig, - FramePublicAPI, -} from '../types'; +import { Visualization, OperationMetadata, VisualizationType, AccessorConfig } from '../types'; import { State, SeriesType, visualizationTypes, LayerConfig } from './types'; -import { getColumnToLabelMap, isHorizontalChart } from './state_helpers'; +import { isHorizontalChart } from './state_helpers'; import { toExpression, toPreviewExpression, getSortedAccessors } from './to_expression'; import { LensIconChartBarStacked } from '../assets/chart_bar_stacked'; import { LensIconChartMixedXy } from '../assets/chart_mixed_xy'; import { LensIconChartBarHorizontal } from '../assets/chart_bar_horizontal'; -import { ColorAssignments, getColorAssignments } from './color_assignment'; +import { getAccessorColorConfig, getColorAssignments } from './color_assignment'; +import { getColumnToLabelMap } from './state_helpers'; const defaultIcon = LensIconChartBarStacked; const defaultSeriesType = 'bar_stacked'; @@ -328,7 +323,11 @@ export const getXyVisualization = ({ renderDimensionEditor(domElement, props) { render( - + , domElement ); @@ -406,51 +405,6 @@ export const getXyVisualization = ({ }, }); -function getAccessorColorConfig( - colorAssignments: ColorAssignments, - frame: FramePublicAPI, - layer: LayerConfig, - sortedAccessors: string[], - paletteService: PaletteRegistry -): AccessorConfig[] { - const layerContainsSplits = Boolean(layer.splitAccessor); - const currentPalette: PaletteOutput = layer.palette || { type: 'palette', name: 'default' }; - const totalSeriesCount = colorAssignments[currentPalette.name].totalSeriesCount; - return sortedAccessors.map((accessor) => { - const currentYConfig = layer.yConfig?.find((yConfig) => yConfig.forAccessor === accessor); - if (layerContainsSplits) { - return { - columnId: accessor as string, - triggerIcon: 'disabled', - }; - } - const columnToLabel = getColumnToLabelMap(layer, frame.datasourceLayers[layer.layerId]); - const rank = colorAssignments[currentPalette.name].getRank( - layer, - columnToLabel[accessor] || accessor, - accessor - ); - const customColor = - currentYConfig?.color || - paletteService.get(currentPalette.name).getColor( - [ - { - name: columnToLabel[accessor] || accessor, - rankAtDepth: rank, - totalSeriesAtDepth: totalSeriesCount, - }, - ], - { maxDepth: 1, totalSeries: totalSeriesCount }, - currentPalette.params - ); - return { - columnId: accessor as string, - triggerIcon: customColor ? 'color' : 'disabled', - color: customColor ? customColor : undefined, - }; - }); -} - function validateLayersForDimension( dimension: string, layers: LayerConfig[], diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 99fbfa058a2de..7b84b990f963a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -14,6 +14,8 @@ import { FramePublicAPI } from '../types'; import { State } from './types'; import { Position } from '@elastic/charts'; import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import { EuiColorPicker } from '@elastic/eui'; describe('XY Config panels', () => { let frame: FramePublicAPI; @@ -322,6 +324,8 @@ describe('XY Config panels', () => { accessor="bar" groupId="left" state={{ ...state, layers: [{ ...state.layers[0], seriesType: 'bar_horizontal' }] }} + formatFactory={jest.fn()} + paletteService={chartPluginMock.createPaletteRegistry()} /> ); @@ -343,6 +347,8 @@ describe('XY Config panels', () => { accessor="bar" groupId="left" state={state} + formatFactory={jest.fn()} + paletteService={chartPluginMock.createPaletteRegistry()} /> ); @@ -353,5 +359,82 @@ describe('XY Config panels', () => { expect(options!.map(({ label }) => label)).toEqual(['Auto', 'Left', 'Right']); }); + + test('sets the color of a dimension to the color from palette service if not set explicitly', () => { + const state = testState(); + const component = mount( + + ); + + expect(component.find(EuiColorPicker).prop('color')).toEqual('black'); + }); + + test('uses the overwrite color if set', () => { + const state = testState(); + const component = mount( + + ); + + expect(component.find(EuiColorPicker).prop('color')).toEqual('red'); + }); }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index a22530c5743b4..cd8a5993d3ecb 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -5,7 +5,7 @@ */ import './xy_config_panel.scss'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { Position } from '@elastic/charts'; import { debounce } from 'lodash'; @@ -22,10 +22,12 @@ import { EuiToolTip, EuiIcon, } from '@elastic/eui'; +import { PaletteRegistry } from 'src/plugins/charts/public'; import { VisualizationLayerWidgetProps, VisualizationToolbarProps, VisualizationDimensionEditorProps, + FormatFactory, } from '../types'; import { State, @@ -48,6 +50,7 @@ import { AxisSettingsPopover } from './axis_settings_popover'; import { TooltipWrapper } from './tooltip_wrapper'; import { getAxesConfiguration } from './axes_configuration'; import { PalettePicker } from '../shared_components'; +import { getAccessorColorConfig, getColorAssignments } from './color_assignment'; type UnwrapArray = T extends Array ? P : T; type AxesSettingsConfigKeys = keyof AxesSettingsConfig; @@ -445,7 +448,12 @@ export function XyToolbar(props: VisualizationToolbarProps) { } const idPrefix = htmlIdGenerator()(); -export function DimensionEditor(props: VisualizationDimensionEditorProps) { +export function DimensionEditor( + props: VisualizationDimensionEditorProps & { + formatFactory: FormatFactory; + paletteService: PaletteRegistry; + } +) { const { state, setState, layerId, accessor } = props; const index = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[index]; @@ -556,12 +564,37 @@ const ColorPicker = ({ setState, layerId, accessor, -}: VisualizationDimensionEditorProps) => { + frame, + formatFactory, + paletteService, +}: VisualizationDimensionEditorProps & { + formatFactory: FormatFactory; + paletteService: PaletteRegistry; +}) => { const index = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[index]; const disabled = !!layer.splitAccessor; - const [color, setColor] = useState(getSeriesColor(layer, accessor)); + const overwriteColor = getSeriesColor(layer, accessor); + const currentColor = useMemo(() => { + if (overwriteColor || !frame.activeData) return overwriteColor; + + const colorAssignments = getColorAssignments( + state.layers, + { tables: frame.activeData }, + formatFactory + ); + const mappedAccessors = getAccessorColorConfig( + colorAssignments, + frame, + layer, + [accessor], + paletteService + ); + return mappedAccessors[0].color; + }, [overwriteColor, frame, paletteService, state.layers, accessor, formatFactory, layer]); + + const [color, setColor] = useState(currentColor); const handleColor: EuiColorPickerProps['onChange'] = (text, output) => { setColor(text); @@ -596,9 +629,9 @@ const ColorPicker = ({ { diff --git a/x-pack/plugins/lens/server/routes/existing_fields.test.ts b/x-pack/plugins/lens/server/routes/existing_fields.test.ts index c877e69d7b0dd..0a3e669ba8538 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.test.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { IndexPattern } from 'src/plugins/data/common'; import { existingFields, Field, buildFieldList } from './existing_fields'; describe('existingFields', () => { @@ -71,25 +72,20 @@ describe('existingFields', () => { describe('buildFieldList', () => { const indexPattern = { - id: '', - type: 'indexpattern', - attributes: { - title: 'testpattern', - type: 'type', - typeMeta: 'typemeta', - fields: JSON.stringify([ - { name: 'foo', scripted: true, lang: 'painless', script: '2+2' }, - { name: 'bar' }, - { name: '@bar' }, - { name: 'baz' }, - { name: '_mymeta' }, - ]), - }, - references: [], + title: 'testpattern', + type: 'type', + typeMeta: 'typemeta', + fields: [ + { name: 'foo', scripted: true, lang: 'painless', script: '2+2' }, + { name: 'bar' }, + { name: '@bar' }, + { name: 'baz' }, + { name: '_mymeta' }, + ], }; it('supports scripted fields', () => { - const fields = buildFieldList(indexPattern, []); + const fields = buildFieldList((indexPattern as unknown) as IndexPattern, []); expect(fields.find((f) => f.isScript)).toMatchObject({ isScript: true, name: 'foo', @@ -99,7 +95,7 @@ describe('buildFieldList', () => { }); it('supports meta fields', () => { - const fields = buildFieldList(indexPattern, ['_mymeta']); + const fields = buildFieldList((indexPattern as unknown) as IndexPattern, ['_mymeta']); expect(fields.find((f) => f.isMeta)).toMatchObject({ isScript: false, isMeta: true, diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index 844c7b16e1eaa..aef8b1b3d7076 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -6,10 +6,12 @@ import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; -import { ILegacyScopedClusterClient, SavedObject, RequestHandlerContext } from 'src/core/server'; +import { ILegacyScopedClusterClient, RequestHandlerContext } from 'src/core/server'; import { CoreSetup, Logger } from 'src/core/server'; +import { IndexPattern, IndexPatternsService } from 'src/plugins/data/common'; import { BASE_API_URL } from '../../common'; -import { IndexPatternAttributes, UI_SETTINGS } from '../../../../../src/plugins/data/server'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/server'; +import { PluginStartContract } from '../plugin'; export function isBoomError(error: { isBoom?: boolean }): error is Boom { return error.isBoom === true; @@ -28,7 +30,7 @@ export interface Field { script?: string; } -export async function existingFieldsRoute(setup: CoreSetup, logger: Logger) { +export async function existingFieldsRoute(setup: CoreSetup, logger: Logger) { const router = setup.http.createRouter(); router.post( @@ -47,11 +49,18 @@ export async function existingFieldsRoute(setup: CoreSetup, logger: Logger) { }, }, async (context, req, res) => { + const [{ savedObjects, elasticsearch }, { data }] = await setup.getStartServices(); + const savedObjectsClient = savedObjects.getScopedClient(req); + const esClient = elasticsearch.client.asScoped(req).asCurrentUser; try { return res.ok({ body: await fetchFieldExistence({ ...req.params, ...req.body, + indexPatternsService: await data.indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + esClient + ), context, }), }); @@ -80,6 +89,7 @@ export async function existingFieldsRoute(setup: CoreSetup, logger: Logger) { async function fetchFieldExistence({ context, indexPatternId, + indexPatternsService, dslQuery = { match_all: {} }, fromDate, toDate, @@ -87,16 +97,14 @@ async function fetchFieldExistence({ }: { indexPatternId: string; context: RequestHandlerContext; + indexPatternsService: IndexPatternsService; dslQuery: object; fromDate?: string; toDate?: string; timeFieldName?: string; }) { const metaFields: string[] = await context.core.uiSettings.client.get(UI_SETTINGS.META_FIELDS); - const { indexPattern, indexPatternTitle } = await fetchIndexPatternDefinition( - indexPatternId, - context - ); + const indexPattern = await indexPatternsService.get(indexPatternId); const fields = buildFieldList(indexPattern, metaFields); const docs = await fetchIndexPatternStats({ @@ -104,51 +112,32 @@ async function fetchFieldExistence({ toDate, dslQuery, client: context.core.elasticsearch.legacy.client, - index: indexPatternTitle, - timeFieldName: timeFieldName || indexPattern.attributes.timeFieldName, + index: indexPattern.title, + timeFieldName: timeFieldName || indexPattern.timeFieldName, fields, }); return { - indexPatternTitle, + indexPatternTitle: indexPattern.title, existingFieldNames: existingFields(docs, fields), }; } -async function fetchIndexPatternDefinition(indexPatternId: string, context: RequestHandlerContext) { - const savedObjectsClient = context.core.savedObjects.client; - const indexPattern = await savedObjectsClient.get( - 'index-pattern', - indexPatternId - ); - const indexPatternTitle = indexPattern.attributes.title; - - return { - indexPattern, - indexPatternTitle, - }; -} - /** * Exported only for unit tests. */ -export function buildFieldList( - indexPattern: SavedObject, - metaFields: string[] -): Field[] { - return JSON.parse(indexPattern.attributes.fields).map( - (field: { name: string; lang: string; scripted?: boolean; script?: string }) => { - return { - name: field.name, - isScript: !!field.scripted, - lang: field.lang, - script: field.script, - // id is a special case - it doesn't show up in the meta field list, - // but as it's not part of source, it has to be handled separately. - isMeta: metaFields.includes(field.name) || field.name === '_id', - }; - } - ); +export function buildFieldList(indexPattern: IndexPattern, metaFields: string[]): Field[] { + return indexPattern.fields.map((field) => { + return { + name: field.name, + isScript: !!field.scripted, + lang: field.lang, + script: field.script, + // id is a special case - it doesn't show up in the meta field list, + // but as it's not part of source, it has to be handled separately. + isMeta: metaFields.includes(field.name) || field.name === '_id', + }; + }); } async function fetchIndexPatternStats({ diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index 29e2416b74618..e0f1e05ed970d 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -11,10 +11,11 @@ import { CoreSetup } from 'src/core/server'; import { IFieldType } from 'src/plugins/data/common'; import { ESSearchResponse } from '../../../../typings/elasticsearch'; import { FieldStatsResponse, BASE_API_URL } from '../../common'; +import { PluginStartContract } from '../plugin'; const SHARD_SIZE = 5000; -export async function initFieldsRoute(setup: CoreSetup) { +export async function initFieldsRoute(setup: CoreSetup) { const router = setup.http.createRouter(); router.post( { diff --git a/x-pack/plugins/lens/server/routes/index.ts b/x-pack/plugins/lens/server/routes/index.ts index 01018d8cd7fe5..b1d7e7ca8bc17 100644 --- a/x-pack/plugins/lens/server/routes/index.ts +++ b/x-pack/plugins/lens/server/routes/index.ts @@ -5,11 +5,12 @@ */ import { CoreSetup, Logger } from 'src/core/server'; +import { PluginStartContract } from '../plugin'; import { existingFieldsRoute } from './existing_fields'; import { initFieldsRoute } from './field_stats'; import { initLensUsageRoute } from './telemetry'; -export function setupRoutes(setup: CoreSetup, logger: Logger) { +export function setupRoutes(setup: CoreSetup, logger: Logger) { existingFieldsRoute(setup, logger); initFieldsRoute(setup); initLensUsageRoute(setup); diff --git a/x-pack/plugins/lens/server/routes/telemetry.ts b/x-pack/plugins/lens/server/routes/telemetry.ts index 306c631cd78a7..820e32509923e 100644 --- a/x-pack/plugins/lens/server/routes/telemetry.ts +++ b/x-pack/plugins/lens/server/routes/telemetry.ts @@ -8,10 +8,11 @@ import Boom from '@hapi/boom'; import { CoreSetup } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { BASE_API_URL } from '../../common'; +import { PluginStartContract } from '../plugin'; // This route is responsible for taking a batch of click events from the browser // and writing them to saved objects -export async function initLensUsageRoute(setup: CoreSetup) { +export async function initLensUsageRoute(setup: CoreSetup) { const router = setup.http.createRouter(); router.post( { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index d31f6ee626245..1c0645ae797ec 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -487,8 +487,14 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye return {}; } + const { docValueFields } = getDocValueAndSourceFields( + indexPattern, + this._getTooltipPropertyNames() + ); + + const initialSearchContext = { docvalue_fields: docValueFields }; // Request fields in docvalue_fields insted of _source const searchService = getSearchService(); - const searchSource = searchService.searchSource.createEmpty(); + const searchSource = await searchService.searchSource.create(initialSearchContext as object); searchSource.setField('index', indexPattern); searchSource.setField('size', 1); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts index b7b6aa15ffe44..543e6898193a4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts @@ -30,7 +30,7 @@ export function getDefaultDatafeedQuery() { export function createSearchItems( kibanaConfig: IUiSettingsClient, - indexPattern: IIndexPattern, + indexPattern: IIndexPattern | undefined, savedSearch: SavedSearchSavedObject | null ) { // query is only used by the data visualizer as it needs diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index e436ff468ccf0..771123532dcbf 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -38,7 +38,7 @@ export const analyticsJobExplorationRouteFactory = ( }); const PageWrapper: FC = ({ location, deps }) => { - const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); + const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps)); const [globalState] = useUrlState('_g'); diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx index 80706a82121d5..3b68c5078e99e 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx @@ -34,7 +34,7 @@ export const analyticsJobsListRouteFactory = ( }); const PageWrapper: FC = ({ location, deps }) => { - const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); + const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps)); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx index 18002648cfaa6..3acd12402932f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_map.tsx @@ -34,7 +34,7 @@ export const analyticsMapRouteFactory = ( }); const PageWrapper: FC = ({ deps }) => { - const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); + const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps)); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx index b1fd6e93a744c..2f58ef756e51c 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx @@ -34,7 +34,7 @@ export const modelsListRouteFactory = ( }); const PageWrapper: FC = ({ location, deps }) => { - const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); + const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps)); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx index 837616a8a76d2..f651d17e02de4 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -45,7 +45,7 @@ export const fileBasedRouteFactory = ( const PageWrapper: FC = ({ location, deps }) => { const { redirectToMlAccessDeniedPage } = deps; - const { context } = useResolver('', undefined, deps.config, { + const { context } = useResolver(undefined, undefined, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), checkFindFileStructurePrivilege: () => diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index df92c77252565..7de59cba495af 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -63,7 +63,7 @@ export const timeSeriesExplorerRouteFactory = ( }); const PageWrapper: FC = ({ deps }) => { - const { context, results } = useResolver('', undefined, deps.config, { + const { context, results } = useResolver(undefined, undefined, deps.config, { ...basicResolvers(deps), jobs: mlJobService.loadJobsWrapper, jobsWithTimeRange: () => ml.jobs.jobsWithTimerange(getDateFormatTz()), diff --git a/x-pack/plugins/ml/public/application/routing/use_resolver.test.ts b/x-pack/plugins/ml/public/application/routing/use_resolver.test.ts new file mode 100644 index 0000000000000..07cc038538745 --- /dev/null +++ b/x-pack/plugins/ml/public/application/routing/use_resolver.test.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. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; + +import { IUiSettingsClient } from 'kibana/public'; + +import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url'; +import { useNotifications } from '../contexts/kibana'; + +import { useResolver } from './use_resolver'; + +jest.mock('../contexts/kibana/use_create_url', () => { + return { + useCreateAndNavigateToMlLink: jest.fn(), + }; +}); + +jest.mock('../contexts/kibana', () => { + return { + useMlUrlGenerator: () => ({ + createUrl: jest.fn(), + }), + useNavigateToPath: () => jest.fn(), + useNotifications: jest.fn(), + }; +}); + +const addError = jest.fn(); +(useNotifications as jest.Mock).mockImplementation(() => ({ + toasts: { addSuccess: jest.fn(), addDanger: jest.fn(), addError }, +})); + +const redirectToJobsManagementPage = jest.fn(() => Promise.resolve()); +(useCreateAndNavigateToMlLink as jest.Mock).mockImplementation(() => redirectToJobsManagementPage); + +describe('useResolver', () => { + afterEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.advanceTimersByTime(0); + jest.useRealTimers(); + }); + + it('should accept undefined as indexPatternId and savedSearchId.', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useResolver(undefined, undefined, {} as IUiSettingsClient, {}) + ); + + await act(async () => { + await waitForNextUpdate(); + }); + + expect(result.current).toStrictEqual({ + context: { + combinedQuery: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + currentIndexPattern: null, + currentSavedSearch: null, + indexPatterns: null, + kibanaConfig: {}, + }, + results: {}, + }); + expect(addError).toHaveBeenCalledTimes(0); + expect(redirectToJobsManagementPage).toHaveBeenCalledTimes(0); + }); + + it('should add an error toast and redirect if indexPatternId is an empty string.', async () => { + const { result } = renderHook(() => useResolver('', undefined, {} as IUiSettingsClient, {})); + + await act(async () => {}); + + expect(result.current).toStrictEqual({ context: null, results: {} }); + expect(addError).toHaveBeenCalledTimes(1); + expect(redirectToJobsManagementPage).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/ml/public/application/routing/use_resolver.ts b/x-pack/plugins/ml/public/application/routing/use_resolver.ts index e4cd90145bee4..3ce23f8b8a19c 100644 --- a/x-pack/plugins/ml/public/application/routing/use_resolver.ts +++ b/x-pack/plugins/ml/public/application/routing/use_resolver.ts @@ -11,6 +11,7 @@ import { getIndexPatternById, getIndexPatternsContract, getIndexPatternAndSavedSearch, + IndexPatternAndSavedSearch, } from '../util/index_utils'; import { createSearchItems } from '../jobs/new_job/utils/new_job_utils'; import { ResolverResults, Resolvers } from './resolvers'; @@ -19,6 +20,14 @@ import { useNotifications } from '../contexts/kibana'; import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url'; import { ML_PAGES } from '../../../common/constants/ml_url_generator'; +/** + * Hook to resolve route specific requirements + * @param indexPatternId optional Kibana index pattern id, used for wizards + * @param savedSearchId optional Kibana saved search id, used for wizards + * @param config Kibana UI Settings + * @param resolvers an array of resolvers to be executed for the route + * @return { context, results } returns the ML context and resolver results + */ export const useResolver = ( indexPatternId: string | undefined, savedSearchId: string | undefined, @@ -52,36 +61,49 @@ export const useResolver = ( return; } - if (indexPatternId !== undefined || savedSearchId !== undefined) { - try { - // note, currently we're using our own kibana context that requires a current index pattern to be set - // this means, if the page uses this context, useResolver must be passed a string for the index pattern id - // and loadIndexPatterns must be part of the resolvers. - const { indexPattern, savedSearch } = - savedSearchId !== undefined - ? await getIndexPatternAndSavedSearch(savedSearchId) - : { savedSearch: null, indexPattern: await getIndexPatternById(indexPatternId!) }; + try { + if (indexPatternId === '') { + throw new Error( + i18n.translate('xpack.ml.useResolver.errorIndexPatternIdEmptyString', { + defaultMessage: 'indexPatternId must not be empty string.', + }) + ); + } - const { combinedQuery } = createSearchItems(config, indexPattern!, savedSearch); + let indexPatternAndSavedSearch: IndexPatternAndSavedSearch = { + savedSearch: null, + indexPattern: null, + }; - setContext({ - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns: getIndexPatternsContract()!, - kibanaConfig: config, - }); - } catch (error) { - // an unexpected error has occurred. This could be caused by an incorrect index pattern or saved search ID - notifications.toasts.addError(new Error(error), { - title: i18n.translate('xpack.ml.useResolver.errorTitle', { - defaultMessage: 'An error has occurred', - }), - }); - await redirectToJobsManagementPage(); + if (savedSearchId !== undefined) { + indexPatternAndSavedSearch = await getIndexPatternAndSavedSearch(savedSearchId); + } else if (indexPatternId !== undefined) { + indexPatternAndSavedSearch.indexPattern = await getIndexPatternById(indexPatternId); } - } else { - setContext({}); + + const { savedSearch, indexPattern } = indexPatternAndSavedSearch; + + const { combinedQuery } = createSearchItems( + config, + indexPattern !== null ? indexPattern : undefined, + savedSearch + ); + + setContext({ + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns: getIndexPatternsContract(), + kibanaConfig: config, + }); + } catch (error) { + // an unexpected error has occurred. This could be caused by an incorrect index pattern or saved search ID + notifications.toasts.addError(new Error(error), { + title: i18n.translate('xpack.ml.useResolver.errorTitle', { + defaultMessage: 'An error has occurred', + }), + }); + await redirectToJobsManagementPage(); } })(); }, []); diff --git a/x-pack/plugins/ml/public/application/util/index_utils.ts b/x-pack/plugins/ml/public/application/util/index_utils.ts index 42be3dd8252f9..de08553af9906 100644 --- a/x-pack/plugins/ml/public/application/util/index_utils.ts +++ b/x-pack/plugins/ml/public/application/util/index_utils.ts @@ -73,9 +73,12 @@ export function getIndexPatternIdFromName(name: string) { } return null; } - +export interface IndexPatternAndSavedSearch { + savedSearch: SavedSearchSavedObject | null; + indexPattern: IIndexPattern | null; +} export async function getIndexPatternAndSavedSearch(savedSearchId: string) { - const resp: { savedSearch: SavedSearchSavedObject | null; indexPattern: IIndexPattern | null } = { + const resp: IndexPatternAndSavedSearch = { savedSearch: null, indexPattern: null, }; diff --git a/x-pack/plugins/monitoring/public/views/base_controller.js b/x-pack/plugins/monitoring/public/views/base_controller.js index 0eb40c8dd5963..62c15f0913569 100644 --- a/x-pack/plugins/monitoring/public/views/base_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_controller.js @@ -163,12 +163,12 @@ export class MonitoringViewBaseController { if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { promises.push(updateSetupModeData()); } - this.updateDataPromise = new PromiseWithCancel(Promise.all(promises)); + this.updateDataPromise = new PromiseWithCancel(Promise.allSettled(promises)); return this.updateDataPromise.promise().then(([pageData, alerts]) => { $scope.$apply(() => { this._isDataInitialized = true; // render will replace loading screen with the react component - $scope.pageData = this.data = pageData; // update the view's data with the fetch result - $scope.alerts = this.alerts = alerts; + $scope.pageData = this.data = pageData.value; // update the view's data with the fetch result + $scope.alerts = this.alerts = alerts.value || {}; }); }); }; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index ddc33a4b93730..b676abd3de2dd 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -151,20 +151,32 @@ export async function getClustersFromRequest( 'production' ); if (prodLicenseInfo.clusterAlerts.enabled) { - cluster.alerts = { - list: await fetchStatus( - alertsClient, - req.server.plugins.monitoring.info, - undefined, - cluster.cluster_uuid, - start, - end, - [] - ), - alertsMeta: { - enabled: true, - }, - }; + try { + cluster.alerts = { + list: await fetchStatus( + alertsClient, + req.server.plugins.monitoring.info, + undefined, + cluster.cluster_uuid, + start, + end, + [] + ), + alertsMeta: { + enabled: true, + }, + }; + } catch (err) { + req.logger.warn( + `Unable to fetch alert status because '${err.message}'. Alerts may not properly show up in the UI.` + ); + cluster.alerts = { + list: {}, + alertsMeta: { + enabled: true, + }, + }; + } continue; } diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts index 7b1b877c51278..792389485164d 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts @@ -5,8 +5,8 @@ */ import { SearchResponse } from 'elasticsearch'; -import { ESLicense } from 'src/plugins/telemetry_collection_manager/server'; import { LegacyAPICaller } from 'kibana/server'; +import { ESLicense } from '../../../telemetry_collection_xpack/server'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; /** diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts index 07e6ab6c72cb9..f53b5ca6d56ca 100644 --- a/x-pack/plugins/security/common/constants.ts +++ b/x-pack/plugins/security/common/constants.ts @@ -19,3 +19,6 @@ export const APPLICATION_PREFIX = 'kibana-'; export const RESERVED_PRIVILEGES_APPLICATION_WILDCARD = 'kibana-*'; export const AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER = 'auth_provider_hint'; +export const LOGOUT_PROVIDER_QUERY_STRING_PARAMETER = 'provider'; +export const LOGOUT_REASON_QUERY_STRING_PARAMETER = 'msg'; +export const NEXT_URL_QUERY_STRING_PARAMETER = 'next'; diff --git a/x-pack/plugins/security/common/model/authenticated_user.ts b/x-pack/plugins/security/common/model/authenticated_user.ts index c22c5fc4ef0da..491ceb6845e28 100644 --- a/x-pack/plugins/security/common/model/authenticated_user.ts +++ b/x-pack/plugins/security/common/model/authenticated_user.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import type { AuthenticationProvider } from '../types'; -import { User } from './user'; +import type { AuthenticationProvider, User } from '.'; const REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE = ['reserved', 'native']; diff --git a/x-pack/plugins/security/common/model/authentication_provider.test.ts b/x-pack/plugins/security/common/model/authentication_provider.test.ts new file mode 100644 index 0000000000000..fc32d3108be08 --- /dev/null +++ b/x-pack/plugins/security/common/model/authentication_provider.test.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 { shouldProviderUseLoginForm } from './authentication_provider'; + +describe('#shouldProviderUseLoginForm', () => { + ['basic', 'token'].forEach((providerType) => { + it(`returns "true" for "${providerType}" provider`, () => { + expect(shouldProviderUseLoginForm(providerType)).toEqual(true); + }); + }); + + ['anonymous', 'http', 'kerberos', 'oidc', 'pki', 'saml'].forEach((providerType) => { + it(`returns "false" for "${providerType}" provider`, () => { + expect(shouldProviderUseLoginForm(providerType)).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security/common/model/authentication_provider.ts b/x-pack/plugins/security/common/model/authentication_provider.ts new file mode 100644 index 0000000000000..1b34fbc9da29a --- /dev/null +++ b/x-pack/plugins/security/common/model/authentication_provider.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. + */ + +/** + * Type and name tuple to identify provider used to authenticate user. + */ +export interface AuthenticationProvider { + type: string; + name: string; +} + +/** + * Checks whether authentication provider with the specified type uses Kibana's native login form. + * @param providerType Type of the authentication provider. + */ +export function shouldProviderUseLoginForm(providerType: string) { + return providerType === 'basic' || providerType === 'token'; +} diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index 59d4908c67ffb..ee1dcffd4a794 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -7,6 +7,7 @@ export { ApiKey, ApiKeyToInvalidate } from './api_key'; export { User, EditUser, getUserDisplayName } from './user'; export { AuthenticatedUser, canUserChangePassword } from './authenticated_user'; +export { AuthenticationProvider, shouldProviderUseLoginForm } from './authentication_provider'; export { BuiltinESPrivileges } from './builtin_es_privileges'; export { RawKibanaPrivileges, RawKibanaFeaturePrivileges } from './raw_kibana_privileges'; export { FeaturesPrivileges } from './features_privileges'; diff --git a/x-pack/plugins/security/common/parse_next.ts b/x-pack/plugins/security/common/parse_next.ts index 7ce0de05ad526..68903baeb05b8 100644 --- a/x-pack/plugins/security/common/parse_next.ts +++ b/x-pack/plugins/security/common/parse_next.ts @@ -5,19 +5,21 @@ */ import { parse } from 'url'; +import { NEXT_URL_QUERY_STRING_PARAMETER } from './constants'; import { isInternalURL } from './is_internal_url'; export function parseNext(href: string, basePath = '') { const { query, hash } = parse(href, true); - if (!query.next) { + + let next = query[NEXT_URL_QUERY_STRING_PARAMETER]; + if (!next) { return `${basePath}/`; } - let next: string; - if (Array.isArray(query.next) && query.next.length > 0) { - next = query.next[0]; + if (Array.isArray(next) && next.length > 0) { + next = next[0]; } else { - next = query.next as string; + next = next as string; } // validate that `next` is not attempting a redirect to somewhere diff --git a/x-pack/plugins/security/common/types.ts b/x-pack/plugins/security/common/types.ts index c668c6ccf71d1..33e2875acefef 100644 --- a/x-pack/plugins/security/common/types.ts +++ b/x-pack/plugins/security/common/types.ts @@ -4,13 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -/** - * Type and name tuple to identify provider used to authenticate user. - */ -export interface AuthenticationProvider { - type: string; - name: string; -} +import type { AuthenticationProvider } from './model'; export interface SessionInfo { now: number; diff --git a/x-pack/plugins/security/public/authentication/logged_out/logged_out_page.test.tsx b/x-pack/plugins/security/public/authentication/logged_out/logged_out_page.test.tsx new file mode 100644 index 0000000000000..89d622e086b38 --- /dev/null +++ b/x-pack/plugins/security/public/authentication/logged_out/logged_out_page.test.tsx @@ -0,0 +1,39 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; +import { mountWithIntl } from '@kbn/test/jest'; +import { LoggedOutPage } from './logged_out_page'; + +import { coreMock } from '../../../../../../src/core/public/mocks'; + +describe('LoggedOutPage', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { href: 'https://some-host' }, + writable: true, + }); + }); + + it('points to a base path if `next` parameter is not provided', async () => { + const basePathMock = coreMock.createStart({ basePath: '/mock-base-path' }).http.basePath; + const wrapper = mountWithIntl(); + + expect(wrapper.find(EuiButton).prop('href')).toBe('/mock-base-path/'); + }); + + it('properly parses `next` parameter', async () => { + window.location.href = `https://host.com/mock-base-path/security/logged_out?next=${encodeURIComponent( + '/mock-base-path/app/home#/?_g=()' + )}`; + + const basePathMock = coreMock.createStart({ basePath: '/mock-base-path' }).http.basePath; + const wrapper = mountWithIntl(); + + expect(wrapper.find(EuiButton).prop('href')).toBe('/mock-base-path/app/home#/?_g=()'); + }); +}); diff --git a/x-pack/plugins/security/public/authentication/logged_out/logged_out_page.tsx b/x-pack/plugins/security/public/authentication/logged_out/logged_out_page.tsx index a708931c3fa95..5498b8ef3644c 100644 --- a/x-pack/plugins/security/public/authentication/logged_out/logged_out_page.tsx +++ b/x-pack/plugins/security/public/authentication/logged_out/logged_out_page.tsx @@ -9,6 +9,7 @@ import ReactDOM from 'react-dom'; import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { CoreStart, IBasePath } from 'src/core/public'; +import { parseNext } from '../../../common/parse_next'; import { AuthenticationStatePage } from '../components'; interface Props { @@ -25,7 +26,7 @@ export function LoggedOutPage({ basePath }: Props) { /> } > - + diff --git a/x-pack/plugins/security/public/authentication/login/login_page.tsx b/x-pack/plugins/security/public/authentication/login/login_page.tsx index 0646962684284..35703212762fd 100644 --- a/x-pack/plugins/security/public/authentication/login/login_page.tsx +++ b/x-pack/plugins/security/public/authentication/login/login_page.tsx @@ -15,7 +15,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiTitle } from '@elasti import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { CoreStart, FatalErrorsStart, HttpStart, NotificationsStart } from 'src/core/public'; -import { AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER } from '../../../common/constants'; +import { + AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER, + LOGOUT_REASON_QUERY_STRING_PARAMETER, +} from '../../../common/constants'; import { LoginState } from '../../../common/login_state'; import { LoginForm, DisabledLoginForm } from './components'; @@ -219,7 +222,7 @@ export class LoginPage extends Component { http={this.props.http} notifications={this.props.notifications} selector={selector} - infoMessage={infoMessageMap.get(query.msg?.toString())} + infoMessage={infoMessageMap.get(query[LOGOUT_REASON_QUERY_STRING_PARAMETER]?.toString())} loginAssistanceMessage={this.props.loginAssistanceMessage} loginHelp={loginHelp} authProviderHint={query[AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER]?.toString()} diff --git a/x-pack/plugins/security/public/session/session_expired.ts b/x-pack/plugins/security/public/session/session_expired.ts index 5866526b8851e..52ba37c242d09 100644 --- a/x-pack/plugins/security/public/session/session_expired.ts +++ b/x-pack/plugins/security/public/session/session_expired.ts @@ -4,6 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + LOGOUT_PROVIDER_QUERY_STRING_PARAMETER, + LOGOUT_REASON_QUERY_STRING_PARAMETER, + NEXT_URL_QUERY_STRING_PARAMETER, +} from '../../common/constants'; + export interface ISessionExpired { logout(): void; } @@ -11,13 +17,15 @@ export interface ISessionExpired { const getNextParameter = () => { const { location } = window; const next = encodeURIComponent(`${location.pathname}${location.search}${location.hash}`); - return `&next=${next}`; + return `&${NEXT_URL_QUERY_STRING_PARAMETER}=${next}`; }; const getProviderParameter = (tenant: string) => { const key = `${tenant}/session_provider`; const providerName = sessionStorage.getItem(key); - return providerName ? `&provider=${encodeURIComponent(providerName)}` : ''; + return providerName + ? `&${LOGOUT_PROVIDER_QUERY_STRING_PARAMETER}=${encodeURIComponent(providerName)}` + : ''; }; export class SessionExpired { @@ -26,6 +34,8 @@ export class SessionExpired { logout() { const next = getNextParameter(); const provider = getProviderParameter(this.tenant); - window.location.assign(`${this.logoutUrl}?msg=SESSION_EXPIRED${next}${provider}`); + window.location.assign( + `${this.logoutUrl}?${LOGOUT_REASON_QUERY_STRING_PARAMETER}=SESSION_EXPIRED${next}${provider}` + ); } } diff --git a/x-pack/plugins/security/server/audit/security_audit_logger.ts b/x-pack/plugins/security/server/audit/security_audit_logger.ts index ee81f5f330f44..c0d431b3c2fa2 100644 --- a/x-pack/plugins/security/server/audit/security_audit_logger.ts +++ b/x-pack/plugins/security/server/audit/security_audit_logger.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuthenticationProvider } from '../../common/types'; +import type { AuthenticationProvider } from '../../common/model'; import { LegacyAuditLogger } from './audit_service'; /** diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 16cb51cbfccf5..ed5d05dbcf619 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -111,31 +111,78 @@ describe('Authenticator', () => { ).toThrowError('Provider name "__http__" is reserved.'); }); - it('properly sets `loggedOut` URL.', () => { - const basicAuthenticationProviderMock = jest.requireMock('./providers/basic') - .BasicAuthenticationProvider; + describe('#options.urls.loggedOut', () => { + it('points to /login if provider requires login form', () => { + const authenticationProviderMock = jest.requireMock(`./providers/basic`) + .BasicAuthenticationProvider; + authenticationProviderMock.mockClear(); + new Authenticator(getMockOptions()); + const getLoggedOutURL = authenticationProviderMock.mock.calls[0][0].urls.loggedOut; - basicAuthenticationProviderMock.mockClear(); - new Authenticator(getMockOptions()); - expect(basicAuthenticationProviderMock).toHaveBeenCalledWith( - expect.objectContaining({ - urls: { - loggedOut: '/mock-server-basepath/security/logged_out', - }, - }), - expect.anything() - ); + expect(getLoggedOutURL(httpServerMock.createKibanaRequest())).toBe( + '/mock-server-basepath/login?msg=LOGGED_OUT' + ); - basicAuthenticationProviderMock.mockClear(); - new Authenticator(getMockOptions({ selector: { enabled: true } })); - expect(basicAuthenticationProviderMock).toHaveBeenCalledWith( - expect.objectContaining({ - urls: { - loggedOut: `/mock-server-basepath/login?msg=LOGGED_OUT`, - }, - }), - expect.anything() - ); + expect( + getLoggedOutURL( + httpServerMock.createKibanaRequest({ + query: { next: '/app/ml/encode me', msg: 'SESSION_EXPIRED' }, + }) + ) + ).toBe('/mock-server-basepath/login?next=%2Fapp%2Fml%2Fencode+me&msg=SESSION_EXPIRED'); + }); + + it('points to /login if login selector is enabled', () => { + const authenticationProviderMock = jest.requireMock(`./providers/saml`) + .SAMLAuthenticationProvider; + authenticationProviderMock.mockClear(); + new Authenticator( + getMockOptions({ + selector: { enabled: true }, + providers: { saml: { saml1: { order: 0, realm: 'realm' } } }, + }) + ); + const getLoggedOutURL = authenticationProviderMock.mock.calls[0][0].urls.loggedOut; + + expect(getLoggedOutURL(httpServerMock.createKibanaRequest())).toBe( + '/mock-server-basepath/login?msg=LOGGED_OUT' + ); + + expect( + getLoggedOutURL( + httpServerMock.createKibanaRequest({ + query: { next: '/app/ml/encode me', msg: 'SESSION_EXPIRED' }, + }) + ) + ).toBe('/mock-server-basepath/login?next=%2Fapp%2Fml%2Fencode+me&msg=SESSION_EXPIRED'); + }); + + it('points to /security/logged_out if login selector is NOT enabled', () => { + const authenticationProviderMock = jest.requireMock(`./providers/saml`) + .SAMLAuthenticationProvider; + authenticationProviderMock.mockClear(); + new Authenticator( + getMockOptions({ + selector: { enabled: false }, + providers: { saml: { saml1: { order: 0, realm: 'realm' } } }, + }) + ); + const getLoggedOutURL = authenticationProviderMock.mock.calls[0][0].urls.loggedOut; + + expect(getLoggedOutURL(httpServerMock.createKibanaRequest())).toBe( + '/mock-server-basepath/security/logged_out?msg=LOGGED_OUT' + ); + + expect( + getLoggedOutURL( + httpServerMock.createKibanaRequest({ + query: { next: '/app/ml/encode me', msg: 'SESSION_EXPIRED' }, + }) + ) + ).toBe( + '/mock-server-basepath/security/logged_out?next=%2Fapp%2Fml%2Fencode+me&msg=SESSION_EXPIRED' + ); + }); }); describe('HTTP authentication provider', () => { @@ -1769,7 +1816,9 @@ describe('Authenticator', () => { }); it('if session does not exist but provider name is valid, returns whatever authentication provider returns.', async () => { - const request = httpServerMock.createKibanaRequest({ query: { provider: 'basic1' } }); + const request = httpServerMock.createKibanaRequest({ + query: { provider: 'basic1' }, + }); mockOptions.session.get.mockResolvedValue(null); mockBasicAuthenticationProvider.logout.mockResolvedValue( @@ -1782,7 +1831,7 @@ describe('Authenticator', () => { expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledWith(request, null); - expect(mockOptions.session.clear).not.toHaveBeenCalled(); + expect(mockOptions.session.clear).toHaveBeenCalled(); }); it('if session does not exist and provider name is not available, returns whatever authentication provider returns.', async () => { @@ -1811,7 +1860,7 @@ describe('Authenticator', () => { ); expect(mockBasicAuthenticationProvider.logout).not.toHaveBeenCalled(); - expect(mockOptions.session.clear).not.toHaveBeenCalled(); + expect(mockOptions.session.clear).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 718415e485725..f175f47d30351 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -10,10 +10,15 @@ import { ILegacyClusterClient, IBasePath, } from '../../../../../src/core/server'; -import { AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER } from '../../common/constants'; +import { + AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER, + LOGOUT_PROVIDER_QUERY_STRING_PARAMETER, + LOGOUT_REASON_QUERY_STRING_PARAMETER, + NEXT_URL_QUERY_STRING_PARAMETER, +} from '../../common/constants'; import type { SecurityLicense } from '../../common/licensing'; -import type { AuthenticatedUser } from '../../common/model'; -import type { AuthenticationProvider } from '../../common/types'; +import type { AuthenticatedUser, AuthenticationProvider } from '../../common/model'; +import { shouldProviderUseLoginForm } from '../../common/model'; import { SecurityAuditLogger, AuditServiceSetup, userLoginEvent } from '../audit'; import type { ConfigType } from '../config'; import { getErrorStatusCode } from '../errors'; @@ -199,11 +204,6 @@ export class Authenticator { client: this.options.clusterClient, logger: this.options.loggers.get('tokens'), }), - urls: { - loggedOut: options.config.authc.selector.enabled - ? `${options.basePath.serverBasePath}/login?msg=LOGGED_OUT` - : `${options.basePath.serverBasePath}/security/logged_out`, - }, }; this.providers = new Map( @@ -218,6 +218,7 @@ export class Authenticator { ...providerCommonOptions, name, logger: options.loggers.get(type, name), + urls: { loggedOut: (request) => this.getLoggedOutURL(request, type) }, }), this.options.config.authc.providers[type]?.[name] ), @@ -232,6 +233,9 @@ export class Authenticator { ...providerCommonOptions, name: '__http__', logger: options.loggers.get(HTTPAuthenticationProvider.type), + urls: { + loggedOut: (request) => this.getLoggedOutURL(request, HTTPAuthenticationProvider.type), + }, }) ); } @@ -338,7 +342,9 @@ export class Authenticator { if (this.shouldRedirectToLoginSelector(request, existingSessionValue)) { this.logger.debug('Redirecting request to Login Selector.'); return AuthenticationResult.redirectTo( - `${this.options.basePath.serverBasePath}/login?next=${encodeURIComponent( + `${ + this.options.basePath.serverBasePath + }/login?${NEXT_URL_QUERY_STRING_PARAMETER}=${encodeURIComponent( `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` )}${ suggestedProviderName && !existingSessionValue @@ -385,20 +391,17 @@ export class Authenticator { assertRequest(request); const sessionValue = await this.getSessionValue(request); - if (sessionValue) { + const suggestedProviderName = + sessionValue?.provider.name ?? + request.url.searchParams.get(LOGOUT_PROVIDER_QUERY_STRING_PARAMETER); + if (suggestedProviderName) { await this.session.clear(request); - return this.providers - .get(sessionValue.provider.name)! - .logout(request, sessionValue.state ?? null); - } - const queryStringProviderName = (request.query as Record)?.provider; - if (queryStringProviderName) { - // provider name is passed in a query param and sourced from the browser's local storage; - // hence, we can't assume that this provider exists, so we have to check it - const provider = this.providers.get(queryStringProviderName); + // Provider name may be passed in a query param and sourced from the browser's local storage; + // hence, we can't assume that this provider exists, so we have to check it. + const provider = this.providers.get(suggestedProviderName); if (provider) { - return provider.logout(request, null); + return provider.logout(request, sessionValue?.state ?? null); } } else { // In case logout is called and we cannot figure out what provider is supposed to handle it, @@ -737,7 +740,7 @@ export class Authenticator { // redirect URL in the `next` parameter. Redirect URL provided in authentication result, if any, // always takes precedence over what is specified in `redirectURL` parameter. if (preAccessRedirectURL) { - preAccessRedirectURL = `${preAccessRedirectURL}?next=${encodeURIComponent( + preAccessRedirectURL = `${preAccessRedirectURL}?${NEXT_URL_QUERY_STRING_PARAMETER}=${encodeURIComponent( authenticationResult.redirectURL || redirectURL || `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` @@ -754,4 +757,30 @@ export class Authenticator { }) : authenticationResult; } + + /** + * Creates a logged out URL for the specified request and provider. + * @param request Request that initiated logout. + * @param providerType Type of the provider that handles logout. + */ + private getLoggedOutURL(request: KibanaRequest, providerType: string) { + // The app that handles logout needs to know the reason of the logout and the URL we may need to + // redirect user to once they log in again (e.g. when session expires). + const searchParams = new URLSearchParams(); + for (const [key, defaultValue] of [ + [NEXT_URL_QUERY_STRING_PARAMETER, null], + [LOGOUT_REASON_QUERY_STRING_PARAMETER, 'LOGGED_OUT'], + ] as Array<[string, string | null]>) { + const value = request.url.searchParams.get(key) || defaultValue; + if (value) { + searchParams.append(key, value); + } + } + + // Query string may contain the path where logout has been called or + // logout reason that login page may need to know. + return this.options.config.authc.selector.enabled || shouldProviderUseLoginForm(providerType) + ? `${this.options.basePath.serverBasePath}/login?${searchParams.toString()}` + : `${this.options.basePath.serverBasePath}/security/logged_out?${searchParams.toString()}`; + } } diff --git a/x-pack/plugins/security/server/authentication/providers/anonymous.test.ts b/x-pack/plugins/security/server/authentication/providers/anonymous.test.ts index c296cb9c8e94d..9674181e18750 100644 --- a/x-pack/plugins/security/server/authentication/providers/anonymous.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/anonymous.test.ts @@ -29,32 +29,48 @@ function expectAuthenticateCall( expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); } +enum CredentialsType { + Basic = 'Basic', + ApiKey = 'ApiKey', + None = 'ES native anonymous', +} + describe('AnonymousAuthenticationProvider', () => { const user = mockAuthenticatedUser({ authentication_provider: { type: 'anonymous', name: 'anonymous1' }, }); - for (const useBasicCredentials of [true, false]) { - describe(`with ${useBasicCredentials ? '`Basic`' : '`ApiKey`'} credentials`, () => { + for (const credentialsType of [ + CredentialsType.Basic, + CredentialsType.ApiKey, + CredentialsType.None, + ]) { + describe(`with ${credentialsType} credentials`, () => { let provider: AnonymousAuthenticationProvider; let mockOptions: ReturnType; let authorization: string; beforeEach(() => { mockOptions = mockAuthenticationProviderOptions({ name: 'anonymous1' }); - provider = useBasicCredentials - ? new AnonymousAuthenticationProvider(mockOptions, { - credentials: { username: 'user', password: 'pass' }, - }) - : new AnonymousAuthenticationProvider(mockOptions, { - credentials: { apiKey: 'some-apiKey' }, - }); - authorization = useBasicCredentials - ? new HTTPAuthorizationHeader( + let credentials; + switch (credentialsType) { + case CredentialsType.Basic: + credentials = { username: 'user', password: 'pass' }; + authorization = new HTTPAuthorizationHeader( 'Basic', new BasicHTTPAuthorizationHeaderCredentials('user', 'pass').toString() - ).toString() - : new HTTPAuthorizationHeader('ApiKey', 'some-apiKey').toString(); + ).toString(); + break; + case CredentialsType.ApiKey: + credentials = { apiKey: 'some-apiKey' }; + authorization = new HTTPAuthorizationHeader('ApiKey', 'some-apiKey').toString(); + break; + default: + credentials = 'elasticsearch_anonymous_user' as 'elasticsearch_anonymous_user'; + break; + } + + provider = new AnonymousAuthenticationProvider(mockOptions, { credentials }); }); describe('`login` method', () => { @@ -111,23 +127,29 @@ describe('AnonymousAuthenticationProvider', () => { }); it('does not handle authentication via `authorization` header.', async () => { - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); + const originalAuthorizationHeader = 'Basic credentials'; + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: originalAuthorizationHeader }, + }); await expect(provider.authenticate(request)).resolves.toEqual( AuthenticationResult.notHandled() ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(request.headers.authorization).toBe(authorization); + expect(request.headers.authorization).toBe(originalAuthorizationHeader); }); it('does not handle authentication via `authorization` header even if state exists.', async () => { - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); + const originalAuthorizationHeader = 'Basic credentials'; + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: originalAuthorizationHeader }, + }); await expect(provider.authenticate(request, {})).resolves.toEqual( AuthenticationResult.notHandled() ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(request.headers.authorization).toBe(authorization); + expect(request.headers.authorization).toBe(originalAuthorizationHeader); }); it('succeeds for non-AJAX requests if state is available.', async () => { @@ -191,7 +213,7 @@ describe('AnonymousAuthenticationProvider', () => { expect(request.headers).not.toHaveProperty('authorization'); }); - if (!useBasicCredentials) { + if (credentialsType === CredentialsType.ApiKey) { it('properly handles extended format for the ApiKey credentials', async () => { provider = new AnonymousAuthenticationProvider(mockOptions, { credentials: { apiKey: { id: 'some-id', key: 'some-key' } }, @@ -237,9 +259,19 @@ describe('AnonymousAuthenticationProvider', () => { }); it('`getHTTPAuthenticationScheme` method', () => { - expect(provider.getHTTPAuthenticationScheme()).toBe( - useBasicCredentials ? 'basic' : 'apikey' - ); + let expectedAuthenticationScheme; + switch (credentialsType) { + case CredentialsType.Basic: + expectedAuthenticationScheme = 'basic'; + break; + case CredentialsType.ApiKey: + expectedAuthenticationScheme = 'apikey'; + break; + default: + expectedAuthenticationScheme = null; + break; + } + expect(provider.getHTTPAuthenticationScheme()).toBe(expectedAuthenticationScheme); }); }); } diff --git a/x-pack/plugins/security/server/authentication/providers/anonymous.ts b/x-pack/plugins/security/server/authentication/providers/anonymous.ts index 6f02cce371a41..4d1b5f4a74b2f 100644 --- a/x-pack/plugins/security/server/authentication/providers/anonymous.ts +++ b/x-pack/plugins/security/server/authentication/providers/anonymous.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from '../../../../../../src/core/server'; +import { KibanaRequest, LegacyElasticsearchErrorHelpers } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { canRedirectRequest } from '../can_redirect_request'; import { DeauthenticationResult } from '../deauthentication_result'; @@ -29,6 +29,11 @@ interface APIKeyCredentials { apiKey: { id: string; key: string } | string; } +/** + * Credentials that imply authentication based on the Elasticsearch native anonymous user. + */ +type ElasticsearchAnonymousUserCredentials = 'elasticsearch_anonymous_user'; + /** * Checks whether current request can initiate a new session. * @param request Request instance. @@ -44,7 +49,10 @@ function canStartNewSession(request: KibanaRequest) { * @param credentials */ function isAPIKeyCredentials( - credentials: UsernameAndPasswordCredentials | APIKeyCredentials + credentials: + | ElasticsearchAnonymousUserCredentials + | APIKeyCredentials + | UsernameAndPasswordCredentials ): credentials is APIKeyCredentials { return !!(credentials as APIKeyCredentials).apiKey; } @@ -59,14 +67,17 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider static readonly type = 'anonymous'; /** - * Defines HTTP authorization header that should be used to authenticate request. + * Defines HTTP authorization header that should be used to authenticate request. It isn't defined + * if provider should rely on Elasticsearch native anonymous access. */ - private readonly httpAuthorizationHeader: HTTPAuthorizationHeader; + private readonly httpAuthorizationHeader?: HTTPAuthorizationHeader; constructor( protected readonly options: Readonly, anonymousOptions?: Readonly<{ - credentials?: Readonly; + credentials?: Readonly< + ElasticsearchAnonymousUserCredentials | UsernameAndPasswordCredentials | APIKeyCredentials + >; }> ) { super(options); @@ -76,7 +87,11 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider throw new Error('Credentials must be specified'); } - if (isAPIKeyCredentials(credentials)) { + if (credentials === 'elasticsearch_anonymous_user') { + this.logger.debug( + 'Anonymous requests will be authenticated using Elasticsearch native anonymous user.' + ); + } else if (isAPIKeyCredentials(credentials)) { this.logger.debug('Anonymous requests will be authenticated via API key.'); this.httpAuthorizationHeader = new HTTPAuthorizationHeader( 'ApiKey', @@ -147,7 +162,7 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider return DeauthenticationResult.notHandled(); } - return DeauthenticationResult.redirectTo(this.options.urls.loggedOut); + return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request)); } /** @@ -155,7 +170,7 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider * HTTP header that provider attaches to all successfully authenticated requests to Elasticsearch. */ public getHTTPAuthenticationScheme() { - return this.httpAuthorizationHeader.scheme.toLowerCase(); + return this.httpAuthorizationHeader?.scheme.toLowerCase() ?? null; } /** @@ -164,7 +179,9 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider * @param state State value previously stored by the provider. */ private async authenticateViaAuthorizationHeader(request: KibanaRequest, state?: unknown) { - const authHeaders = { authorization: this.httpAuthorizationHeader.toString() }; + const authHeaders = this.httpAuthorizationHeader + ? { authorization: this.httpAuthorizationHeader.toString() } + : ({} as Record); try { const user = await this.getUser(request, authHeaders); this.logger.debug( @@ -173,7 +190,23 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider // Create session only if it doesn't exist yet, otherwise keep it unchanged. return AuthenticationResult.succeeded(user, { authHeaders, state: state ? undefined : {} }); } catch (err) { - this.logger.debug(`Failed to authenticate request : ${err.message}`); + if (LegacyElasticsearchErrorHelpers.isNotAuthorizedError(err)) { + if (!this.httpAuthorizationHeader) { + this.logger.error( + `Failed to authenticate anonymous request using Elasticsearch reserved anonymous user. Anonymous access may not be properly configured in Elasticsearch: ${err.message}` + ); + } else if (this.httpAuthorizationHeader.scheme.toLowerCase() === 'basic') { + this.logger.error( + `Failed to authenticate anonymous request using provided username/password credentials. The user with the provided username may not exist or the password is wrong: ${err.message}` + ); + } else { + this.logger.error( + `Failed to authenticate anonymous request using provided API key. The key may not exist or expired: ${err.message}` + ); + } + } else { + this.logger.error(`Failed to authenticate request : ${err.message}`); + } return AuthenticationResult.failed(err); } } diff --git a/x-pack/plugins/security/server/authentication/providers/base.mock.ts b/x-pack/plugins/security/server/authentication/providers/base.mock.ts index 1b574e6e44c10..47d961bc8faf8 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.mock.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.mock.ts @@ -22,7 +22,7 @@ export function mockAuthenticationProviderOptions(options?: { name: string }) { tokens: { refresh: jest.fn(), invalidate: jest.fn() }, name: options?.name ?? 'basic1', urls: { - loggedOut: '/mock-server-basepath/security/logged_out', + loggedOut: jest.fn().mockReturnValue('/mock-server-basepath/security/logged_out'), }, }; } diff --git a/x-pack/plugins/security/server/authentication/providers/base.ts b/x-pack/plugins/security/server/authentication/providers/base.ts index a5a68f2a49315..f1845617c87a4 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.ts @@ -29,7 +29,7 @@ export interface AuthenticationProviderOptions { logger: Logger; tokens: PublicMethodsOf; urls: { - loggedOut: string; + loggedOut: (request: KibanaRequest) => string; }; } diff --git a/x-pack/plugins/security/server/authentication/providers/basic.test.ts b/x-pack/plugins/security/server/authentication/providers/basic.test.ts index 87002ebed5672..4f93e2327da06 100644 --- a/x-pack/plugins/security/server/authentication/providers/basic.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/basic.test.ts @@ -34,6 +34,8 @@ describe('BasicAuthenticationProvider', () => { let mockOptions: ReturnType; beforeEach(() => { mockOptions = mockAuthenticationProviderOptions(); + mockOptions.urls.loggedOut.mockReturnValue('/some-logged-out-page'); + provider = new BasicAuthenticationProvider(mockOptions); }); @@ -184,30 +186,13 @@ describe('BasicAuthenticationProvider', () => { ); }); - it('redirects to login view if state is `null`.', async () => { - await expect(provider.logout(httpServerMock.createKibanaRequest(), null)).resolves.toEqual( - DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT') - ); - }); - - it('always redirects to the login page.', async () => { + it('redirects to the logged out URL.', async () => { await expect(provider.logout(httpServerMock.createKibanaRequest(), {})).resolves.toEqual( - DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT') + DeauthenticationResult.redirectTo('/some-logged-out-page') ); - }); - it('passes query string parameters to the login page.', async () => { - await expect( - provider.logout( - httpServerMock.createKibanaRequest({ - query: { next: '/app/ml', msg: 'SESSION_EXPIRED' }, - }), - {} - ) - ).resolves.toEqual( - DeauthenticationResult.redirectTo( - '/mock-server-basepath/login?next=%2Fapp%2Fml&msg=SESSION_EXPIRED' - ) + await expect(provider.logout(httpServerMock.createKibanaRequest(), null)).resolves.toEqual( + DeauthenticationResult.redirectTo('/some-logged-out-page') ); }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/basic.ts b/x-pack/plugins/security/server/authentication/providers/basic.ts index 28b671346ee7f..6a5ae29dfd832 100644 --- a/x-pack/plugins/security/server/authentication/providers/basic.ts +++ b/x-pack/plugins/security/server/authentication/providers/basic.ts @@ -5,6 +5,7 @@ */ import { KibanaRequest } from '../../../../../../src/core/server'; +import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../../common/constants'; import { canRedirectRequest } from '../can_redirect_request'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; @@ -108,7 +109,7 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider { this.logger.debug('Redirecting request to Login page.'); const basePath = this.options.basePath.get(request); return AuthenticationResult.redirectTo( - `${basePath}/login?next=${encodeURIComponent( + `${basePath}/login?${NEXT_URL_QUERY_STRING_PARAMETER}=${encodeURIComponent( `${basePath}${request.url.pathname}${request.url.search}` )}` ); @@ -131,12 +132,7 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider { return DeauthenticationResult.notHandled(); } - // Query string may contain the path where logout has been called or - // logout reason that login page may need to know. - const queryString = request.url.search || `?msg=LOGGED_OUT`; - return DeauthenticationResult.redirectTo( - `${this.options.basePath.get(request)}/login${queryString}` - ); + return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request)); } /** diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts index eb4ac8f4dcbed..d368bf90cf360 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts @@ -470,7 +470,7 @@ describe('KerberosAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); await expect(provider.logout(request, null)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); @@ -501,7 +501,7 @@ describe('KerberosAuthenticationProvider', () => { mockOptions.tokens.invalidate.mockResolvedValue(undefined); await expect(provider.logout(request, tokenPair)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.ts index 2e15893d0845f..9bf419c7dacaa 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.ts @@ -124,7 +124,7 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { } } - return DeauthenticationResult.redirectTo(this.options.urls.loggedOut); + return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request)); } /** diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts index 126306c885e53..9988ddd99c395 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts @@ -611,10 +611,10 @@ describe('OIDCAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); await expect(provider.logout(request, null)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); await expect(provider.logout(request, { nonce: 'x', realm: 'oidc1' })).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); @@ -647,7 +647,7 @@ describe('OIDCAuthenticationProvider', () => { await expect( provider.logout(request, { accessToken, refreshToken, realm: 'oidc1' }) - ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)); + ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcLogout', { diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.ts b/x-pack/plugins/security/server/authentication/providers/oidc.ts index 250641d1cf174..c46ea37f144e9 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.ts @@ -7,6 +7,7 @@ import Boom from '@hapi/boom'; import type from 'type-detect'; import { KibanaRequest } from '../../../../../../src/core/server'; +import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../../common/constants'; import type { AuthenticationInfo } from '../../elasticsearch'; import { AuthenticationResult } from '../authentication_result'; import { canRedirectRequest } from '../can_redirect_request'; @@ -434,7 +435,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { } } - return DeauthenticationResult.redirectTo(this.options.urls.loggedOut); + return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request)); } /** @@ -450,14 +451,18 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { * @param request Request instance. */ private captureRedirectURL(request: KibanaRequest) { + const searchParams = new URLSearchParams([ + [ + NEXT_URL_QUERY_STRING_PARAMETER, + `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`, + ], + ['providerType', this.type], + ['providerName', this.options.name], + ]); return AuthenticationResult.redirectTo( `${ this.options.basePath.serverBasePath - }/internal/security/capture-url?next=${encodeURIComponent( - `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` - )}&providerType=${encodeURIComponent(this.type)}&providerName=${encodeURIComponent( - this.options.name - )}`, + }/internal/security/capture-url?${searchParams.toString()}`, // Here we indicate that current session, if any, should be invalidated. It is a no-op for the // initial handshake, but is essential when both access and refresh tokens are expired. { state: null } diff --git a/x-pack/plugins/security/server/authentication/providers/pki.test.ts b/x-pack/plugins/security/server/authentication/providers/pki.test.ts index aa85b8a43af4d..763231f7fd0df 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.test.ts @@ -544,7 +544,7 @@ describe('PKIAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); await expect(provider.logout(request, null)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); @@ -572,7 +572,7 @@ describe('PKIAuthenticationProvider', () => { mockOptions.tokens.invalidate.mockResolvedValue(undefined); await expect(provider.logout(request, state)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security/server/authentication/providers/pki.ts b/x-pack/plugins/security/server/authentication/providers/pki.ts index 974a838127e1d..4bb0ddaa4ee65 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.ts @@ -128,7 +128,7 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { } } - return DeauthenticationResult.redirectTo(this.options.urls.loggedOut); + return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request)); } /** diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index 03c0b7404da39..5cba017e4916b 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -1022,10 +1022,10 @@ describe('SAMLAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); await expect(provider.logout(request, null)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); await expect(provider.logout(request, { somethingElse: 'x' } as any)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); @@ -1082,7 +1082,7 @@ describe('SAMLAuthenticationProvider', () => { refreshToken, realm: 'test-realm', }) - ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)); + ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { @@ -1103,7 +1103,7 @@ describe('SAMLAuthenticationProvider', () => { refreshToken, realm: 'test-realm', }) - ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)); + ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { @@ -1126,7 +1126,7 @@ describe('SAMLAuthenticationProvider', () => { refreshToken, realm: 'test-realm', }) - ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)); + ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { @@ -1145,7 +1145,7 @@ describe('SAMLAuthenticationProvider', () => { refreshToken: 'x-saml-refresh-token', realm: 'test-realm', }) - ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut)); + ).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request))); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { @@ -1159,7 +1159,7 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); await expect(provider.logout(request)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); @@ -1174,7 +1174,7 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: undefined }); await expect(provider.logout(request)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); @@ -1187,7 +1187,7 @@ describe('SAMLAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLResponse: 'xxx yyy' } }); await expect(provider.logout(request)).resolves.toEqual( - DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut) + DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)) ); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts index 8f31968e5f639..34639a849d354 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.ts @@ -7,6 +7,7 @@ import Boom from '@hapi/boom'; import { KibanaRequest } from '../../../../../../src/core/server'; import { isInternalURL } from '../../../common/is_internal_url'; +import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../../common/constants'; import type { AuthenticationInfo } from '../../elasticsearch'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; @@ -282,7 +283,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { } } - return DeauthenticationResult.redirectTo(this.options.urls.loggedOut); + return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request)); } /** @@ -606,14 +607,18 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { * @param request Request instance. */ private captureRedirectURL(request: KibanaRequest) { + const searchParams = new URLSearchParams([ + [ + NEXT_URL_QUERY_STRING_PARAMETER, + `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`, + ], + ['providerType', this.type], + ['providerName', this.options.name], + ]); return AuthenticationResult.redirectTo( `${ this.options.basePath.serverBasePath - }/internal/security/capture-url?next=${encodeURIComponent( - `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` - )}&providerType=${encodeURIComponent(this.type)}&providerName=${encodeURIComponent( - this.options.name - )}`, + }/internal/security/capture-url?${searchParams.toString()}`, // Here we indicate that current session, if any, should be invalidated. It is a no-op for the // initial handshake, but is essential when both access and refresh tokens are expired. { state: null } diff --git a/x-pack/plugins/security/server/authentication/providers/token.test.ts b/x-pack/plugins/security/server/authentication/providers/token.test.ts index e09400e9bb44a..5a600461ef467 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -37,6 +37,8 @@ describe('TokenAuthenticationProvider', () => { let mockOptions: MockAuthenticationProviderOptions; beforeEach(() => { mockOptions = mockAuthenticationProviderOptions({ name: 'token' }); + mockOptions.urls.loggedOut.mockReturnValue('/some-logged-out-page'); + provider = new TokenAuthenticationProvider(mockOptions); }); @@ -347,11 +349,9 @@ describe('TokenAuthenticationProvider', () => { expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); }); - it('redirects to login view if state is `null`.', async () => { - const request = httpServerMock.createKibanaRequest(); - - await expect(provider.logout(request, null)).resolves.toEqual( - DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT') + it('redirects to the logged out URL if state is `null`.', async () => { + await expect(provider.logout(httpServerMock.createKibanaRequest(), null)).resolves.toEqual( + DeauthenticationResult.redirectTo('/some-logged-out-page') ); expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); @@ -372,28 +372,14 @@ describe('TokenAuthenticationProvider', () => { expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); }); - it('redirects to /login if tokens are invalidated successfully', async () => { + it('redirects to the logged out URL if tokens are invalidated successfully.', async () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; mockOptions.tokens.invalidate.mockResolvedValue(undefined); await expect(provider.logout(request, tokenPair)).resolves.toEqual( - DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT') - ); - - expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); - }); - - it('redirects to /login with optional search parameters if tokens are invalidated successfully', async () => { - const request = httpServerMock.createKibanaRequest({ query: { yep: 'nope' } }); - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - - mockOptions.tokens.invalidate.mockResolvedValue(undefined); - - await expect(provider.logout(request, tokenPair)).resolves.toEqual( - DeauthenticationResult.redirectTo('/mock-server-basepath/login?yep=nope') + DeauthenticationResult.redirectTo('/some-logged-out-page') ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index 2032db4b0a8f2..67c2d244e75a2 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -6,6 +6,7 @@ import Boom from '@hapi/boom'; import { KibanaRequest } from '../../../../../../src/core/server'; +import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../../common/constants'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; import { canRedirectRequest } from '../can_redirect_request'; @@ -145,10 +146,7 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { } } - const queryString = request.url.search || `?msg=LOGGED_OUT`; - return DeauthenticationResult.redirectTo( - `${this.options.basePath.get(request)}/login${queryString}` - ); + return DeauthenticationResult.redirectTo(this.options.urls.loggedOut(request)); } /** @@ -235,6 +233,8 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { const nextURL = encodeURIComponent( `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` ); - return `${this.options.basePath.get(request)}/login?next=${nextURL}`; + return `${this.options.basePath.get( + request + )}/login?${NEXT_URL_QUERY_STRING_PARAMETER}=${nextURL}`; } } diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index a306e701e4e8d..f41e721db33a2 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -902,8 +902,9 @@ describe('config schema', () => { "[authc.providers]: types that failed validation: - [authc.providers.0]: expected value of type [array] but got [Object] - [authc.providers.1.anonymous.anonymous1.credentials]: types that failed validation: - - [credentials.0.password]: expected value of type [string] but got [undefined] - - [credentials.1.apiKey]: expected at least one defined value but got [undefined]" + - [credentials.0]: expected value to equal [elasticsearch_anonymous_user] + - [credentials.1.password]: expected value of type [string] but got [undefined] + - [credentials.2.apiKey]: expected at least one defined value but got [undefined]" `); expect(() => @@ -918,8 +919,9 @@ describe('config schema', () => { "[authc.providers]: types that failed validation: - [authc.providers.0]: expected value of type [array] but got [Object] - [authc.providers.1.anonymous.anonymous1.credentials]: types that failed validation: - - [credentials.0.username]: expected value of type [string] but got [undefined] - - [credentials.1.apiKey]: expected at least one defined value but got [undefined]" + - [credentials.0]: expected value to equal [elasticsearch_anonymous_user] + - [credentials.1.username]: expected value of type [string] but got [undefined] + - [credentials.2.apiKey]: expected at least one defined value but got [undefined]" `); }); @@ -973,8 +975,9 @@ describe('config schema', () => { "[authc.providers]: types that failed validation: - [authc.providers.0]: expected value of type [array] but got [Object] - [authc.providers.1.anonymous.anonymous1.credentials]: types that failed validation: - - [credentials.0.username]: expected value of type [string] but got [undefined] - - [credentials.1.apiKey]: types that failed validation: + - [credentials.0]: expected value to equal [elasticsearch_anonymous_user] + - [credentials.1.username]: expected value of type [string] but got [undefined] + - [credentials.2.apiKey]: types that failed validation: - [credentials.apiKey.0.key]: expected value of type [string] but got [undefined] - [credentials.apiKey.1]: expected value of type [string] but got [Object]" `); @@ -993,8 +996,9 @@ describe('config schema', () => { "[authc.providers]: types that failed validation: - [authc.providers.0]: expected value of type [array] but got [Object] - [authc.providers.1.anonymous.anonymous1.credentials]: types that failed validation: - - [credentials.0.username]: expected value of type [string] but got [undefined] - - [credentials.1.apiKey]: types that failed validation: + - [credentials.0]: expected value to equal [elasticsearch_anonymous_user] + - [credentials.1.username]: expected value of type [string] but got [undefined] + - [credentials.2.apiKey]: types that failed validation: - [credentials.apiKey.0.id]: expected value of type [string] but got [undefined] - [credentials.apiKey.1]: expected value of type [string] but got [Object]" `); @@ -1073,6 +1077,40 @@ describe('config schema', () => { `); }); + it('can be successfully validated with `elasticsearch_anonymous_user` credentials', () => { + expect( + ConfigSchema.validate({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 0, + credentials: 'elasticsearch_anonymous_user', + }, + }, + }, + }, + }).authc.providers + ).toMatchInlineSnapshot(` + Object { + "anonymous": Object { + "anonymous1": Object { + "credentials": "elasticsearch_anonymous_user", + "description": "Continue as Guest", + "enabled": true, + "hint": "For anonymous users", + "icon": "globe", + "order": 0, + "session": Object { + "idleTimeout": null, + }, + "showInSelector": true, + }, + }, + } + `); + }); + it('can be successfully validated with session config overrides', () => { expect( ConfigSchema.validate({ diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index b46c8dc2178a4..8d1415e1574d4 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -9,7 +9,7 @@ import type { Duration } from 'moment'; import { schema, Type, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { Logger, config as coreConfig } from '../../../../src/core/server'; -import type { AuthenticationProvider } from '../common/types'; +import type { AuthenticationProvider } from '../common/model'; export type ConfigType = ReturnType; type RawConfigType = TypeOf; @@ -150,6 +150,7 @@ const providersConfigSchema = schema.object( }, { credentials: schema.oneOf([ + schema.literal('elasticsearch_anonymous_user'), schema.object({ username: schema.string(), password: schema.string(), diff --git a/x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts b/x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts index 529e8a8aa6e9c..0a43d8dd6973a 100644 --- a/x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts +++ b/x-pack/plugins/security/server/elasticsearch/elasticsearch_client_plugin.ts @@ -22,133 +22,6 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen }, }); - /** - * Perform a [shield.changePassword](Change the password of a user) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.username - The username of the user to change the password for - */ - shield.changePassword = ca({ - params: { - refresh: { - type: 'boolean', - }, - }, - urls: [ - { - fmt: '/_security/user/<%=username%>/_password', - req: { - username: { - type: 'string', - required: false, - }, - }, - }, - { - fmt: '/_security/user/_password', - }, - ], - needBody: true, - method: 'POST', - }); - - /** - * Perform a [shield.clearCachedRealms](Clears the internal user caches for specified realms) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String} params.usernames - Comma-separated list of usernames to clear from the cache - * @param {String} params.realms - Comma-separated list of realms to clear - */ - shield.clearCachedRealms = ca({ - params: { - usernames: { - type: 'string', - required: false, - }, - }, - url: { - fmt: '/_security/realm/<%=realms%>/_clear_cache', - req: { - realms: { - type: 'string', - required: true, - }, - }, - }, - method: 'POST', - }); - - /** - * Perform a [shield.clearCachedRoles](Clears the internal caches for specified roles) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String} params.name - Role name - */ - shield.clearCachedRoles = ca({ - params: {}, - url: { - fmt: '/_security/role/<%=name%>/_clear_cache', - req: { - name: { - type: 'string', - required: true, - }, - }, - }, - method: 'POST', - }); - - /** - * Perform a [shield.deleteRole](Remove a role from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.name - Role name - */ - shield.deleteRole = ca({ - params: { - refresh: { - type: 'boolean', - }, - }, - url: { - fmt: '/_security/role/<%=name%>', - req: { - name: { - type: 'string', - required: true, - }, - }, - }, - method: 'DELETE', - }); - - /** - * Perform a [shield.deleteUser](Remove a user from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.username - username - */ - shield.deleteUser = ca({ - params: { - refresh: { - type: 'boolean', - }, - }, - url: { - fmt: '/_security/user/<%=username%>', - req: { - username: { - type: 'string', - required: true, - }, - }, - }, - method: 'DELETE', - }); - /** * Perform a [shield.getRole](Retrieve one or more roles from the native shield realm) request * @@ -173,30 +46,6 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen ], }); - /** - * Perform a [shield.getUser](Retrieve one or more users from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String, String[], Boolean} params.username - A comma-separated list of usernames - */ - shield.getUser = ca({ - params: {}, - urls: [ - { - fmt: '/_security/user/<%=username%>', - req: { - username: { - type: 'list', - required: false, - }, - }, - }, - { - fmt: '/_security/user', - }, - ], - }); - /** * Perform a [shield.putRole](Update or create a role for the native shield realm) request * @@ -249,19 +98,6 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen method: 'PUT', }); - /** - * Perform a [shield.getUserPrivileges](Retrieve a user's list of privileges) request - * - */ - shield.getUserPrivileges = ca({ - params: {}, - urls: [ - { - fmt: '/_security/user/_privileges', - }, - ], - }); - /** * Asks Elasticsearch to prepare SAML authentication request to be sent to * the 3rd-party SAML identity provider. @@ -489,36 +325,6 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen }, }); - shield.getBuiltinPrivileges = ca({ - params: {}, - urls: [ - { - fmt: '/_security/privilege/_builtin', - }, - ], - }); - - /** - * Gets API keys in Elasticsearch - * @param {boolean} owner A boolean flag that can be used to query API keys owned by the currently authenticated user. - * Defaults to false. The realm_name or username parameters cannot be specified when this parameter is set to true as - * they are assumed to be the currently authenticated ones. - */ - shield.getAPIKeys = ca({ - method: 'GET', - urls: [ - { - fmt: `/_security/api_key?owner=<%=owner%>`, - req: { - owner: { - type: 'boolean', - required: true, - }, - }, - }, - ], - }); - /** * Creates an API key in Elasticsearch for the current user. * @@ -591,64 +397,4 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen fmt: '/_security/delegate_pki', }, }); - - /** - * Retrieves all configured role mappings. - * - * @returns {{ [roleMappingName]: { enabled: boolean; roles: string[]; rules: Record} }} - */ - shield.getRoleMappings = ca({ - method: 'GET', - urls: [ - { - fmt: '/_security/role_mapping', - }, - { - fmt: '/_security/role_mapping/<%=name%>', - req: { - name: { - type: 'string', - required: true, - }, - }, - }, - ], - }); - - /** - * Saves the specified role mapping. - */ - shield.saveRoleMapping = ca({ - method: 'POST', - needBody: true, - urls: [ - { - fmt: '/_security/role_mapping/<%=name%>', - req: { - name: { - type: 'string', - required: true, - }, - }, - }, - ], - }); - - /** - * Deletes the specified role mapping. - */ - shield.deleteRoleMapping = ca({ - method: 'DELETE', - urls: [ - { - fmt: '/_security/role_mapping/<%=name%>', - req: { - name: { - type: 'string', - required: true, - }, - }, - }, - ], - }); } diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 17f2480026cc7..d6fe1356ce145 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -230,7 +230,6 @@ export class Plugin { basePath: core.http.basePath, httpResources: core.http.resources, logger: this.initializerContext.logger.get('routes'), - clusterClient, config, authc: this.authc, authz, diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts index 7968402cd2176..3a22b9fe003a1 100644 --- a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts @@ -4,115 +4,96 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; -import { LicenseCheck } from '../../../../licensing/server'; +import Boom from '@hapi/boom'; +import { + kibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from '../../../../../../src/core/server'; import { httpServerMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; -import Boom from '@hapi/boom'; -import { defineEnabledApiKeysRoutes } from './enabled'; -import { APIKeys } from '../../authentication/api_keys'; -interface TestOptions { - licenseCheckResult?: LicenseCheck; - apiResponse?: () => Promise; - asserts: { statusCode: number; result?: Record }; -} +import { defineEnabledApiKeysRoutes } from './enabled'; +import { Authentication } from '../../authentication'; describe('API keys enabled', () => { - const enabledApiKeysTest = ( - description: string, - { licenseCheckResult = { state: 'valid' }, apiResponse, asserts }: TestOptions - ) => { - test(description, async () => { - const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const apiKeys = new APIKeys({ - logger: mockRouteDefinitionParams.logger, - clusterClient: mockRouteDefinitionParams.clusterClient, - license: mockRouteDefinitionParams.license, - }); - - mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() => - apiKeys.areAPIKeysEnabled() + function getMockContext( + licenseCheckResult: { state: string; message?: string } = { state: 'valid' } + ) { + return ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + } + + let routeHandler: RequestHandler; + let authc: jest.Mocked; + beforeEach(() => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + authc = mockRouteDefinitionParams.authc; + + defineEnabledApiKeysRoutes(mockRouteDefinitionParams); + + const [, apiKeyRouteHandler] = mockRouteDefinitionParams.router.get.mock.calls.find( + ([{ path }]) => path === '/internal/security/api_key/_enabled' + )!; + routeHandler = apiKeyRouteHandler; + }); + + describe('failure', () => { + test('returns result of license checker', async () => { + const mockContext = getMockContext({ state: 'invalid', message: 'test forbidden message' }); + const response = await routeHandler( + mockContext, + httpServerMock.createKibanaRequest(), + kibanaResponseFactory ); - if (apiResponse) { - mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementation(apiResponse); - } - - defineEnabledApiKeysRoutes(mockRouteDefinitionParams); - const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; - - const headers = { authorization: 'foo' }; - const mockRequest = httpServerMock.createKibanaRequest({ - method: 'get', - path: '/internal/security/api_key/_enabled', - headers, - }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; - - const response = await handler(mockContext, mockRequest, kibanaResponseFactory); - expect(response.status).toBe(asserts.statusCode); - expect(response.payload).toEqual(asserts.result); - - if (apiResponse) { - expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.invalidateAPIKey', - { - body: { - id: expect.any(String), - }, - } - ); - } else { - expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); - } + expect(response.status).toBe(403); + expect(response.payload).toEqual({ message: 'test forbidden message' }); expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); - }; - describe('failure', () => { - enabledApiKeysTest('returns result of license checker', { - licenseCheckResult: { state: 'invalid', message: 'test forbidden message' }, - asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, - }); + test('returns error from cluster client', async () => { + const error = Boom.notAcceptable('test not acceptable message'); + authc.areAPIKeysEnabled.mockRejectedValue(error); + + const response = await routeHandler( + getMockContext(), + httpServerMock.createKibanaRequest(), + kibanaResponseFactory + ); - const error = Boom.notAcceptable('test not acceptable message'); - enabledApiKeysTest('returns error from cluster client', { - apiResponse: async () => { - throw error; - }, - asserts: { statusCode: 406, result: error }, + expect(response.status).toBe(406); + expect(response.payload).toEqual(error); }); }); describe('success', () => { - enabledApiKeysTest('returns true if API Keys are enabled', { - apiResponse: async () => ({}), - asserts: { - statusCode: 200, - result: { - apiKeysEnabled: true, - }, - }, + test('returns true if API Keys are enabled', async () => { + authc.areAPIKeysEnabled.mockResolvedValue(true); + + const response = await routeHandler( + getMockContext(), + httpServerMock.createKibanaRequest(), + kibanaResponseFactory + ); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ apiKeysEnabled: true }); }); - enabledApiKeysTest('returns false if API Keys are disabled', { - apiResponse: async () => { - const error = new Error(); - (error as any).body = { - error: { 'disabled.feature': 'api_keys' }, - }; - throw error; - }, - asserts: { - statusCode: 200, - result: { - apiKeysEnabled: false, - }, - }, + + test('returns false if API Keys are disabled', async () => { + authc.areAPIKeysEnabled.mockResolvedValue(false); + + const response = await routeHandler( + getMockContext(), + httpServerMock.createKibanaRequest(), + kibanaResponseFactory + ); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ apiKeysEnabled: false }); }); }); }); diff --git a/x-pack/plugins/security/server/routes/api_keys/get.test.ts b/x-pack/plugins/security/server/routes/api_keys/get.test.ts index cb991fb2f5aac..cc9e9a68e6e36 100644 --- a/x-pack/plugins/security/server/routes/api_keys/get.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/get.test.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { LicenseCheck } from '../../../../licensing/server'; import { defineGetApiKeysRoutes } from './get'; -import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { httpServerMock, coreMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; import Boom from '@hapi/boom'; @@ -26,11 +26,15 @@ describe('Get API keys', () => { ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { - mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + mockContext.core.elasticsearch.client.asCurrentUser.security.getApiKey.mockImplementation( + (async () => ({ body: await apiResponse() })) as any + ); } defineGetApiKeysRoutes(mockRouteDefinitionParams); @@ -43,22 +47,15 @@ describe('Get API keys', () => { query: { isAdmin: isAdmin.toString() }, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); if (apiResponse) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.getAPIKeys', - { owner: !isAdmin } - ); - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getApiKey + ).toHaveBeenCalledWith({ owner: !isAdmin }); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); diff --git a/x-pack/plugins/security/server/routes/api_keys/get.ts b/x-pack/plugins/security/server/routes/api_keys/get.ts index 6e98b4b098405..b0c6ec090a0e5 100644 --- a/x-pack/plugins/security/server/routes/api_keys/get.ts +++ b/x-pack/plugins/security/server/routes/api_keys/get.ts @@ -10,7 +10,7 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineGetApiKeysRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineGetApiKeysRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/api_key', @@ -28,11 +28,11 @@ export function defineGetApiKeysRoutes({ router, clusterClient }: RouteDefinitio createLicensedRouteHandler(async (context, request, response) => { try { const isAdmin = request.query.isAdmin === 'true'; - const { api_keys: apiKeys } = (await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getAPIKeys', { owner: !isAdmin })) as { api_keys: ApiKey[] }; + const apiResponse = await context.core.elasticsearch.client.asCurrentUser.security.getApiKey<{ + api_keys: ApiKey[]; + }>({ owner: !isAdmin }); - const validKeys = apiKeys.filter(({ invalidated }) => !invalidated); + const validKeys = apiResponse.body.api_keys.filter(({ invalidated }) => !invalidated); return response.ok({ body: { apiKeys: validKeys } }); } catch (error) { diff --git a/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts b/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts index 88e52f735395d..9ac41fdfa7483 100644 --- a/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts @@ -6,18 +6,18 @@ import Boom from '@hapi/boom'; import { Type } from '@kbn/config-schema'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { LicenseCheck } from '../../../../licensing/server'; import { defineInvalidateApiKeysRoutes } from './invalidate'; -import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; interface TestOptions { licenseCheckResult?: LicenseCheck; apiResponses?: Array<() => Promise>; payload?: Record; - asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; + asserts: { statusCode: number; result?: Record; apiArguments?: unknown[] }; } describe('Invalidate API keys', () => { @@ -27,10 +27,15 @@ describe('Invalidate API keys', () => { ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; + for (const apiResponse of apiResponses) { - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); + mockContext.core.elasticsearch.client.asCurrentUser.security.invalidateApiKey.mockImplementationOnce( + (async () => ({ body: await apiResponse() })) as any + ); } defineInvalidateApiKeysRoutes(mockRouteDefinitionParams); @@ -43,9 +48,6 @@ describe('Invalidate API keys', () => { body: payload !== undefined ? (validate as any).body.validate(payload) : undefined, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); @@ -53,13 +55,10 @@ describe('Invalidate API keys', () => { if (Array.isArray(asserts.apiArguments)) { for (const apiArguments of asserts.apiArguments) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith( - mockRequest - ); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.invalidateApiKey + ).toHaveBeenCalledWith(apiArguments); } - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); @@ -128,7 +127,7 @@ describe('Invalidate API keys', () => { isAdmin: true, }, asserts: { - apiArguments: [['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV' } }]], + apiArguments: [{ body: { id: 'si8If24B1bKsmSLTAhJV' } }], statusCode: 200, result: { itemsInvalidated: [], @@ -152,7 +151,7 @@ describe('Invalidate API keys', () => { isAdmin: true, }, asserts: { - apiArguments: [['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV' } }]], + apiArguments: [{ body: { id: 'si8If24B1bKsmSLTAhJV' } }], statusCode: 200, result: { itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], @@ -168,9 +167,7 @@ describe('Invalidate API keys', () => { isAdmin: false, }, asserts: { - apiArguments: [ - ['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV', owner: true } }], - ], + apiArguments: [{ body: { id: 'si8If24B1bKsmSLTAhJV', owner: true } }], statusCode: 200, result: { itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], @@ -195,8 +192,8 @@ describe('Invalidate API keys', () => { }, asserts: { apiArguments: [ - ['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV' } }], - ['shield.invalidateAPIKey', { body: { id: 'ab8If24B1bKsmSLTAhNC' } }], + { body: { id: 'si8If24B1bKsmSLTAhJV' } }, + { body: { id: 'ab8If24B1bKsmSLTAhNC' } }, ], statusCode: 200, result: { diff --git a/x-pack/plugins/security/server/routes/api_keys/invalidate.ts b/x-pack/plugins/security/server/routes/api_keys/invalidate.ts index dd472c0b60cbc..3977954197007 100644 --- a/x-pack/plugins/security/server/routes/api_keys/invalidate.ts +++ b/x-pack/plugins/security/server/routes/api_keys/invalidate.ts @@ -15,7 +15,7 @@ interface ResponseType { errors: Array & { error: Error }>; } -export function defineInvalidateApiKeysRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineInvalidateApiKeysRoutes({ router }: RouteDefinitionParams) { router.post( { path: '/internal/security/api_key/invalidate', @@ -28,8 +28,6 @@ export function defineInvalidateApiKeysRoutes({ router, clusterClient }: RouteDe }, createLicensedRouteHandler(async (context, request, response) => { try { - const scopedClusterClient = clusterClient.asScoped(request); - // Invalidate all API keys in parallel. const invalidationResult = ( await Promise.all( @@ -41,7 +39,9 @@ export function defineInvalidateApiKeysRoutes({ router, clusterClient }: RouteDe } // Send the request to invalidate the API key and return an error if it could not be deleted. - await scopedClusterClient.callAsCurrentUser('shield.invalidateAPIKey', { body }); + await context.core.elasticsearch.client.asCurrentUser.security.invalidateApiKey({ + body, + }); return { key, error: undefined }; } catch (error) { return { key, error: wrapError(error) }; diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts index ecc3d32e20aec..b06d1329dc1db 100644 --- a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts @@ -6,23 +6,17 @@ import Boom from '@hapi/boom'; import { LicenseCheck } from '../../../../licensing/server'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; -import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; import { defineCheckPrivilegesRoutes } from './privileges'; -import { APIKeys } from '../../authentication/api_keys'; interface TestOptions { licenseCheckResult?: LicenseCheck; - callAsInternalUserResponses?: Array<() => Promise>; - callAsCurrentUserResponses?: Array<() => Promise>; - asserts: { - statusCode: number; - result?: Record; - callAsInternalUserAPIArguments?: unknown[][]; - callAsCurrentUserAPIArguments?: unknown[][]; - }; + areAPIKeysEnabled?: boolean; + apiResponse?: () => Promise; + asserts: { statusCode: number; result?: Record; apiArguments?: unknown }; } describe('Check API keys privileges', () => { @@ -30,32 +24,23 @@ describe('Check API keys privileges', () => { description: string, { licenseCheckResult = { state: 'valid' }, - callAsInternalUserResponses = [], - callAsCurrentUserResponses = [], + areAPIKeysEnabled = true, + apiResponse, asserts, }: TestOptions ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const apiKeys = new APIKeys({ - logger: mockRouteDefinitionParams.logger, - clusterClient: mockRouteDefinitionParams.clusterClient, - license: mockRouteDefinitionParams.license, - }); - - mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() => - apiKeys.areAPIKeysEnabled() - ); + mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockResolvedValue(areAPIKeysEnabled); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - for (const apiResponse of callAsCurrentUserResponses) { - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); - } - for (const apiResponse of callAsInternalUserResponses) { - mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementationOnce( - apiResponse + if (apiResponse) { + mockContext.core.elasticsearch.client.asCurrentUser.security.hasPrivileges.mockImplementation( + (async () => ({ body: await apiResponse() })) as any ); } @@ -68,33 +53,15 @@ describe('Check API keys privileges', () => { path: '/internal/security/api_key/privileges', headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); - if (Array.isArray(asserts.callAsCurrentUserAPIArguments)) { - for (const apiArguments of asserts.callAsCurrentUserAPIArguments) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith( - mockRequest - ); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); - } - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); - } - - if (Array.isArray(asserts.callAsInternalUserAPIArguments)) { - for (const apiArguments of asserts.callAsInternalUserAPIArguments) { - expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith( - ...apiArguments - ); - } - } else { - expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).not.toHaveBeenCalled(); + if (asserts.apiArguments) { + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.hasPrivileges + ).toHaveBeenCalledWith(asserts.apiArguments); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); @@ -109,22 +76,13 @@ describe('Check API keys privileges', () => { const error = Boom.notAcceptable('test not acceptable message'); getPrivilegesTest('returns error from cluster client', { - callAsCurrentUserResponses: [ - async () => { - throw error; - }, - ], - callAsInternalUserResponses: [async () => {}], + apiResponse: async () => { + throw error; + }, asserts: { - callAsCurrentUserAPIArguments: [ - [ - 'shield.hasPrivileges', - { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, - ], - ], - callAsInternalUserAPIArguments: [ - ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], - ], + apiArguments: { + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, + }, statusCode: 406, result: error, }, @@ -133,40 +91,17 @@ describe('Check API keys privileges', () => { describe('success', () => { getPrivilegesTest('returns areApiKeysEnabled and isAdmin', { - callAsCurrentUserResponses: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: false }, - index: {}, - application: {}, - }), - ], - callAsInternalUserResponses: [ - async () => ({ - api_keys: [ - { - id: 'si8If24B1bKsmSLTAhJV', - name: 'my-api-key', - creation: 1574089261632, - expiration: 1574175661632, - invalidated: false, - username: 'elastic', - realm: 'reserved', - }, - ], - }), - ], + apiResponse: async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: false }, + index: {}, + application: {}, + }), asserts: { - callAsCurrentUserAPIArguments: [ - [ - 'shield.hasPrivileges', - { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, - ], - ], - callAsInternalUserAPIArguments: [ - ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], - ], + apiArguments: { + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, + }, statusCode: 200, result: { areApiKeysEnabled: true, isAdmin: true, canManage: true }, }, @@ -175,36 +110,18 @@ describe('Check API keys privileges', () => { getPrivilegesTest( 'returns areApiKeysEnabled=false when API Keys are disabled in Elasticsearch', { - callAsCurrentUserResponses: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: true }, - index: {}, - application: {}, - }), - ], - callAsInternalUserResponses: [ - async () => { - const error = new Error(); - (error as any).body = { - error: { - 'disabled.feature': 'api_keys', - }, - }; - throw error; - }, - ], + apiResponse: async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: true }, + index: {}, + application: {}, + }), + areAPIKeysEnabled: false, asserts: { - callAsCurrentUserAPIArguments: [ - [ - 'shield.hasPrivileges', - { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, - ], - ], - callAsInternalUserAPIArguments: [ - ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], - ], + apiArguments: { + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, + }, statusCode: 200, result: { areApiKeysEnabled: false, isAdmin: true, canManage: true }, }, @@ -212,52 +129,34 @@ describe('Check API keys privileges', () => { ); getPrivilegesTest('returns isAdmin=false when user has insufficient privileges', { - callAsCurrentUserResponses: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: false }, - index: {}, - application: {}, - }), - ], - callAsInternalUserResponses: [async () => ({})], + apiResponse: async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: false }, + index: {}, + application: {}, + }), asserts: { - callAsCurrentUserAPIArguments: [ - [ - 'shield.hasPrivileges', - { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, - ], - ], - callAsInternalUserAPIArguments: [ - ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], - ], + apiArguments: { + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, + }, statusCode: 200, result: { areApiKeysEnabled: true, isAdmin: false, canManage: false }, }, }); getPrivilegesTest('returns canManage=true when user can manage their own API Keys', { - callAsCurrentUserResponses: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: true }, - index: {}, - application: {}, - }), - ], - callAsInternalUserResponses: [async () => ({})], + apiResponse: async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: true }, + index: {}, + application: {}, + }), asserts: { - callAsCurrentUserAPIArguments: [ - [ - 'shield.hasPrivileges', - { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, - ], - ], - callAsInternalUserAPIArguments: [ - ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], - ], + apiArguments: { + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, + }, statusCode: 200, result: { areApiKeysEnabled: true, isAdmin: false, canManage: true }, }, diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.ts index 9cccb96752772..dd5d81060c7e5 100644 --- a/x-pack/plugins/security/server/routes/api_keys/privileges.ts +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.ts @@ -8,11 +8,7 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineCheckPrivilegesRoutes({ - router, - clusterClient, - authc, -}: RouteDefinitionParams) { +export function defineCheckPrivilegesRoutes({ router, authc }: RouteDefinitionParams) { router.get( { path: '/internal/security/api_key/privileges', @@ -20,19 +16,25 @@ export function defineCheckPrivilegesRoutes({ }, createLicensedRouteHandler(async (context, request, response) => { try { - const scopedClusterClient = clusterClient.asScoped(request); - const [ { - cluster: { - manage_security: manageSecurity, - manage_api_key: manageApiKey, - manage_own_api_key: manageOwnApiKey, + body: { + cluster: { + manage_security: manageSecurity, + manage_api_key: manageApiKey, + manage_own_api_key: manageOwnApiKey, + }, }, }, areApiKeysEnabled, ] = await Promise.all([ - scopedClusterClient.callAsCurrentUser('shield.hasPrivileges', { + context.core.elasticsearch.client.asCurrentUser.security.hasPrivileges<{ + cluster: { + manage_security: boolean; + manage_api_key: boolean; + manage_own_api_key: boolean; + }; + }>({ body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, }), authc.areAPIKeysEnabled(), diff --git a/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts b/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts index 08cd3ba487b0b..39e6a4838d34d 100644 --- a/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts +++ b/x-pack/plugins/security/server/routes/authorization/privileges/get_builtin.ts @@ -7,13 +7,13 @@ import { BuiltinESPrivileges } from '../../../../common/model'; import { RouteDefinitionParams } from '../..'; -export function defineGetBuiltinPrivilegesRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineGetBuiltinPrivilegesRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/esPrivileges/builtin', validate: false }, async (context, request, response) => { - const privileges: BuiltinESPrivileges = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getBuiltinPrivileges'); + const { + body: privileges, + } = await context.core.elasticsearch.client.asCurrentUser.security.getBuiltinPrivileges(); // Exclude the `none` privilege, as it doesn't make sense as an option within the Kibana UI privileges.cluster = privileges.cluster.filter((privilege) => privilege !== 'none'); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts index 9f5ec635f56cd..5143b727fcb4e 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts @@ -5,14 +5,11 @@ */ import Boom from '@hapi/boom'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../../src/core/server'; import { LicenseCheck } from '../../../../../licensing/server'; import { defineDeleteRolesRoutes } from './delete'; -import { - elasticsearchServiceMock, - httpServerMock, -} from '../../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; interface TestOptions { @@ -29,11 +26,15 @@ describe('DELETE role', () => { ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { - mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + mockContext.core.elasticsearch.client.asCurrentUser.security.deleteRole.mockImplementation( + (async () => ({ body: await apiResponse() })) as any + ); } defineDeleteRolesRoutes(mockRouteDefinitionParams); @@ -46,22 +47,15 @@ describe('DELETE role', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); if (apiResponse) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.deleteRole', - { name } - ); - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.deleteRole + ).toHaveBeenCalledWith({ name }); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts index eb56143288747..b877aaf6abd77 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts @@ -9,7 +9,7 @@ import { RouteDefinitionParams } from '../../index'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../../errors'; -export function defineDeleteRolesRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineDeleteRolesRoutes({ router }: RouteDefinitionParams) { router.delete( { path: '/api/security/role/{name}', @@ -19,7 +19,7 @@ export function defineDeleteRolesRoutes({ router, clusterClient }: RouteDefiniti }, createLicensedRouteHandler(async (context, request, response) => { try { - await clusterClient.asScoped(request).callAsCurrentUser('shield.deleteRole', { + await context.core.elasticsearch.client.asCurrentUser.security.deleteRole({ name: request.params.name, }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts index b25b13b9fc04a..a6090ee405329 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from '@hapi/boom'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../../src/core/server'; import { LicenseCheck } from '../../../../../licensing/server'; import { defineGetRolesRoutes } from './get'; -import { - elasticsearchServiceMock, - httpServerMock, -} from '../../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; const application = 'kibana-.kibana'; @@ -32,11 +29,15 @@ describe('GET role', () => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); mockRouteDefinitionParams.authz.applicationName = application; + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { - mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole.mockImplementation( + (async () => ({ body: await apiResponse() })) as any + ); } defineGetRolesRoutes(mockRouteDefinitionParams); @@ -49,22 +50,17 @@ describe('GET role', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); if (apiResponse) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.getRole', { - name, - }); - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole + ).toHaveBeenCalledWith({ name }); } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); }; diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts index bf1140e2e6fd1..ce4a622d30e61 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -8,9 +8,9 @@ import { schema } from '@kbn/config-schema'; import { RouteDefinitionParams } from '../..'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../../errors'; -import { transformElasticsearchRoleToRole } from './model'; +import { ElasticsearchRole, transformElasticsearchRoleToRole } from './model'; -export function defineGetRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) { +export function defineGetRolesRoutes({ router, authz }: RouteDefinitionParams) { router.get( { path: '/api/security/role/{name}', @@ -20,9 +20,11 @@ export function defineGetRolesRoutes({ router, authz, clusterClient }: RouteDefi }, createLicensedRouteHandler(async (context, request, response) => { try { - const elasticsearchRoles = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getRole', { name: request.params.name }); + const { + body: elasticsearchRoles, + } = await context.core.elasticsearch.client.asCurrentUser.security.getRole< + Record + >({ name: request.params.name }); const elasticsearchRole = elasticsearchRoles[request.params.name]; if (elasticsearchRole) { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts index 30e0c52c4c443..b3a855b2e0ae7 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from '@hapi/boom'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../../src/core/server'; import { LicenseCheck } from '../../../../../licensing/server'; import { defineGetAllRolesRoutes } from './get_all'; -import { - elasticsearchServiceMock, - httpServerMock, -} from '../../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; const application = 'kibana-.kibana'; @@ -32,11 +29,15 @@ describe('GET all roles', () => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); mockRouteDefinitionParams.authz.applicationName = application; + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { - mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole.mockImplementation( + (async () => ({ body: await apiResponse() })) as any + ); } defineGetAllRolesRoutes(mockRouteDefinitionParams); @@ -48,19 +49,15 @@ describe('GET all roles', () => { path: '/api/security/role', headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); if (apiResponse) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.getRole'); - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole + ).toHaveBeenCalled(); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts index 24be6c60e4b12..21521dd6dbae3 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts @@ -9,14 +9,16 @@ import { createLicensedRouteHandler } from '../../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../../errors'; import { ElasticsearchRole, transformElasticsearchRoleToRole } from './model'; -export function defineGetAllRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) { +export function defineGetAllRolesRoutes({ router, authz }: RouteDefinitionParams) { router.get( { path: '/api/security/role', validate: false }, createLicensedRouteHandler(async (context, request, response) => { try { - const elasticsearchRoles = (await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getRole')) as Record; + const { + body: elasticsearchRoles, + } = await context.core.elasticsearch.client.asCurrentUser.security.getRole< + Record + >(); // Transform elasticsearch roles into Kibana roles and return in a list sorted by the role name. return response.ok({ diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts index 811ea080b4316..779e1a7fab177 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts @@ -5,15 +5,12 @@ */ import { Type } from '@kbn/config-schema'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../../src/core/server'; import { LicenseCheck } from '../../../../../licensing/server'; import { GLOBAL_RESOURCE } from '../../../../common/constants'; import { definePutRolesRoutes } from './put'; -import { - elasticsearchServiceMock, - httpServerMock, -} from '../../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; import { KibanaFeature } from '../../../../../features/server'; import { securityFeatureUsageServiceMock } from '../../../feature_usage/index.mock'; @@ -47,35 +44,43 @@ const privilegeMap = { interface TestOptions { name: string; licenseCheckResult?: LicenseCheck; - apiResponses?: Array<() => Promise>; + apiResponses?: { + get: () => Promise; + put: () => Promise; + }; payload?: Record; asserts: { statusCode: number; result?: Record; - apiArguments?: unknown[][]; + apiArguments?: { get: unknown[]; put: unknown[] }; recordSubFeaturePrivilegeUsage?: boolean; }; } const putRoleTest = ( description: string, - { - name, - payload, - licenseCheckResult = { state: 'valid' }, - apiResponses = [], - asserts, - }: TestOptions + { name, payload, licenseCheckResult = { state: 'valid' }, apiResponses, asserts }: TestOptions ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); mockRouteDefinitionParams.authz.applicationName = application; mockRouteDefinitionParams.authz.privileges.get.mockReturnValue(privilegeMap); - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - for (const apiResponse of apiResponses) { - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; + + if (apiResponses?.get) { + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole.mockImplementationOnce( + (async () => ({ body: await apiResponses?.get() })) as any + ); + } + + if (apiResponses?.put) { + mockContext.core.elasticsearch.client.asCurrentUser.security.putRole.mockImplementationOnce( + (async () => ({ body: await apiResponses?.put() })) as any + ); } mockRouteDefinitionParams.getFeatureUsageService.mockReturnValue( @@ -131,21 +136,20 @@ const putRoleTest = ( body: payload !== undefined ? (validate as any).body.validate(payload) : undefined, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); - if (Array.isArray(asserts.apiArguments)) { - for (const apiArguments of asserts.apiArguments) { - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); - } - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + if (asserts.apiArguments?.get) { + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRole + ).toHaveBeenCalledWith(...asserts.apiArguments?.get); + } + if (asserts.apiArguments?.put) { + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRole + ).toHaveBeenCalledWith(...asserts.apiArguments?.put); } expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); @@ -208,12 +212,11 @@ describe('PUT role', () => { putRoleTest(`creates empty role`, { name: 'foo-role', payload: {}, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -224,7 +227,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -239,12 +242,11 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -261,7 +263,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -279,12 +281,11 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -301,7 +302,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -317,12 +318,11 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -339,7 +339,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -383,12 +383,11 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -426,7 +425,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -473,8 +472,8 @@ describe('PUT role', () => { }, ], }, - apiResponses: [ - async () => ({ + apiResponses: { + get: async () => ({ 'foo-role': { metadata: { bar: 'old-metadata', @@ -504,13 +503,12 @@ describe('PUT role', () => { ], }, }), - async () => {}, - ], + put: async () => {}, + }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -548,7 +546,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -577,8 +575,8 @@ describe('PUT role', () => { }, ], }, - apiResponses: [ - async () => ({ + apiResponses: { + get: async () => ({ 'foo-role': { metadata: { bar: 'old-metadata', @@ -613,13 +611,12 @@ describe('PUT role', () => { ], }, }), - async () => {}, - ], + put: async () => {}, + }, asserts: { - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -652,7 +649,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -670,13 +667,12 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { recordSubFeaturePrivilegeUsage: true, - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -694,7 +690,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -712,13 +708,12 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { recordSubFeaturePrivilegeUsage: false, - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -736,7 +731,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, @@ -754,13 +749,12 @@ describe('PUT role', () => { }, ], }, - apiResponses: [async () => ({}), async () => {}], + apiResponses: { get: async () => ({}), put: async () => {} }, asserts: { recordSubFeaturePrivilegeUsage: false, - apiArguments: [ - ['shield.getRole', { name: 'foo-role', ignore: [404] }], - [ - 'shield.putRole', + apiArguments: { + get: [{ name: 'foo-role' }, { ignore: [404] }], + put: [ { name: 'foo-role', body: { @@ -778,7 +772,7 @@ describe('PUT role', () => { }, }, ], - ], + }, statusCode: 204, result: undefined, }, diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.ts index cdedc9ac8a5eb..26c61b4ced15f 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -42,7 +42,6 @@ const roleGrantsSubFeaturePrivileges = ( export function definePutRolesRoutes({ router, authz, - clusterClient, getFeatures, getFeatureUsageService, }: RouteDefinitionParams) { @@ -64,12 +63,11 @@ export function definePutRolesRoutes({ const { name } = request.params; try { - const rawRoles: Record = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getRole', { - name: request.params.name, - ignore: [404], - }); + const { + body: rawRoles, + } = await context.core.elasticsearch.client.asCurrentUser.security.getRole< + Record + >({ name: request.params.name }, { ignore: [404] }); const body = transformPutPayloadToElasticsearchRole( request.body, @@ -77,11 +75,12 @@ export function definePutRolesRoutes({ rawRoles[name] ? rawRoles[name].applications : [] ); - const [features] = await Promise.all([ + const [features] = await Promise.all([ getFeatures(), - clusterClient - .asScoped(request) - .callAsCurrentUser('shield.putRole', { name: request.params.name, body }), + context.core.elasticsearch.client.asCurrentUser.security.putRole({ + name: request.params.name, + body, + }), ]); if (roleGrantsSubFeaturePrivileges(features, request.body)) { diff --git a/x-pack/plugins/security/server/routes/index.mock.ts b/x-pack/plugins/security/server/routes/index.mock.ts index fab4a71df0cb0..1df499d981632 100644 --- a/x-pack/plugins/security/server/routes/index.mock.ts +++ b/x-pack/plugins/security/server/routes/index.mock.ts @@ -5,7 +5,6 @@ */ import { - elasticsearchServiceMock, httpServiceMock, loggingSystemMock, httpResourcesMock, @@ -25,7 +24,6 @@ export const routeDefinitionParamsMock = { basePath: httpServiceMock.createBasePath(), csp: httpServiceMock.createSetupContract().csp, logger: loggingSystemMock.create().get(), - clusterClient: elasticsearchServiceMock.createLegacyClusterClient(), config: createConfig(ConfigSchema.validate(config), loggingSystemMock.create().get(), { isTLSEnabled: false, }), diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index 079c9e8ab9ce7..db71b04b3e6f0 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -5,13 +5,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; import { KibanaFeature } from '../../../features/server'; -import { - HttpResources, - IBasePath, - ILegacyClusterClient, - IRouter, - Logger, -} from '../../../../../src/core/server'; +import { HttpResources, IBasePath, IRouter, Logger } from '../../../../../src/core/server'; import { SecurityLicense } from '../../common/licensing'; import { Authentication } from '../authentication'; import { AuthorizationServiceSetup } from '../authorization'; @@ -36,7 +30,6 @@ export interface RouteDefinitionParams { basePath: IBasePath; httpResources: HttpResources; logger: Logger; - clusterClient: ILegacyClusterClient; config: ConfigType; authc: Authentication; authz: AuthorizationServiceSetup; diff --git a/x-pack/plugins/security/server/routes/indices/get_fields.test.ts b/x-pack/plugins/security/server/routes/indices/get_fields.test.ts index 4c6182e99431d..6d3de11249a16 100644 --- a/x-pack/plugins/security/server/routes/indices/get_fields.test.ts +++ b/x-pack/plugins/security/server/routes/indices/get_fields.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServerMock, elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; +import { httpServerMock, coreMock } from '../../../../../../src/core/server/mocks'; import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { routeDefinitionParamsMock } from '../index.mock'; @@ -36,10 +36,12 @@ const mockFieldMappingResponse = { describe('GET /internal/security/fields/{query}', () => { it('returns a list of deduplicated fields, omitting empty and runtime fields', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const scopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - scopedClient.callAsCurrentUser.mockResolvedValue(mockFieldMappingResponse); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(scopedClient); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + }; + mockContext.core.elasticsearch.client.asCurrentUser.indices.getFieldMapping.mockImplementation( + (async () => ({ body: mockFieldMappingResponse })) as any + ); defineGetFieldsRoutes(mockRouteDefinitionParams); @@ -51,7 +53,7 @@ describe('GET /internal/security/fields/{query}', () => { path: `/internal/security/fields/foo`, headers, }); - const response = await handler({} as any, mockRequest, kibanaResponseFactory); + const response = await handler(mockContext as any, mockRequest, kibanaResponseFactory); expect(response.status).toBe(200); expect(response.payload).toEqual(['fooField', 'commonField', 'barField']); }); diff --git a/x-pack/plugins/security/server/routes/indices/get_fields.ts b/x-pack/plugins/security/server/routes/indices/get_fields.ts index 44b8804ed8d6e..304e121f7fee1 100644 --- a/x-pack/plugins/security/server/routes/indices/get_fields.ts +++ b/x-pack/plugins/security/server/routes/indices/get_fields.ts @@ -22,7 +22,7 @@ interface FieldMappingResponse { }; } -export function defineGetFieldsRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineGetFieldsRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/fields/{query}', @@ -30,14 +30,16 @@ export function defineGetFieldsRoutes({ router, clusterClient }: RouteDefinition }, async (context, request, response) => { try { - const indexMappings = (await clusterClient - .asScoped(request) - .callAsCurrentUser('indices.getFieldMapping', { + const { + body: indexMappings, + } = await context.core.elasticsearch.client.asCurrentUser.indices.getFieldMapping( + { index: request.params.query, fields: '*', - allowNoIndices: false, - includeDefaults: true, - })) as FieldMappingResponse; + allow_no_indices: false, + include_defaults: true, + } + ); // The flow is the following (see response format at https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html): // 1. Iterate over all matched indices. diff --git a/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts b/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts index aec0310129f6e..33fd66f9e929d 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts @@ -5,24 +5,26 @@ */ import { routeDefinitionParamsMock } from '../index.mock'; -import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { defineRoleMappingDeleteRoutes } from './delete'; describe('DELETE role mappings', () => { it('allows a role mapping to be deleted', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({ acknowledged: true }); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue({ state: 'valid' }) } } as any, + }; + mockContext.core.elasticsearch.client.asCurrentUser.security.deleteRoleMapping.mockResolvedValue( + { body: { acknowledged: true } } as any + ); defineRoleMappingDeleteRoutes(mockRouteDefinitionParams); const [[, handler]] = mockRouteDefinitionParams.router.delete.mock.calls; const name = 'mapping1'; - const headers = { authorization: 'foo' }; const mockRequest = httpServerMock.createKibanaRequest({ method: 'delete', @@ -30,31 +32,35 @@ describe('DELETE role mappings', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(200); expect(response.payload).toEqual({ acknowledged: true }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); expect( - mockScopedClusterClient.callAsCurrentUser - ).toHaveBeenCalledWith('shield.deleteRoleMapping', { name }); + mockContext.core.elasticsearch.client.asCurrentUser.security.deleteRoleMapping + ).toHaveBeenCalledWith({ name }); }); describe('failure', () => { it('returns result of license check', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { + license: { + check: jest.fn().mockReturnValue({ + state: 'invalid', + message: 'test forbidden message', + }), + }, + } as any, + }; defineRoleMappingDeleteRoutes(mockRouteDefinitionParams); const [[, handler]] = mockRouteDefinitionParams.router.delete.mock.calls; const name = 'mapping1'; - const headers = { authorization: 'foo' }; const mockRequest = httpServerMock.createKibanaRequest({ method: 'delete', @@ -62,21 +68,13 @@ describe('DELETE role mappings', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { - license: { - check: jest.fn().mockReturnValue({ - state: 'invalid', - message: 'test forbidden message', - }), - }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(403); expect(response.payload).toEqual({ message: 'test forbidden message' }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.deleteRoleMapping + ).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/security/server/routes/role_mapping/delete.ts b/x-pack/plugins/security/server/routes/role_mapping/delete.ts index dc11bcd914b35..dbe9c5662a1f1 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/delete.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/delete.ts @@ -8,9 +8,7 @@ import { createLicensedRouteHandler } from '../licensed_route_handler'; import { wrapError } from '../../errors'; import { RouteDefinitionParams } from '..'; -export function defineRoleMappingDeleteRoutes(params: RouteDefinitionParams) { - const { clusterClient, router } = params; - +export function defineRoleMappingDeleteRoutes({ router }: RouteDefinitionParams) { router.delete( { path: '/internal/security/role_mapping/{name}', @@ -22,11 +20,11 @@ export function defineRoleMappingDeleteRoutes(params: RouteDefinitionParams) { }, createLicensedRouteHandler(async (context, request, response) => { try { - const deleteResponse = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.deleteRoleMapping', { - name: request.params.name, - }); + const { + body: deleteResponse, + } = await context.core.elasticsearch.client.asCurrentUser.security.deleteRoleMapping({ + name: request.params.name, + }); return response.ok({ body: deleteResponse }); } catch (error) { const wrappedError = wrapError(error); diff --git a/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts b/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts index ee1d550bbe24d..8bd9f095b0f68 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts @@ -5,21 +5,16 @@ */ import { routeDefinitionParamsMock } from '../index.mock'; -import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; -import { - kibanaResponseFactory, - RequestHandlerContext, - ILegacyClusterClient, -} from '../../../../../../src/core/server'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { LicenseCheck } from '../../../../licensing/server'; import { defineRoleMappingFeatureCheckRoute } from './feature_check'; interface TestOptions { licenseCheckResult?: LicenseCheck; canManageRoleMappings?: boolean; - nodeSettingsResponse?: Record; - xpackUsageResponse?: Record; - internalUserClusterClientImpl?: ILegacyClusterClient['callAsInternalUser']; + nodeSettingsResponse?: () => Record; + xpackUsageResponse?: () => Record; asserts: { statusCode: number; result?: Record }; } @@ -38,57 +33,34 @@ const defaultXpackUsageResponse = { }, }; -const getDefaultInternalUserClusterClientImpl = ( - nodeSettingsResponse: TestOptions['nodeSettingsResponse'], - xpackUsageResponse: TestOptions['xpackUsageResponse'] -) => - ((async (endpoint: string, clientParams: Record) => { - if (!clientParams) throw new TypeError('expected clientParams'); - - if (endpoint === 'nodes.info') { - return nodeSettingsResponse; - } - - if (endpoint === 'transport.request') { - if (clientParams.path === '/_xpack/usage') { - return xpackUsageResponse; - } - } - - throw new Error(`unexpected endpoint: ${endpoint}`); - }) as unknown) as TestOptions['internalUserClusterClientImpl']; - describe('GET role mappings feature check', () => { const getFeatureCheckTest = ( description: string, { licenseCheckResult = { state: 'valid' }, canManageRoleMappings = true, - nodeSettingsResponse = {}, - xpackUsageResponse = defaultXpackUsageResponse, - internalUserClusterClientImpl = getDefaultInternalUserClusterClientImpl( - nodeSettingsResponse, - xpackUsageResponse - ), + nodeSettingsResponse = async () => ({}), + xpackUsageResponse = async () => defaultXpackUsageResponse, asserts, }: TestOptions ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementation( - internalUserClusterClientImpl + mockContext.core.elasticsearch.client.asInternalUser.nodes.info.mockImplementation( + (async () => ({ body: await nodeSettingsResponse() })) as any + ); + mockContext.core.elasticsearch.client.asInternalUser.transport.request.mockImplementation( + (async () => ({ body: await xpackUsageResponse() })) as any ); - mockScopedClusterClient.callAsCurrentUser.mockImplementation(async (method, payload) => { - if (method === 'shield.hasPrivileges') { - return { - has_all_requested: canManageRoleMappings, - }; - } - }); + mockContext.core.elasticsearch.client.asCurrentUser.security.hasPrivileges.mockResolvedValue({ + body: { has_all_requested: canManageRoleMappings }, + } as any); defineRoleMappingFeatureCheckRoute(mockRouteDefinitionParams); const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; @@ -99,9 +71,6 @@ describe('GET role mappings feature check', () => { path: `/internal/security/_check_role_mapping_features`, headers, }); - const mockContext = ({ - licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(asserts.statusCode); @@ -124,7 +93,7 @@ describe('GET role mappings feature check', () => { }); getFeatureCheckTest('allows both script types when explicitly enabled', { - nodeSettingsResponse: { + nodeSettingsResponse: async () => ({ nodes: { someNodeId: { settings: { @@ -134,7 +103,7 @@ describe('GET role mappings feature check', () => { }, }, }, - }, + }), asserts: { statusCode: 200, result: { @@ -147,7 +116,7 @@ describe('GET role mappings feature check', () => { }); getFeatureCheckTest('disallows stored scripts when disabled', { - nodeSettingsResponse: { + nodeSettingsResponse: async () => ({ nodes: { someNodeId: { settings: { @@ -157,7 +126,7 @@ describe('GET role mappings feature check', () => { }, }, }, - }, + }), asserts: { statusCode: 200, result: { @@ -170,7 +139,7 @@ describe('GET role mappings feature check', () => { }); getFeatureCheckTest('disallows inline scripts when disabled', { - nodeSettingsResponse: { + nodeSettingsResponse: async () => ({ nodes: { someNodeId: { settings: { @@ -180,7 +149,7 @@ describe('GET role mappings feature check', () => { }, }, }, - }, + }), asserts: { statusCode: 200, result: { @@ -193,7 +162,7 @@ describe('GET role mappings feature check', () => { }); getFeatureCheckTest('indicates incompatible realms when only native and file are enabled', { - xpackUsageResponse: { + xpackUsageResponse: async () => ({ security: { realms: { native: { @@ -206,7 +175,7 @@ describe('GET role mappings feature check', () => { }, }, }, - }, + }), asserts: { statusCode: 200, result: { @@ -231,9 +200,12 @@ describe('GET role mappings feature check', () => { getFeatureCheckTest( 'falls back to allowing both script types if there is an error retrieving node settings', { - internalUserClusterClientImpl: (() => { - return Promise.reject(new Error('something bad happened')); - }) as TestOptions['internalUserClusterClientImpl'], + nodeSettingsResponse: async () => { + throw new Error('something bad happened'); + }, + xpackUsageResponse: async () => { + throw new Error('something bad happened'); + }, asserts: { statusCode: 200, result: { diff --git a/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts b/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts index 88c7f193cea34..470039b8ae92b 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, ILegacyClusterClient } from 'src/core/server'; +import { ElasticsearchClient, Logger } from 'src/core/server'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; @@ -34,24 +34,18 @@ interface XPackUsageResponse { const INCOMPATIBLE_REALMS = ['file', 'native']; -export function defineRoleMappingFeatureCheckRoute({ - router, - clusterClient, - logger, -}: RouteDefinitionParams) { +export function defineRoleMappingFeatureCheckRoute({ router, logger }: RouteDefinitionParams) { router.get( { path: '/internal/security/_check_role_mapping_features', validate: false, }, createLicensedRouteHandler(async (context, request, response) => { - const { has_all_requested: canManageRoleMappings } = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.hasPrivileges', { - body: { - cluster: ['manage_security'], - }, - }); + const { + body: { has_all_requested: canManageRoleMappings }, + } = await context.core.elasticsearch.client.asCurrentUser.security.hasPrivileges<{ + has_all_requested: boolean; + }>({ body: { cluster: ['manage_security'] } }); if (!canManageRoleMappings) { return response.ok({ @@ -61,7 +55,10 @@ export function defineRoleMappingFeatureCheckRoute({ }); } - const enabledFeatures = await getEnabledRoleMappingsFeatures(clusterClient, logger); + const enabledFeatures = await getEnabledRoleMappingsFeatures( + context.core.elasticsearch.client.asInternalUser, + logger + ); return response.ok({ body: { @@ -73,13 +70,12 @@ export function defineRoleMappingFeatureCheckRoute({ ); } -async function getEnabledRoleMappingsFeatures(clusterClient: ILegacyClusterClient, logger: Logger) { +async function getEnabledRoleMappingsFeatures(esClient: ElasticsearchClient, logger: Logger) { logger.debug(`Retrieving role mappings features`); - const nodeScriptSettingsPromise: Promise = clusterClient - .callAsInternalUser('nodes.info', { - filterPath: 'nodes.*.settings.script', - }) + const nodeScriptSettingsPromise = esClient.nodes + .info({ filter_path: 'nodes.*.settings.script' }) + .then(({ body }) => body) .catch((error) => { // fall back to assuming that node settings are unset/at their default values. // this will allow the role mappings UI to permit both role template script types, @@ -88,13 +84,11 @@ async function getEnabledRoleMappingsFeatures(clusterClient: ILegacyClusterClien return {}; }); - const xpackUsagePromise: Promise = clusterClient - // `transport.request` is potentially unsafe when combined with untrusted user input. - // Do not augment with such input. - .callAsInternalUser('transport.request', { - method: 'GET', - path: '/_xpack/usage', - }) + // `transport.request` is potentially unsafe when combined with untrusted user input. + // Do not augment with such input. + const xpackUsagePromise = esClient.transport + .request({ method: 'GET', path: '/_xpack/usage' }) + .then(({ body }) => body as XPackUsageResponse) .catch((error) => { // fall back to no external realms configured. // this will cause a warning in the UI about no compatible realms being enabled, but will otherwise allow diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.test.ts b/x-pack/plugins/security/server/routes/role_mapping/get.test.ts index 2519034b386bf..625aae42a3907 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.test.ts @@ -6,9 +6,9 @@ import Boom from '@hapi/boom'; import { routeDefinitionParamsMock } from '../index.mock'; -import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { defineRoleMappingGetRoutes } from './get'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; const mockRoleMappingResponse = { mapping1: { @@ -49,13 +49,22 @@ const mockRoleMappingResponse = { }, }; +function getMockContext( + licenseCheckResult: { state: string; message?: string } = { state: 'valid' } +) { + return { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any, + }; +} + describe('GET role mappings', () => { it('returns all role mappings', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(mockRoleMappingResponse); + const mockContext = getMockContext(); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockResolvedValue({ + body: mockRoleMappingResponse, + } as any); defineRoleMappingGetRoutes(mockRouteDefinitionParams); @@ -67,11 +76,6 @@ describe('GET role mappings', () => { path: `/internal/security/role_mapping`, headers, }); - const mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(200); @@ -118,29 +122,27 @@ describe('GET role mappings', () => { }, ]); - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.getRoleMappings', - { name: undefined } - ); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping + ).toHaveBeenCalledWith({ name: undefined }); }); it('returns role mapping by name', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({ - mapping1: { - enabled: true, - roles: ['foo', 'bar'], - rules: { - field: { - dn: 'CN=bob,OU=example,O=com', + const mockContext = getMockContext(); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockResolvedValue({ + body: { + mapping1: { + enabled: true, + roles: ['foo', 'bar'], + rules: { + field: { + dn: 'CN=bob,OU=example,O=com', + }, }, }, }, - }); + } as any); defineRoleMappingGetRoutes(mockRouteDefinitionParams); @@ -155,11 +157,6 @@ describe('GET role mappings', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(200); @@ -175,16 +172,15 @@ describe('GET role mappings', () => { }, }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.getRoleMappings', - { name } - ); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping + ).toHaveBeenCalledWith({ name }); }); describe('failure', () => { it('returns result of license check', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = getMockContext({ state: 'invalid', message: 'test forbidden message' }); defineRoleMappingGetRoutes(mockRouteDefinitionParams); @@ -196,29 +192,19 @@ describe('GET role mappings', () => { path: `/internal/security/role_mapping`, headers, }); - const mockContext = ({ - licensing: { - license: { - check: jest.fn().mockReturnValue({ - state: 'invalid', - message: 'test forbidden message', - }), - }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(403); expect(response.payload).toEqual({ message: 'test forbidden message' }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping + ).not.toHaveBeenCalled(); }); it('returns a 404 when the role mapping is not found', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + const mockContext = getMockContext(); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockRejectedValue( Boom.notFound('role mapping not found!') ); @@ -235,18 +221,12 @@ describe('GET role mappings', () => { params: { name }, headers, }); - const mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(404); - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); expect( - mockScopedClusterClient.callAsCurrentUser - ).toHaveBeenCalledWith('shield.getRoleMappings', { name }); + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping + ).toHaveBeenCalledWith({ name }); }); }); }); diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts index 63598584b5d1b..5ab9b1f6b4a24 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.ts @@ -14,7 +14,7 @@ interface RoleMappingsResponse { } export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) { - const { clusterClient, logger, router } = params; + const { logger, router } = params; router.get( { @@ -29,13 +29,11 @@ export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) { const expectSingleEntity = typeof request.params.name === 'string'; try { - const roleMappingsResponse: RoleMappingsResponse = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getRoleMappings', { - name: request.params.name, - }); + const roleMappingsResponse = await context.core.elasticsearch.client.asCurrentUser.security.getRoleMapping( + { name: request.params.name } + ); - const mappings = Object.entries(roleMappingsResponse).map(([name, mapping]) => { + const mappings = Object.entries(roleMappingsResponse.body).map(([name, mapping]) => { return { name, ...mapping, diff --git a/x-pack/plugins/security/server/routes/role_mapping/post.test.ts b/x-pack/plugins/security/server/routes/role_mapping/post.test.ts index 8f61d2a122f0c..5dc7a21a02c6f 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/post.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/post.test.ts @@ -5,17 +5,20 @@ */ import { routeDefinitionParamsMock } from '../index.mock'; -import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks'; -import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; import { defineRoleMappingPostRoutes } from './post'; describe('POST role mappings', () => { it('allows a role mapping to be created', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({ created: true }); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue({ state: 'valid' }) } } as any, + }; + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping.mockResolvedValue({ + body: { created: true }, + } as any); defineRoleMappingPostRoutes(mockRouteDefinitionParams); @@ -39,37 +42,41 @@ describe('POST role mappings', () => { }, headers, }); - const mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ state: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(200); expect(response.payload).toEqual({ created: true }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.saveRoleMapping', - { - name, - body: { - enabled: true, - roles: ['foo', 'bar'], - rules: { - field: { - dn: 'CN=bob,OU=example,O=com', - }, + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).toHaveBeenCalledWith({ + name, + body: { + enabled: true, + roles: ['foo', 'bar'], + rules: { + field: { + dn: 'CN=bob,OU=example,O=com', }, }, - } - ); + }, + }); }); describe('failure', () => { it('returns result of license check', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { + license: { + check: jest.fn().mockReturnValue({ + state: 'invalid', + message: 'test forbidden message', + }), + }, + } as any, + }; defineRoleMappingPostRoutes(mockRouteDefinitionParams); @@ -81,22 +88,14 @@ describe('POST role mappings', () => { path: `/internal/security/role_mapping`, headers, }); - const mockContext = ({ - licensing: { - license: { - check: jest.fn().mockReturnValue({ - state: 'invalid', - message: 'test forbidden message', - }), - }, - }, - } as unknown) as RequestHandlerContext; const response = await handler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(403); expect(response.payload).toEqual({ message: 'test forbidden message' }); - expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/security/server/routes/role_mapping/post.ts b/x-pack/plugins/security/server/routes/role_mapping/post.ts index 11149f38069a7..6c1b19dacb601 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/post.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/post.ts @@ -8,9 +8,7 @@ import { createLicensedRouteHandler } from '../licensed_route_handler'; import { wrapError } from '../../errors'; import { RouteDefinitionParams } from '..'; -export function defineRoleMappingPostRoutes(params: RouteDefinitionParams) { - const { clusterClient, router } = params; - +export function defineRoleMappingPostRoutes({ router }: RouteDefinitionParams) { router.post( { path: '/internal/security/role_mapping/{name}', @@ -43,13 +41,10 @@ export function defineRoleMappingPostRoutes(params: RouteDefinitionParams) { }, createLicensedRouteHandler(async (context, request, response) => { try { - const saveResponse = await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.saveRoleMapping', { - name: request.params.name, - body: request.body, - }); - return response.ok({ body: saveResponse }); + const saveResponse = await context.core.elasticsearch.client.asCurrentUser.security.putRoleMapping( + { name: request.params.name, body: request.body } + ); + return response.ok({ body: saveResponse.body }); } catch (error) { const wrappedError = wrapError(error); return response.customError({ diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index c66b5f985cb33..d98c0acb7d86d 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -7,21 +7,20 @@ import { errors } from 'elasticsearch'; import { ObjectType } from '@kbn/config-schema'; import type { PublicMethodsOf } from '@kbn/utility-types'; +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { - ILegacyClusterClient, + Headers, IRouter, - ILegacyScopedClusterClient, kibanaResponseFactory, RequestHandler, RequestHandlerContext, RouteConfig, - ScopeableRequest, } from '../../../../../../src/core/server'; import { Authentication, AuthenticationResult } from '../../authentication'; import { Session } from '../../session_management'; import { defineChangeUserPasswordRoutes } from './change_password'; -import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import { sessionMock } from '../../session_management/session.mock'; import { routeDefinitionParamsMock } from '../index.mock'; @@ -30,19 +29,19 @@ describe('Change password', () => { let router: jest.Mocked; let authc: jest.Mocked; let session: jest.Mocked>; - let mockClusterClient: jest.Mocked; - let mockScopedClusterClient: jest.Mocked; let routeHandler: RequestHandler; let routeConfig: RouteConfig; - let mockContext: RequestHandlerContext; - - function checkPasswordChangeAPICall(username: string, request: ScopeableRequest) { - expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); - expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( - 'shield.changePassword', - { username, body: { password: 'new-password' } } + let mockContext: DeeplyMockedKeys; + + function checkPasswordChangeAPICall(username: string, headers?: Headers) { + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword + ).toHaveBeenCalledTimes(1); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword + ).toHaveBeenCalledWith( + { username, body: { password: 'new-password' } }, + headers && { headers } ); } @@ -56,15 +55,10 @@ describe('Change password', () => { authc.login.mockResolvedValue(AuthenticationResult.succeeded(mockAuthenticatedUser())); session.get.mockResolvedValue(sessionMock.createValue()); - mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockClusterClient = routeParamsMock.clusterClient; - mockClusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - - mockContext = ({ - licensing: { - license: { check: jest.fn().mockReturnValue({ check: 'valid' }) }, - }, - } as unknown) as RequestHandlerContext; + mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue({ state: 'valid' }) } }, + } as any; defineChangeUserPasswordRoutes(routeParamsMock); @@ -114,20 +108,18 @@ describe('Change password', () => { const changePasswordFailure = new (errors.AuthenticationException as any)('Unauthorized', { body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, }); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(changePasswordFailure); + mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword.mockRejectedValue( + changePasswordFailure + ); const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); expect(response.status).toBe(403); expect(response.payload).toEqual(changePasswordFailure); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); - expect(mockClusterClient.asScoped).toHaveBeenCalledWith({ - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + checkPasswordChangeAPICall(username, { + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); }); @@ -148,16 +140,16 @@ describe('Change password', () => { expect(response.payload).toEqual(loginFailureReason); checkPasswordChangeAPICall(username, { - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); }); it('returns 500 if password update request fails with non-401 error.', async () => { const failureReason = new Error('Request failed.'); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword.mockRejectedValue( + failureReason + ); const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); @@ -165,10 +157,8 @@ describe('Change password', () => { expect(response.payload).toEqual(failureReason); checkPasswordChangeAPICall(username, { - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); }); @@ -179,10 +169,8 @@ describe('Change password', () => { expect(response.payload).toBeUndefined(); checkPasswordChangeAPICall(username, { - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); expect(authc.login).toHaveBeenCalledTimes(1); @@ -209,10 +197,8 @@ describe('Change password', () => { expect(response.payload).toBeUndefined(); checkPasswordChangeAPICall(username, { - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); expect(authc.login).toHaveBeenCalledTimes(1); @@ -230,10 +216,8 @@ describe('Change password', () => { expect(response.payload).toBeUndefined(); checkPasswordChangeAPICall(username, { - headers: { - ...mockRequest.headers, - authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, - }, + ...mockRequest.headers, + authorization: `Basic ${Buffer.from(`${username}:old-password`).toString('base64')}`, }); expect(authc.login).not.toHaveBeenCalled(); @@ -249,7 +233,9 @@ describe('Change password', () => { it('returns 500 if password update request fails.', async () => { const failureReason = new Error('Request failed.'); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockContext.core.elasticsearch.client.asCurrentUser.security.changePassword.mockRejectedValue( + failureReason + ); const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); @@ -257,7 +243,7 @@ describe('Change password', () => { expect(response.payload).toEqual(failureReason); expect(authc.login).not.toHaveBeenCalled(); - checkPasswordChangeAPICall(username, mockRequest); + checkPasswordChangeAPICall(username); }); it('successfully changes user password.', async () => { @@ -267,7 +253,7 @@ describe('Change password', () => { expect(response.payload).toBeUndefined(); expect(authc.login).not.toHaveBeenCalled(); - checkPasswordChangeAPICall(username, mockRequest); + checkPasswordChangeAPICall(username); }); }); }); diff --git a/x-pack/plugins/security/server/routes/users/change_password.ts b/x-pack/plugins/security/server/routes/users/change_password.ts index be868f841eeeb..66d36b4294883 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.ts @@ -14,12 +14,7 @@ import { } from '../../authentication'; import { RouteDefinitionParams } from '..'; -export function defineChangeUserPasswordRoutes({ - authc, - session, - router, - clusterClient, -}: RouteDefinitionParams) { +export function defineChangeUserPasswordRoutes({ authc, session, router }: RouteDefinitionParams) { router.post( { path: '/internal/security/users/{username}/password', @@ -43,28 +38,26 @@ export function defineChangeUserPasswordRoutes({ // If user is changing their own password they should provide a proof of knowledge their // current password via sending it in `Authorization: Basic base64(username:current password)` // HTTP header no matter how they logged in to Kibana. - const scopedClusterClient = clusterClient.asScoped( - isUserChangingOwnPassword - ? { - headers: { - ...request.headers, - authorization: new HTTPAuthorizationHeader( - 'Basic', - new BasicHTTPAuthorizationHeaderCredentials( - username, - currentPassword || '' - ).toString() - ).toString(), - }, - } - : request - ); + const options = isUserChangingOwnPassword + ? { + headers: { + ...request.headers, + authorization: new HTTPAuthorizationHeader( + 'Basic', + new BasicHTTPAuthorizationHeaderCredentials( + username, + currentPassword || '' + ).toString() + ).toString(), + }, + } + : undefined; try { - await scopedClusterClient.callAsCurrentUser('shield.changePassword', { - username, - body: { password: newPassword }, - }); + await context.core.elasticsearch.client.asCurrentUser.security.changePassword( + { username, body: { password: newPassword } }, + options + ); } catch (error) { // This may happen only if user's credentials are rejected meaning that current password // isn't correct. diff --git a/x-pack/plugins/security/server/routes/users/create_or_update.ts b/x-pack/plugins/security/server/routes/users/create_or_update.ts index 5a3e50bb11d5c..a98848a583500 100644 --- a/x-pack/plugins/security/server/routes/users/create_or_update.ts +++ b/x-pack/plugins/security/server/routes/users/create_or_update.ts @@ -9,7 +9,7 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineCreateOrUpdateUserRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineCreateOrUpdateUserRoutes({ router }: RouteDefinitionParams) { router.post( { path: '/internal/security/users/{username}', @@ -28,7 +28,7 @@ export function defineCreateOrUpdateUserRoutes({ router, clusterClient }: RouteD }, createLicensedRouteHandler(async (context, request, response) => { try { - await clusterClient.asScoped(request).callAsCurrentUser('shield.putUser', { + await context.core.elasticsearch.client.asCurrentUser.security.putUser({ username: request.params.username, // Omit `username`, `enabled` and all fields with `null` value. body: Object.fromEntries( diff --git a/x-pack/plugins/security/server/routes/users/delete.ts b/x-pack/plugins/security/server/routes/users/delete.ts index 99a8d5c18ab3d..26a1765b4fbdf 100644 --- a/x-pack/plugins/security/server/routes/users/delete.ts +++ b/x-pack/plugins/security/server/routes/users/delete.ts @@ -9,7 +9,7 @@ import { RouteDefinitionParams } from '../index'; import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; -export function defineDeleteUserRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineDeleteUserRoutes({ router }: RouteDefinitionParams) { router.delete( { path: '/internal/security/users/{username}', @@ -19,9 +19,9 @@ export function defineDeleteUserRoutes({ router, clusterClient }: RouteDefinitio }, createLicensedRouteHandler(async (context, request, response) => { try { - await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.deleteUser', { username: request.params.username }); + await context.core.elasticsearch.client.asCurrentUser.security.deleteUser({ + username: request.params.username, + }); return response.noContent(); } catch (error) { diff --git a/x-pack/plugins/security/server/routes/users/get.ts b/x-pack/plugins/security/server/routes/users/get.ts index 0867910372546..aa6a4f6be8bad 100644 --- a/x-pack/plugins/security/server/routes/users/get.ts +++ b/x-pack/plugins/security/server/routes/users/get.ts @@ -9,7 +9,7 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineGetUserRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineGetUserRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/users/{username}', @@ -20,9 +20,13 @@ export function defineGetUserRoutes({ router, clusterClient }: RouteDefinitionPa createLicensedRouteHandler(async (context, request, response) => { try { const username = request.params.username; - const users = (await clusterClient - .asScoped(request) - .callAsCurrentUser('shield.getUser', { username })) as Record; + const { + body: users, + } = await context.core.elasticsearch.client.asCurrentUser.security.getUser< + Record + >({ + username, + }); if (!users[username]) { return response.notFound(); diff --git a/x-pack/plugins/security/server/routes/users/get_all.ts b/x-pack/plugins/security/server/routes/users/get_all.ts index 492ab27ab27ad..3c5ca184f747c 100644 --- a/x-pack/plugins/security/server/routes/users/get_all.ts +++ b/x-pack/plugins/security/server/routes/users/get_all.ts @@ -8,7 +8,7 @@ import { RouteDefinitionParams } from '../index'; import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; -export function defineGetAllUsersRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineGetAllUsersRoutes({ router }: RouteDefinitionParams) { router.get( { path: '/internal/security/users', validate: false }, createLicensedRouteHandler(async (context, request, response) => { @@ -16,7 +16,7 @@ export function defineGetAllUsersRoutes({ router, clusterClient }: RouteDefiniti return response.ok({ // Return only values since keys (user names) are already duplicated there. body: Object.values( - await clusterClient.asScoped(request).callAsCurrentUser('shield.getUser') + (await context.core.elasticsearch.client.asCurrentUser.security.getUser()).body ), }); } catch (error) { diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.test.ts b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts index b513230b3ba6f..dfe5faa95ae15 100644 --- a/x-pack/plugins/security/server/routes/views/access_agreement.test.ts +++ b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts @@ -14,7 +14,7 @@ import { RequestHandlerContext, } from '../../../../../../src/core/server'; import { SecurityLicense, SecurityLicenseFeatures } from '../../../common/licensing'; -import { AuthenticationProvider } from '../../../common/types'; +import type { AuthenticationProvider } from '../../../common/model'; import { ConfigType } from '../../config'; import { Session } from '../../session_management'; import { defineAccessAgreementRoutes } from './access_agreement'; diff --git a/x-pack/plugins/security/server/routes/views/login.ts b/x-pack/plugins/security/server/routes/views/login.ts index 93d43d04a86ca..68becb48f596e 100644 --- a/x-pack/plugins/security/server/routes/views/login.ts +++ b/x-pack/plugins/security/server/routes/views/login.ts @@ -7,6 +7,11 @@ import { schema } from '@kbn/config-schema'; import { parseNext } from '../../../common/parse_next'; import { LoginState } from '../../../common/login_state'; +import { shouldProviderUseLoginForm } from '../../../common/model'; +import { + LOGOUT_REASON_QUERY_STRING_PARAMETER, + NEXT_URL_QUERY_STRING_PARAMETER, +} from '../../../common/constants'; import { RouteDefinitionParams } from '..'; /** @@ -26,8 +31,8 @@ export function defineLoginRoutes({ validate: { query: schema.object( { - next: schema.maybe(schema.string()), - msg: schema.maybe(schema.string()), + [NEXT_URL_QUERY_STRING_PARAMETER]: schema.maybe(schema.string()), + [LOGOUT_REASON_QUERY_STRING_PARAMETER]: schema.maybe(schema.string()), }, { unknowns: 'allow' } ), @@ -59,7 +64,7 @@ export function defineLoginRoutes({ // Since `config.authc.sortedProviders` is based on `config.authc.providers` config we can // be sure that config is present for every provider in `config.authc.sortedProviders`. const { showInSelector, description, hint, icon } = config.authc.providers[type]?.[name]!; - const usesLoginForm = type === 'basic' || type === 'token'; + const usesLoginForm = shouldProviderUseLoginForm(type); return { type, name, diff --git a/x-pack/plugins/security/server/session_management/session.ts b/x-pack/plugins/security/server/session_management/session.ts index 757b1aaeddcbc..4dc83a1abe4af 100644 --- a/x-pack/plugins/security/server/session_management/session.ts +++ b/x-pack/plugins/security/server/session_management/session.ts @@ -9,7 +9,7 @@ import { randomBytes, createHash } from 'crypto'; import nodeCrypto, { Crypto } from '@elastic/node-crypto'; import type { PublicMethodsOf } from '@kbn/utility-types'; import type { KibanaRequest, Logger } from '../../../../../src/core/server'; -import type { AuthenticationProvider } from '../../common/types'; +import type { AuthenticationProvider } from '../../common/model'; import type { ConfigType } from '../config'; import type { SessionIndex, SessionIndexValue } from './session_index'; import type { SessionCookie } from './session_cookie'; diff --git a/x-pack/plugins/security/server/session_management/session_index.ts b/x-pack/plugins/security/server/session_management/session_index.ts index 96fff41d57503..45b2f4489c195 100644 --- a/x-pack/plugins/security/server/session_management/session_index.ts +++ b/x-pack/plugins/security/server/session_management/session_index.ts @@ -5,7 +5,7 @@ */ import type { ILegacyClusterClient, Logger } from '../../../../../src/core/server'; -import type { AuthenticationProvider } from '../../common/types'; +import type { AuthenticationProvider } from '../../common/model'; import type { ConfigType } from '../config'; export interface SessionIndexOptions { diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index e58aed15a8a10..c47ec70341845 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -160,6 +160,7 @@ export const ML_GROUP_IDS = [ML_GROUP_ID, LEGACY_ML_GROUP_ID]; /* Rule notifications options */ +export const ENABLE_CASE_CONNECTOR = false; export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.email', '.slack', @@ -169,6 +170,11 @@ export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.jira', '.resilient', ]; + +if (ENABLE_CASE_CONNECTOR) { + NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.push('.case'); +} + export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions'; export const NOTIFICATION_THROTTLE_RULE = 'rule'; 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 82b803c62a940..ff76a0fcb5593 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 @@ -320,7 +320,12 @@ export type SeverityMappingOrUndefined = t.TypeOf; -export const job_status = t.keyof({ succeeded: null, failed: null, 'going to run': null }); +export const job_status = t.keyof({ + succeeded: null, + failed: null, + 'going to run': null, + 'partial failure': null, +}); export type JobStatus = t.TypeOf; export const conflicts = t.keyof({ abort: null, proceed: null }); diff --git a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts index ec3887ad72625..b32402851ac7c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts @@ -39,7 +39,12 @@ import { TIMELINE_DESCRIPTION, TIMELINE_QUERY, TIMELINE_TITLE } from '../screens import { goToCaseDetails, goToCreateNewCase } from '../tasks/all_cases'; import { openCaseTimeline } from '../tasks/case_details'; -import { backToCases, createNewCaseWithTimeline } from '../tasks/create_new_case'; +import { + attachTimeline, + backToCases, + createCase, + fillCasesMandatoryfields, +} from '../tasks/create_new_case'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; @@ -57,7 +62,9 @@ describe('Cases', () => { it('Creates a new case with timeline and opens the timeline', () => { loginAndWaitForPageWithoutDateRange(CASES_URL); goToCreateNewCase(); - createNewCaseWithTimeline(case1); + fillCasesMandatoryfields(case1); + attachTimeline(case1); + createCase(); backToCases(); cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases'); diff --git a/x-pack/plugins/security_solution/cypress/integration/cases_connector_options.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases_connector_options.spec.ts new file mode 100644 index 0000000000000..f227042a0f9dc --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/cases_connector_options.spec.ts @@ -0,0 +1,72 @@ +/* + * 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 { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { + case1, + connectorIds, + mockConnectorsResponse, + executeResponses, + ibmResilientConnectorOptions, + jiraConnectorOptions, + serviceNowConnectorOpions, +} from '../objects/case'; +import { + createCase, + fillCasesMandatoryfields, + fillIbmResilientConnectorOptions, + fillJiraConnectorOptions, + fillServiceNowConnectorOptions, +} from '../tasks/create_new_case'; +import { goToCreateNewCase } from '../tasks/all_cases'; +import { deleteCase } from '../tasks/case_details'; +import { CASES_URL } from '../urls/navigation'; +import { CONNECTOR_CARD_DETAILS, CONNECTOR_TITLE } from '../screens/case_details'; + +describe('Cases connector incident fields', () => { + before(() => { + cy.server(); + cy.route('GET', '**/api/cases/configure/connectors/_find', mockConnectorsResponse); + cy.route2('POST', `**/api/actions/action/${connectorIds.jira}/_execute`, (req) => { + const response = + JSON.parse(req.body).params.subAction === 'issueTypes' + ? executeResponses.jira.issueTypes + : executeResponses.jira.fieldsByIssueType; + req.reply(JSON.stringify(response)); + }); + cy.route2('POST', `**/api/actions/action/${connectorIds.resilient}/_execute`, (req) => { + const response = + JSON.parse(req.body).params.subAction === 'incidentTypes' + ? executeResponses.resilient.incidentTypes + : executeResponses.resilient.severity; + req.reply(JSON.stringify(response)); + }); + }); + + after(() => { + deleteCase(); + }); + + it('Correct incident fields show when connector is changed', () => { + loginAndWaitForPageWithoutDateRange(CASES_URL); + goToCreateNewCase(); + fillCasesMandatoryfields(case1); + fillJiraConnectorOptions(jiraConnectorOptions); + fillServiceNowConnectorOptions(serviceNowConnectorOpions); + fillIbmResilientConnectorOptions(ibmResilientConnectorOptions); + createCase(); + + cy.get(CONNECTOR_TITLE).should('have.text', ibmResilientConnectorOptions.title); + cy.get(CONNECTOR_CARD_DETAILS).should( + 'have.text', + `${ + ibmResilientConnectorOptions.title + }Incident Types: ${ibmResilientConnectorOptions.incidentTypes.join(', ')}Severity: ${ + ibmResilientConnectorOptions.severity + }` + ); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/objects/case.ts b/x-pack/plugins/security_solution/cypress/objects/case.ts index 084df31a604a3..01e9a9124ca88 100644 --- a/x-pack/plugins/security_solution/cypress/objects/case.ts +++ b/x-pack/plugins/security_solution/cypress/objects/case.ts @@ -21,6 +21,23 @@ export interface Connector { password: string; } +export interface JiraConnectorOptions { + issueType: string; + priority: string; +} + +export interface ServiceNowconnectorOptions { + urgency: string; + severity: string; + impact: string; +} + +export interface IbmResilientConnectorOptions { + title: string; + severity: string; + incidentTypes: string[]; +} + export const caseTimeline: TimelineWithId = { title: 'SIEM test', description: 'description', @@ -43,4 +60,207 @@ export const serviceNowConnector: Connector = { password: 'password', }; +export const jiraConnectorOptions: JiraConnectorOptions = { + issueType: '10006', + priority: 'High', +}; + +export const serviceNowConnectorOpions: ServiceNowconnectorOptions = { + urgency: '2', + severity: '1', + impact: '3', +}; + +export const ibmResilientConnectorOptions: IbmResilientConnectorOptions = { + title: 'Resilient', + severity: 'Medium', + incidentTypes: ['Communication error (fax; email)', 'Denial of Service'], +}; + export const TIMELINE_CASE_ID = '68248e00-f689-11ea-9ab2-59238b522856'; +export const connectorIds = { + jira: '000e5f86-08b0-4882-adfd-6df981d45c1b', + sn: '93a69ba3-3c31-4b4c-bf86-cc79a090f437', + resilient: 'a6a8dd7f-7e88-48fe-9b9f-70b668da8cbc', +}; + +export const mockConnectorsResponse = [ + { + id: connectorIds.jira, + actionTypeId: '.jira', + name: 'Jira', + config: { + incidentConfiguration: { + mapping: [ + { source: 'title', target: 'summary', actionType: 'overwrite' }, + { source: 'description', target: 'description', actionType: 'overwrite' }, + { source: 'comments', target: 'comments', actionType: 'append' }, + ], + }, + isCaseOwned: true, + apiUrl: 'https://siem-kibana.atlassian.net', + projectKey: 'RJ', + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: connectorIds.resilient, + actionTypeId: '.resilient', + name: 'Resilient', + config: { + incidentConfiguration: { + mapping: [ + { source: 'title', target: 'name', actionType: 'overwrite' }, + { source: 'description', target: 'description', actionType: 'overwrite' }, + { source: 'comments', target: 'comments', actionType: 'append' }, + ], + }, + isCaseOwned: true, + apiUrl: 'https://ibm-resilient.siem.estc.dev', + orgId: '201', + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: connectorIds.sn, + actionTypeId: '.servicenow', + name: 'ServiceNow', + config: { + incidentConfiguration: { + mapping: [ + { source: 'title', target: 'short_description', actionType: 'overwrite' }, + { source: 'description', target: 'description', actionType: 'overwrite' }, + { source: 'comments', target: 'comments', actionType: 'append' }, + ], + }, + isCaseOwned: true, + apiUrl: 'https://dev65287.service-now.com', + }, + isPreconfigured: false, + referencedByCount: 0, + }, +]; +export const executeResponses = { + jira: { + issueTypes: { + status: 'ok', + data: [ + { id: '10006', name: 'Task' }, + { id: '10007', name: 'Sub-task' }, + ], + actionId: connectorIds.jira, + }, + fieldsByIssueType: { + status: 'ok', + data: { + summary: { allowedValues: [], defaultValue: {} }, + issuetype: { + allowedValues: [ + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/issuetype/10006', + id: '10006', + description: 'A small, distinct piece of work.', + iconUrl: + 'https://siem-kibana.atlassian.net/secure/viewavatar?size=medium&avatarId=10318&avatarType=issuetype', + name: 'Task', + subtask: false, + avatarId: 10318, + }, + ], + defaultValue: {}, + }, + attachment: { allowedValues: [], defaultValue: {} }, + duedate: { allowedValues: [], defaultValue: {} }, + description: { allowedValues: [], defaultValue: {} }, + project: { + allowedValues: [ + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/project/10011', + id: '10011', + key: 'RJ', + name: 'Refactor Jira', + projectTypeKey: 'business', + simplified: false, + avatarUrls: { + '48x48': + 'https://siem-kibana.atlassian.net/secure/projectavatar?pid=10011&avatarId=10423', + '24x24': + 'https://siem-kibana.atlassian.net/secure/projectavatar?size=small&s=small&pid=10011&avatarId=10423', + '16x16': + 'https://siem-kibana.atlassian.net/secure/projectavatar?size=xsmall&s=xsmall&pid=10011&avatarId=10423', + '32x32': + 'https://siem-kibana.atlassian.net/secure/projectavatar?size=medium&s=medium&pid=10011&avatarId=10423', + }, + }, + ], + defaultValue: {}, + }, + assignee: { allowedValues: [], defaultValue: {} }, + priority: { + allowedValues: [ + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/1', + iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/highest.svg', + name: 'Highest', + id: '1', + }, + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/2', + iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/high.svg', + name: 'High', + id: '2', + }, + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/3', + iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/medium.svg', + name: 'Medium', + id: '3', + }, + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/4', + iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/low.svg', + name: 'Low', + id: '4', + }, + { + self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/5', + iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/lowest.svg', + name: 'Lowest', + id: '5', + }, + ], + defaultValue: { + self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/3', + iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/medium.svg', + name: 'Medium', + id: '3', + }, + }, + timetracking: { allowedValues: [], defaultValue: {} }, + labels: { allowedValues: [], defaultValue: {} }, + }, + actionId: connectorIds.jira, + }, + }, + resilient: { + incidentTypes: { + status: 'ok', + data: [ + { id: 17, name: 'Communication error (fax; email)' }, + { id: 21, name: 'Denial of Service' }, + ], + actionId: connectorIds.resilient, + }, + severity: { + status: 'ok', + data: [ + { id: 4, name: 'Low' }, + { id: 5, name: 'Medium' }, + { id: 6, name: 'High' }, + ], + actionId: connectorIds.resilient, + }, + }, +}; diff --git a/x-pack/plugins/security_solution/cypress/screens/case_details.ts b/x-pack/plugins/security_solution/cypress/screens/case_details.ts index 7b995f5395543..02ec74aaed29c 100644 --- a/x-pack/plugins/security_solution/cypress/screens/case_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/case_details.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +export const CASE_ACTIONS_BTN = '[data-test-subj="property-actions-ellipses"]'; + export const CASE_DETAILS_DESCRIPTION = '[data-test-subj="description-action"] [data-test-subj="user-action-markdown"]'; @@ -27,6 +29,14 @@ export const CASE_DETAILS_USER_ACTION_DESCRIPTION_USERNAME = export const CASE_DETAILS_USERNAMES = '[data-test-subj="case-view-username"]'; +export const CONNECTOR_CARD_DETAILS = '[data-test-subj="settings-connector-card"]'; + +export const CONNECTOR_TITLE = '[data-test-subj="settings-connector-card"] span.euiTitle'; + +export const DELETE_CASE_BTN = '[data-test-subj="property-actions-trash"]'; + +export const DELETE_CASE_CONFIRMATION_BTN = '[data-test-subj="confirmModalConfirmButton"]'; + export const PARTICIPANTS = 1; export const REPORTER = 0; diff --git a/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts b/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts new file mode 100644 index 0000000000000..c0ae4f94c541b --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/edit_connector.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. + */ + +import { connectorIds } from '../objects/case'; + +export const CONNECTOR_RESILIENT = `[data-test-subj="connector-settings-resilient"]`; + +export const CONNECTOR_SELECTOR = '[data-test-subj="dropdown-connectors"]'; + +export const SELECT_IMPACT = `[data-test-subj="impactSelect"]`; + +export const SELECT_INCIDENT_TYPE = `[data-test-subj="incidentTypeComboBox"] input[data-test-subj="comboBoxSearchInput"]`; + +export const SELECT_ISSUE_TYPE = `[data-test-subj="issueTypeSelect"]`; + +export const SELECT_JIRA = `[data-test-subj="dropdown-connector-${connectorIds.jira}"]`; + +export const SELECT_PRIORITY = `[data-test-subj="prioritySelect"]`; + +export const SELECT_RESILIENT = `[data-test-subj="dropdown-connector-${connectorIds.resilient}"]`; + +export const SELECT_SEVERITY = `[data-test-subj="severitySelect"]`; + +export const SELECT_SN = `[data-test-subj="dropdown-connector-${connectorIds.sn}"]`; + +export const SELECT_URGENCY = `[data-test-subj="urgencySelect"]`; diff --git a/x-pack/plugins/security_solution/cypress/tasks/case_details.ts b/x-pack/plugins/security_solution/cypress/tasks/case_details.ts index 976d568ab3a91..51850997c3685 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/case_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/case_details.ts @@ -5,7 +5,18 @@ */ import { TIMELINE_TITLE } from '../screens/timeline'; -import { CASE_DETAILS_TIMELINE_LINK_MARKDOWN } from '../screens/case_details'; +import { + CASE_ACTIONS_BTN, + CASE_DETAILS_TIMELINE_LINK_MARKDOWN, + DELETE_CASE_BTN, + DELETE_CASE_CONFIRMATION_BTN, +} from '../screens/case_details'; + +export const deleteCase = () => { + cy.get(CASE_ACTIONS_BTN).first().click(); + cy.get(DELETE_CASE_BTN).click(); + cy.get(DELETE_CASE_CONFIRMATION_BTN).click(); +}; export const openCaseTimeline = () => { cy.get(CASE_DETAILS_TIMELINE_LINK_MARKDOWN).click(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts index f5013eed07d29..39654fd115a4a 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TestCase } from '../objects/case'; +import { + IbmResilientConnectorOptions, + JiraConnectorOptions, + ServiceNowconnectorOptions, + TestCase, +} from '../objects/case'; import { BACK_TO_CASES_BTN, @@ -16,34 +21,76 @@ import { TIMELINE_SEARCHBOX, TITLE_INPUT, } from '../screens/create_new_case'; +import { + CONNECTOR_RESILIENT, + CONNECTOR_SELECTOR, + SELECT_IMPACT, + SELECT_INCIDENT_TYPE, + SELECT_ISSUE_TYPE, + SELECT_JIRA, + SELECT_PRIORITY, + SELECT_RESILIENT, + SELECT_SEVERITY, + SELECT_SN, + SELECT_URGENCY, +} from '../screens/edit_connector'; export const backToCases = () => { cy.get(BACK_TO_CASES_BTN).click({ force: true }); }; -export const createNewCase = (newCase: TestCase) => { +export const fillCasesMandatoryfields = (newCase: TestCase) => { cy.get(TITLE_INPUT).type(newCase.name, { force: true }); newCase.tags.forEach((tag) => { cy.get(TAGS_INPUT).type(`${tag}{enter}`, { force: true }); }); cy.get(DESCRIPTION_INPUT).type(`${newCase.description} `, { force: true }); - - cy.get(SUBMIT_BTN).click({ force: true }); - cy.get(LOADING_SPINNER).should('exist'); - cy.get(LOADING_SPINNER).should('not.exist'); }; -export const createNewCaseWithTimeline = (newCase: TestCase) => { - cy.get(TITLE_INPUT).type(newCase.name, { force: true }); - newCase.tags.forEach((tag) => { - cy.get(TAGS_INPUT).type(`${tag}{enter}`, { force: true }); - }); - cy.get(DESCRIPTION_INPUT).type(`${newCase.description} `, { force: true }); - +export const attachTimeline = (newCase: TestCase) => { cy.get(INSERT_TIMELINE_BTN).click({ force: true }); cy.get(TIMELINE_SEARCHBOX).type(`${newCase.timeline.title}{enter}`); +}; +export const createCase = () => { cy.get(SUBMIT_BTN).click({ force: true }); cy.get(LOADING_SPINNER).should('exist'); cy.get(LOADING_SPINNER).should('not.exist'); }; + +export const fillJiraConnectorOptions = (jiraConnector: JiraConnectorOptions) => { + cy.get(CONNECTOR_SELECTOR).click({ force: true }); + cy.get(SELECT_JIRA).click({ force: true }); + cy.get(SELECT_ISSUE_TYPE).should('exist'); + + cy.get(SELECT_PRIORITY).should('exist'); + cy.get(SELECT_ISSUE_TYPE).select(jiraConnector.issueType); + cy.get(SELECT_PRIORITY).select(jiraConnector.priority); +}; + +export const fillServiceNowConnectorOptions = ( + serviceNowConnectorOpions: ServiceNowconnectorOptions +) => { + cy.get(CONNECTOR_SELECTOR).click({ force: true }); + cy.get(SELECT_SN).click({ force: true }); + cy.get(SELECT_SEVERITY).should('exist'); + cy.get(SELECT_URGENCY).should('exist'); + cy.get(SELECT_IMPACT).should('exist'); + cy.get(SELECT_URGENCY).select(serviceNowConnectorOpions.urgency); + cy.get(SELECT_SEVERITY).select(serviceNowConnectorOpions.severity); + cy.get(SELECT_IMPACT).select(serviceNowConnectorOpions.impact); +}; + +export const fillIbmResilientConnectorOptions = ( + ibmResilientConnector: IbmResilientConnectorOptions +) => { + cy.get(CONNECTOR_SELECTOR).click({ force: true }); + cy.get(SELECT_RESILIENT).click({ force: true }); + cy.get(SELECT_INCIDENT_TYPE).should('exist'); + cy.get(SELECT_SEVERITY).should('exist'); + ibmResilientConnector.incidentTypes.forEach((incidentType) => { + cy.get(SELECT_INCIDENT_TYPE).type(`${incidentType}{enter}`, { force: true }); + }); + cy.get(CONNECTOR_RESILIENT).click(); + cy.get(SELECT_SEVERITY).select(ibmResilientConnector.severity); +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx index 2c8051f902b17..50e139bcd215f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx @@ -6,42 +6,24 @@ import React from 'react'; import { mount } from 'enzyme'; +import { waitFor, act } from '@testing-library/react'; import { AddComment, AddCommentRefObject } from '.'; import { TestProviders } from '../../../common/mock'; -import { getFormMock } from '../__mock__/form'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; import { CommentRequest, CommentType } from '../../../../../case/common/api'; -import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; +import { useInsertTimeline } from '../use_insert_timeline'; import { usePostComment } from '../../containers/use_post_comment'; -import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; -import { useFormData } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'; -import { waitFor } from '@testing-library/react'; - -jest.mock( - '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); - -jest.mock( - '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data' -); - -jest.mock('../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'); jest.mock('../../containers/use_post_comment'); +jest.mock('../use_insert_timeline'); -const useFormMock = useForm as jest.Mock; -const useFormDataMock = useFormData as jest.Mock; - -const useInsertTimelineMock = useInsertTimeline as jest.Mock; const usePostCommentMock = usePostComment as jest.Mock; - +const useInsertTimelineMock = useInsertTimeline as jest.Mock; const onCommentSaving = jest.fn(); const onCommentPosted = jest.fn(); const postComment = jest.fn(); -const handleCursorChange = jest.fn(); -const handleOnTimelineChange = jest.fn(); const addCommentProps = { caseId: '1234', @@ -52,15 +34,6 @@ const addCommentProps = { showLoading: false, }; -const defaultInsertTimeline = { - cursorPosition: { - start: 0, - end: 0, - }, - handleCursorChange, - handleOnTimelineChange, -}; - const defaultPostCommment = { isLoading: false, isError: false, @@ -73,14 +46,9 @@ const sampleData: CommentRequest = { }; describe('AddComment ', () => { - const formHookMock = getFormMock(sampleData); - beforeEach(() => { jest.resetAllMocks(); - useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); usePostCommentMock.mockImplementation(() => defaultPostCommment); - useFormMock.mockImplementation(() => ({ form: formHookMock })); - useFormDataMock.mockImplementation(() => [{ comment: sampleData.comment }]); jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); }); @@ -92,14 +60,25 @@ describe('AddComment ', () => { ); + + await act(async () => { + wrapper + .find(`[data-test-subj="add-comment"] textarea`) + .first() + .simulate('change', { target: { value: sampleData.comment } }); + }); + expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeTruthy(); expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeFalsy(); - wrapper.find(`[data-test-subj="submit-comment"]`).first().simulate('click'); + await act(async () => { + wrapper.find(`[data-test-subj="submit-comment"]`).first().simulate('click'); + }); + await waitFor(() => { expect(onCommentSaving).toBeCalled(); expect(postComment).toBeCalledWith(sampleData, onCommentPosted); - expect(formHookMock.reset).toBeCalled(); + expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(''); }); }); @@ -112,6 +91,7 @@ describe('AddComment ', () => { ); + expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeTruthy(); expect( wrapper.find(`[data-test-subj="submit-comment"]`).first().prop('isDisabled') @@ -127,15 +107,16 @@ describe('AddComment ', () => { ); + expect( wrapper.find(`[data-test-subj="submit-comment"]`).first().prop('isDisabled') ).toBeTruthy(); }); - it('should insert a quote', () => { + it('should insert a quote', async () => { const sampleQuote = 'what a cool quote'; const ref = React.createRef(); - mount( + const wrapper = mount( @@ -143,10 +124,37 @@ describe('AddComment ', () => { ); - ref.current!.addQuote(sampleQuote); - expect(formHookMock.setFieldValue).toBeCalledWith( - 'comment', + await act(async () => { + wrapper + .find(`[data-test-subj="add-comment"] textarea`) + .first() + .simulate('change', { target: { value: sampleData.comment } }); + }); + + await act(async () => { + ref.current!.addQuote(sampleQuote); + }); + + expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe( `${sampleData.comment}\n\n${sampleQuote}` ); }); + + it('it should insert a timeline', async () => { + useInsertTimelineMock.mockImplementation((comment, onTimelineAttached) => { + onTimelineAttached(`[title](url)`); + }); + + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe('[title](url)'); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index 859ba3d1a0951..daa7c24858b94 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -12,12 +12,11 @@ import { CommentType } from '../../../../../case/common/api'; import { usePostComment } from '../../containers/use_post_comment'; import { Case } from '../../containers/types'; import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form'; -import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; import { Form, useForm, UseField, useFormData } from '../../../shared_imports'; import * as i18n from './translations'; import { schema, AddCommentFormSchema } from './schema'; -import { useTimelineClick } from '../../../common/utils/timeline/use_timeline_click'; +import { useInsertTimeline } from '../use_insert_timeline'; const MySpinner = styled(EuiLoadingSpinner)` position: absolute; @@ -56,12 +55,6 @@ export const AddComment = React.memo( const { setFieldValue, reset, submit } = form; const [{ comment }] = useFormData<{ comment: string }>({ form, watch: [fieldName] }); - const onCommentChange = useCallback((newValue) => setFieldValue(fieldName, newValue), [ - setFieldValue, - ]); - - const { handleCursorChange } = useInsertTimeline(comment, onCommentChange); - const addQuote = useCallback( (quote) => { setFieldValue(fieldName, `${comment}${comment.length > 0 ? '\n\n' : ''}${quote}`); @@ -73,7 +66,12 @@ export const AddComment = React.memo( addQuote, })); - const handleTimelineClick = useTimelineClick(); + const onTimelineAttached = useCallback( + (newValue: string) => setFieldValue(fieldName, newValue), + [setFieldValue] + ); + + useInsertTimeline(comment ?? '', onTimelineAttached); const onSubmit = useCallback(async () => { const { isValid, data } = await submit(); @@ -98,8 +96,6 @@ export const AddComment = React.memo( isDisabled: isLoading, dataTestSubj: 'add-comment', placeholder: i18n.ADD_COMMENT_HELP_TEXT, - onCursorPositionUpdate: handleCursorChange, - onClickTimeline: handleTimelineClick, bottomRightContent: ( { + const formHookMock = getFormMock({ connectorId: connectorsMock[0].id }); + + beforeEach(() => { + jest.resetAllMocks(); + useFormMock.mockImplementation(() => ({ form: formHookMock })); + }); + + it('it should render', async () => { + const wrapper = mount( +
+ + + ); + + expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy(); + }); + + it('it should not render when is not in edit mode', async () => { + const wrapper = mount( +
+ + + ); + + expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx b/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx index 7de7b3d6b2a96..9017365eea02b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; +import { isEmpty } from 'lodash/fp'; import { EuiFormRow } from '@elastic/eui'; -import React, { useCallback, useEffect } from 'react'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports'; import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown'; @@ -14,9 +15,8 @@ import { ActionConnector } from '../../../../../case/common/api/cases'; interface ConnectorSelectorProps { connectors: ActionConnector[]; dataTestSubj: string; - defaultValue?: ActionConnector; disabled: boolean; - field: FieldHook; + field: FieldHook; idAria: string; isEdit: boolean; isLoading: boolean; @@ -24,7 +24,6 @@ interface ConnectorSelectorProps { export const ConnectorSelector = ({ connectors, dataTestSubj, - defaultValue, disabled = false, field, idAria, @@ -32,19 +31,6 @@ export const ConnectorSelector = ({ isLoading = false, }: ConnectorSelectorProps) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - - useEffect(() => { - field.setValue(defaultValue); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [defaultValue]); - - const handleContentChange = useCallback( - (newConnector: string) => { - field.setValue(newConnector); - }, - [field] - ); - return isEdit ? ( ) : null; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/cases_dropdown.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/case/cases_dropdown.tsx new file mode 100644 index 0000000000000..931e23e811b1b --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/cases_dropdown.tsx @@ -0,0 +1,72 @@ +/* + * 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 { EuiFormRow, EuiSuperSelect, EuiSuperSelectOption } from '@elastic/eui'; +import React, { memo, useMemo, useCallback } from 'react'; +import { Case } from '../../../containers/types'; + +import * as i18n from './translations'; + +interface CaseDropdownProps { + isLoading: boolean; + cases: Case[]; + selectedCase?: string; + onCaseChanged: (id: string) => void; +} + +export const ADD_CASE_BUTTON_ID = 'add-case'; + +const addNewCase = { + value: ADD_CASE_BUTTON_ID, + inputDisplay: ( + + {i18n.CASE_CONNECTOR_ADD_NEW_CASE} + + ), + 'data-test-subj': 'dropdown-connector-add-connector', +}; + +const CasesDropdownComponent: React.FC = ({ + isLoading, + cases, + selectedCase, + onCaseChanged, +}) => { + const caseOptions: Array> = useMemo( + () => + cases.reduce>>( + (acc, theCase) => [ + ...acc, + { + value: theCase.id, + inputDisplay: {theCase.title}, + 'data-test-subj': `case-connector-cases-dropdown-${theCase.id}`, + }, + ], + [] + ), + [cases] + ); + + const options = useMemo(() => [...caseOptions, addNewCase], [caseOptions]); + const onChange = useCallback((id: string) => onCaseChanged(id), [onCaseChanged]); + + return ( + + + + ); +}; + +export const CasesDropdown = memo(CasesDropdownComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx new file mode 100644 index 0000000000000..28e051a713bf4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx @@ -0,0 +1,54 @@ +/* + * 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, { memo, useMemo, useCallback } from 'react'; +import { useGetCases } from '../../../containers/use_get_cases'; +import { useCreateCaseModal } from '../../use_create_case_modal'; +import { CasesDropdown, ADD_CASE_BUTTON_ID } from './cases_dropdown'; + +interface ExistingCaseProps { + selectedCase: string | null; + onCaseChanged: (id: string) => void; +} + +const ExistingCaseComponent: React.FC = ({ onCaseChanged, selectedCase }) => { + const { data: cases, loading: isLoadingCases, refetchCases } = useGetCases(); + + const onCaseCreated = useCallback(() => refetchCases(), [refetchCases]); + + const { Modal: CreateCaseModal, openModal } = useCreateCaseModal({ onCaseCreated }); + + const onChange = useCallback( + (id: string) => { + if (id === ADD_CASE_BUTTON_ID) { + openModal(); + return; + } + + onCaseChanged(id); + }, + [onCaseChanged, openModal] + ); + + const isCasesLoading = useMemo( + () => isLoadingCases.includes('cases') || isLoadingCases.includes('caseUpdate'), + [isLoadingCases] + ); + + return ( + <> + + + + ); +}; + +export const ExistingCase = memo(ExistingCaseComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/case/fields.tsx new file mode 100644 index 0000000000000..91087138e52d5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/fields.tsx @@ -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. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import React, { useCallback, useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { EuiCallOut } from '@elastic/eui'; + +import { ActionParamsProps } from '../../../../../../triggers_actions_ui/public/types'; +import { CommentType } from '../../../../../../case/common/api'; + +import { CaseActionParams } from './types'; +import { ExistingCase } from './existing_case'; + +import * as i18n from './translations'; + +const Container = styled.div` + ${({ theme }) => ` + margin-top: ${theme.eui?.euiSize ?? '16px'}; + `} +`; + +const defaultAlertComment = { + type: CommentType.alert, + alertId: '{{context.rule.id}}', + index: '{{context.rule.output_index}}', +}; + +const CaseParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + errors, + messageVariables, + actionConnector, +}) => { + const { caseId = null, comment = defaultAlertComment } = actionParams.subActionParams ?? {}; + + const [selectedCase, setSelectedCase] = useState(null); + + const editSubActionProperty = useCallback( + (key: string, value: unknown) => { + const newProps = { ...actionParams.subActionParams, [key]: value }; + editAction('subActionParams', newProps, index); + }, + [actionParams.subActionParams, editAction, index] + ); + + const onCaseChanged = useCallback( + (id: string) => { + setSelectedCase(id); + editSubActionProperty('caseId', id); + }, + [editSubActionProperty] + ); + + useEffect(() => { + if (!actionParams.subAction) { + editAction('subAction', 'addComment', index); + } + + if (!actionParams.subActionParams?.caseId) { + editSubActionProperty('caseId', caseId); + } + + if (!actionParams.subActionParams?.comment) { + editSubActionProperty('comment', comment); + } + + if (caseId != null) { + setSelectedCase((prevCaseId) => (prevCaseId !== caseId ? caseId : prevCaseId)); + } + + // editAction creates an infinity loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + actionConnector, + index, + actionParams.subActionParams?.caseId, + actionParams.subActionParams?.comment, + caseId, + comment, + actionParams.subAction, + ]); + + return ( + <> + + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { CaseParamsFields as default }; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/case/index.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/case/index.ts similarity index 57% rename from x-pack/plugins/security_solution/public/common/lib/connectors/case/index.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/case/index.ts index 271b1bfd2e3de..0aacd61991771 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/case/index.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/index.ts @@ -3,11 +3,29 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { lazy } from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ActionTypeModel } from '../../../../../../triggers_actions_ui/public/types'; +import { CaseActionParams } from './types'; import * as i18n from './translations'; +interface ValidationResult { + errors: { + caseId: string[]; + }; +} + +const validateParams = (actionParams: CaseActionParams) => { + const validationResult: ValidationResult = { errors: { caseId: [] } }; + + if (actionParams.subActionParams && !actionParams.subActionParams.caseId) { + validationResult.errors.caseId.push(i18n.CASE_CONNECTOR_CASE_REQUIRED); + } + + return validationResult; +}; + export function getActionType(): ActionTypeModel { return { id: '.case', @@ -15,8 +33,8 @@ export function getActionType(): ActionTypeModel { selectMessage: i18n.CASE_CONNECTOR_DESC, actionTypeTitle: i18n.CASE_CONNECTOR_TITLE, validateConnector: () => ({ errors: {} }), - validateParams: () => ({ errors: {} }), + validateParams, actionConnectorFields: null, - actionParamsFields: null, + actionParamsFields: lazy(() => import('./fields')), }; } diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts new file mode 100644 index 0000000000000..8cfcf2b9a073b --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts @@ -0,0 +1,86 @@ +/* + * 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'; + +export * from '../../../translations'; + +export const CASE_CONNECTOR_DESC = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.selectMessageText', + { + defaultMessage: 'Create or update a case.', + } +); + +export const CASE_CONNECTOR_TITLE = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.actionTypeTitle', + { + defaultMessage: 'Cases', + } +); + +export const CASE_CONNECTOR_COMMENT_LABEL = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.commentLabel', + { + defaultMessage: 'Comment', + } +); + +export const CASE_CONNECTOR_COMMENT_REQUIRED = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.commentRequired', + { + defaultMessage: 'Comment is required.', + } +); + +export const CASE_CONNECTOR_CASES_DROPDOWN_ROW_LABEL = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel', + { + defaultMessage: 'Case', + } +); + +export const CASE_CONNECTOR_CASES_DROPDOWN_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.casesDropdownPlaceholder', + { + defaultMessage: 'Select case', + } +); + +export const CASE_CONNECTOR_CASES_OPTION_NEW_CASE = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.optionAddNewCase', + { + defaultMessage: 'Add to a new case', + } +); + +export const CASE_CONNECTOR_CASES_OPTION_EXISTING_CASE = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.optionAddToExistingCase', + { + defaultMessage: 'Add to existing case', + } +); + +export const CASE_CONNECTOR_CASE_REQUIRED = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.caseRequired', + { + defaultMessage: 'You must select a case.', + } +); + +export const CASE_CONNECTOR_CALL_OUT_INFO = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.callOutInfo', + { + defaultMessage: 'All alerts after rule creation will be appended to the selected case.', + } +); + +export const CASE_CONNECTOR_ADD_NEW_CASE = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.addNewCaseOption', + { + defaultMessage: 'Add new case', + } +); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/types.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/case/types.ts new file mode 100644 index 0000000000000..8173a814c2d89 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/types.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. + */ + +export interface CaseActionParams { + subAction: string; + subActionParams: { + caseId: string; + comment: { + alertId: string; + index: string; + type: 'alert'; + }; + }; +} diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/config.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/lib/connectors/config.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/config.ts diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts similarity index 79% rename from x-pack/plugins/security_solution/public/common/lib/connectors/index.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/index.ts index 58d7e89e080e7..e77aa9bdd84b1 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts @@ -5,3 +5,7 @@ */ export { getActionType as getCaseConnectorUI } from './case'; + +export * from './config'; +export * from './types'; +export * from './utils'; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/types.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/lib/connectors/types.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/types.ts diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/utils.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/utils.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/lib/connectors/utils.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/utils.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx new file mode 100644 index 0000000000000..89b9e3b30ede1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx @@ -0,0 +1,186 @@ +/* + * 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 { mount } from 'enzyme'; +import { act, waitFor } from '@testing-library/react'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { useForm, Form, FormHook } from '../../../shared_imports'; +import { connectorsMock } from '../../containers/mock'; +import { Connector } from './connector'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { useGetIncidentTypes } from '../settings/resilient/use_get_incident_types'; +import { useGetSeverity } from '../settings/resilient/use_get_severity'; + +jest.mock('../../../common/lib/kibana', () => { + return { + useKibana: () => ({ + services: { + notifications: {}, + http: {}, + }, + }), + }; +}); +jest.mock('../../containers/configure/use_connectors'); +jest.mock('../settings/resilient/use_get_incident_types'); +jest.mock('../settings/resilient/use_get_severity'); + +const useConnectorsMock = useConnectors as jest.Mock; +const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; +const useGetSeverityMock = useGetSeverity as jest.Mock; + +const useGetIncidentTypesResponse = { + isLoading: false, + incidentTypes: [ + { + id: 19, + name: 'Malware', + }, + { + id: 21, + name: 'Denial of Service', + }, + ], +}; + +const useGetSeverityResponse = { + isLoading: false, + severity: [ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, + ], +}; + +describe('Connector', () => { + let globalForm: FormHook; + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm<{ connectorId: string; fields: Record | null }>({ + defaultValue: { connectorId: connectorsMock[0].id, fields: null }, + }); + + globalForm = form; + + return
{children}
; + }; + + beforeEach(() => { + jest.resetAllMocks(); + useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock }); + useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); + useGetSeverityMock.mockReturnValue(useGetSeverityResponse); + }); + + it('it renders', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="connector-settings"]`).exists()).toBeTruthy(); + + waitFor(() => { + expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeTruthy(); + }); + }); + + it('it is loading when fetching connectors', async () => { + useConnectorsMock.mockReturnValue({ loading: true, connectors: connectorsMock }); + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('isLoading') + ).toEqual(true); + }); + + it('it is disabled when fetching connectors', async () => { + useConnectorsMock.mockReturnValue({ loading: true, connectors: connectorsMock }); + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('disabled')).toEqual( + true + ); + }); + + it('it is disabled and loading when passing loading as true', async () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('isLoading') + ).toEqual(true); + expect(wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('disabled')).toEqual( + true + ); + }); + + it(`it should change connector`, async () => { + const wrapper = mount( + + + + ); + + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click'); + wrapper.update(); + }); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()).toBeTruthy(); + }); + + act(() => { + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ value: '19', label: 'Denial of Service' }]); + }); + + act(() => { + wrapper + .find('select[data-test-subj="severitySelect"]') + .first() + .simulate('change', { + target: { value: '4' }, + }); + }); + + await waitFor(() => { + expect(globalForm.getFormData()).toEqual({ + connectorId: 'resilient-2', + fields: { incidentTypes: ['19'], severityCode: '4' }, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx new file mode 100644 index 0000000000000..b2a0f3c351552 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx @@ -0,0 +1,83 @@ +/* + * 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, { memo, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { ConnectorTypeFields } from '../../../../../case/common/api/connectors'; +import { UseField, useFormData, FieldHook } from '../../../shared_imports'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { ConnectorSelector } from '../connector_selector/form'; +import { SettingFieldsForm } from '../settings/fields_form'; +import { ActionConnector } from '../../containers/types'; +import { getConnectorById } from '../configure_cases/utils'; + +interface Props { + isLoading: boolean; +} + +interface SettingsFieldProps { + connectors: ActionConnector[]; + field: FieldHook; + isEdit: boolean; +} + +const SettingsField = ({ connectors, isEdit, field }: SettingsFieldProps) => { + const [{ connectorId }] = useFormData({ watch: ['connectorId'] }); + const { setValue } = field; + const connector = getConnectorById(connectorId, connectors) ?? null; + + useEffect(() => { + if (connectorId) { + setValue(null); + } + }, [setValue, connectorId]); + + return ( + + ); +}; + +const ConnectorComponent: React.FC = ({ isLoading }) => { + const { loading: isLoadingConnectors, connectors } = useConnectors(); + + return ( + + + + + + + + + ); +}; + +ConnectorComponent.displayName = 'ConnectorComponent'; + +export const Connector = memo(ConnectorComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/description.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/description.test.tsx new file mode 100644 index 0000000000000..201a61febc628 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/description.test.tsx @@ -0,0 +1,57 @@ +/* + * 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 { mount } from 'enzyme'; +import { act } from '@testing-library/react'; + +import { useForm, Form, FormHook } from '../../../shared_imports'; +import { Description } from './description'; + +describe('Description', () => { + let globalForm: FormHook; + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm<{ description: string }>({ + defaultValue: { description: 'My description' }, + }); + + globalForm = form; + + return
{children}
; + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('it renders', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="caseDescription"]`).exists()).toBeTruthy(); + }); + + it('it changes the description', async () => { + const wrapper = mount( + + + + ); + + await act(async () => { + wrapper + .find(`[data-test-subj="caseDescription"] textarea`) + .first() + .simulate('change', { target: { value: 'My new description' } }); + }); + + expect(globalForm.getFormData()).toEqual({ description: 'My new description' }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/description.tsx b/x-pack/plugins/security_solution/public/cases/components/create/description.tsx new file mode 100644 index 0000000000000..f130bd14644f8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/description.tsx @@ -0,0 +1,31 @@ +/* + * 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, { memo } from 'react'; +import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form'; +import { UseField } from '../../../shared_imports'; + +interface Props { + isLoading: boolean; +} + +export const fieldName = 'description'; + +const DescriptionComponent: React.FC = ({ isLoading }) => ( + +); + +DescriptionComponent.displayName = 'DescriptionComponent'; + +export const Description = memo(DescriptionComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form.test.tsx new file mode 100644 index 0000000000000..e64b2b3a05080 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/form.test.tsx @@ -0,0 +1,66 @@ +/* + * 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 { mount } from 'enzyme'; + +import { useForm, Form } from '../../../shared_imports'; +import { useGetTags } from '../../containers/use_get_tags'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { connectorsMock } from '../../containers/mock'; +import { schema, FormProps } from './schema'; +import { CreateCaseForm } from './form'; + +jest.mock('../../containers/use_get_tags'); +jest.mock('../../containers/configure/use_connectors'); +const useGetTagsMock = useGetTags as jest.Mock; +const useConnectorsMock = useConnectors as jest.Mock; + +const initialCaseValue: FormProps = { + description: '', + tags: [], + title: '', + connectorId: 'none', + fields: null, +}; + +describe('CreateCaseForm', () => { + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm({ + defaultValue: initialCaseValue, + options: { stripEmptyFields: false }, + schema, + }); + + return
{children}
; + }; + + beforeEach(() => { + jest.resetAllMocks(); + useGetTagsMock.mockReturnValue({ tags: ['test'] }); + useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock }); + }); + + it('it renders with steps', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="case-creation-form-steps"]`).exists()).toBeTruthy(); + }); + + it('it renders without steps', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="case-creation-form-steps"]`).exists()).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form.tsx new file mode 100644 index 0000000000000..40db4d792c1c8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/form.tsx @@ -0,0 +1,94 @@ +/* + * 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, { useMemo } from 'react'; +import { EuiLoadingSpinner, EuiSteps } from '@elastic/eui'; +import styled, { css } from 'styled-components'; + +import { useFormContext } from '../../../shared_imports'; + +import { Title } from './title'; +import { Description } from './description'; +import { Tags } from './tags'; +import { Connector } from './connector'; +import * as i18n from './translations'; + +interface ContainerProps { + big?: boolean; +} + +const Container = styled.div.attrs((props) => props)` + ${({ big, theme }) => css` + margin-top: ${big ? theme.eui?.euiSizeXL ?? '32px' : theme.eui?.euiSize ?? '16px'}; + `} +`; + +const MySpinner = styled(EuiLoadingSpinner)` + position: absolute; + top: 50%; + left: 50%; + z-index: 99; +`; + +interface Props { + withSteps?: boolean; +} + +export const CreateCaseForm: React.FC = React.memo(({ withSteps = true }) => { + const { isSubmitting } = useFormContext(); + + const firstStep = useMemo( + () => ({ + title: i18n.STEP_ONE_TITLE, + children: ( + <> + + <Container> + <Tags isLoading={isSubmitting} /> + </Container> + <Container big> + <Description isLoading={isSubmitting} /> + </Container> + </> + ), + }), + [isSubmitting] + ); + + const secondStep = useMemo( + () => ({ + title: i18n.STEP_TWO_TITLE, + children: ( + <Container> + <Connector isLoading={isSubmitting} /> + </Container> + ), + }), + [isSubmitting] + ); + + const allSteps = useMemo(() => [firstStep, secondStep], [firstStep, secondStep]); + + return ( + <> + {isSubmitting && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />} + {withSteps ? ( + <EuiSteps + headingElement="h2" + steps={allSteps} + data-test-subj={'case-creation-form-steps'} + /> + ) : ( + <> + {firstStep.children} + {secondStep.children} + </> + )} + </> + ); +}); + +CreateCaseForm.displayName = 'CreateCaseForm'; diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx new file mode 100644 index 0000000000000..e11e508b60ebf --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx @@ -0,0 +1,66 @@ +/* + * 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, { useCallback, useEffect } from 'react'; + +import { schema, FormProps } from './schema'; +import { Form, useForm } from '../../../shared_imports'; +import { + getConnectorById, + getNoneConnector, + normalizeActionConnector, +} from '../configure_cases/utils'; +import { usePostCase } from '../../containers/use_post_case'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { Case } from '../../containers/types'; + +const initialCaseValue: FormProps = { + description: '', + tags: [], + title: '', + connectorId: 'none', + fields: null, +}; + +interface Props { + onSuccess?: (theCase: Case) => void; +} + +export const FormContext: React.FC<Props> = ({ children, onSuccess }) => { + const { connectors } = useConnectors(); + const { caseData, postCase } = usePostCase(); + + const submitCase = useCallback( + async ({ connectorId: dataConnectorId, fields, ...dataWithoutConnectorId }, isValid) => { + if (isValid) { + const caseConnector = getConnectorById(dataConnectorId, connectors); + const connectorToUpdate = caseConnector + ? normalizeActionConnector(caseConnector, fields) + : getNoneConnector(); + + await postCase({ ...dataWithoutConnectorId, connector: connectorToUpdate }); + } + }, + [postCase, connectors] + ); + + const { form } = useForm<FormProps>({ + defaultValue: initialCaseValue, + options: { stripEmptyFields: false }, + schema, + onSubmit: submitCase, + }); + + useEffect(() => { + if (caseData && onSuccess) { + onSuccess(caseData); + } + }, [caseData, onSuccess]); + + return <Form form={form}>{children}</Form>; +}; + +FormContext.displayName = 'FormContext'; diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx index 7902c7065d9a3..29073e7774158 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx @@ -5,71 +5,40 @@ */ import React from 'react'; -import { mount } from 'enzyme'; -import { Create } from '.'; +import { mount, ReactWrapper } from 'enzyme'; +import { act, waitFor } from '@testing-library/react'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { TestProviders } from '../../../common/mock'; -import { getFormMock } from '../__mock__/form'; -import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; -import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; import { usePostCase } from '../../containers/use_post_case'; import { useGetTags } from '../../containers/use_get_tags'; - -import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; -import { useFormData } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'; - -import { waitFor } from '@testing-library/react'; import { useConnectors } from '../../containers/configure/use_connectors'; import { connectorsMock } from '../../containers/configure/mock'; import { ConnectorTypes } from '../../../../../case/common/api/connectors'; +import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; +import { useGetIncidentTypes } from '../settings/resilient/use_get_incident_types'; +import { useGetSeverity } from '../settings/resilient/use_get_severity'; +import { useGetIssueTypes } from '../settings/jira/use_get_issue_types'; +import { useGetFieldsByIssueType } from '../settings/jira/use_get_fields_by_issue_type'; +import { Create } from '.'; -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); - return { - ...original, - // eslint-disable-next-line react/display-name - EuiFieldText: () => <input />, - }; -}); -jest.mock('../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'); jest.mock('../../containers/use_post_case'); - -jest.mock( - '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); - -jest.mock( - '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data' -); - jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/configure/use_connectors'); -jest.mock( - '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider', - () => ({ - FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) => - children({ tags: ['rad', 'dude'] }), - }) -); -const useConnectorsMock = useConnectors as jest.Mock; -const useFormMock = useForm as jest.Mock; -const useFormDataMock = useFormData as jest.Mock; +jest.mock('../settings/resilient/use_get_incident_types'); +jest.mock('../settings/resilient/use_get_severity'); +jest.mock('../settings/jira/use_get_issue_types'); +jest.mock('../settings/jira/use_get_fields_by_issue_type'); +jest.mock('../settings/jira/use_get_single_issue'); +jest.mock('../settings/jira/use_get_issues'); -const useInsertTimelineMock = useInsertTimeline as jest.Mock; +const useConnectorsMock = useConnectors as jest.Mock; const usePostCaseMock = usePostCase as jest.Mock; - +const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; +const useGetSeverityMock = useGetSeverity as jest.Mock; +const useGetIssueTypesMock = useGetIssueTypes as jest.Mock; +const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock; const postCase = jest.fn(); -const handleCursorChange = jest.fn(); -const handleOnTimelineChange = jest.fn(); - -const defaultInsertTimeline = { - cursorPosition: { - start: 0, - end: 0, - }, - handleCursorChange, - handleOnTimelineChange, -}; const sampleTags = ['coke', 'pepsi']; const sampleData = { @@ -83,27 +52,117 @@ const sampleData = { type: ConnectorTypes.none, }, }; + const defaultPostCase = { isLoading: false, isError: false, caseData: null, postCase, }; + const sampleConnectorData = { loading: false, connectors: [] }; + +const useGetIncidentTypesResponse = { + isLoading: false, + incidentTypes: [ + { + id: 19, + name: 'Malware', + }, + { + id: 21, + name: 'Denial of Service', + }, + ], +}; + +const useGetSeverityResponse = { + isLoading: false, + severity: [ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, + ], +}; + +const useGetIssueTypesResponse = { + isLoading: false, + issueTypes: [ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ], +}; + +const useGetFieldsByIssueTypeResponse = { + isLoading: false, + fields: { + summary: { allowedValues: [], defaultValue: {} }, + labels: { allowedValues: [], defaultValue: {} }, + description: { allowedValues: [], defaultValue: {} }, + priority: { + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + { + name: 'Low', + id: '2', + }, + ], + defaultValue: { name: 'Medium', id: '3' }, + }, + }, +}; + +const fillForm = async (wrapper: ReactWrapper) => { + await act(async () => { + wrapper + .find(`[data-test-subj="caseTitle"] input`) + .first() + .simulate('change', { target: { value: sampleData.title } }); + }); + + await act(async () => { + wrapper + .find(`[data-test-subj="caseDescription"] textarea`) + .first() + .simulate('change', { target: { value: sampleData.description } }); + }); + + await waitFor(() => { + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange(sampleTags.map((tag) => ({ label: tag }))); + }); +}; + describe('Create case', () => { const fetchTags = jest.fn(); - const formHookMock = getFormMock(sampleData); beforeEach(() => { jest.resetAllMocks(); - useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); usePostCaseMock.mockImplementation(() => defaultPostCase); - useFormMock.mockImplementation(() => ({ form: formHookMock })); - useFormDataMock.mockImplementation(() => [ - { - description: sampleData.description, - }, - ]); useConnectorsMock.mockReturnValue(sampleConnectorData); + useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); + useGetSeverityMock.mockReturnValue(useGetSeverityResponse); + useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); + useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); (useGetTags as jest.Mock).mockImplementation(() => ({ tags: sampleTags, @@ -112,7 +171,32 @@ describe('Create case', () => { }); describe('Step 1 - Case Fields', () => { + it('it renders', async () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + + expect(wrapper.find(`[data-test-subj="caseTitle"]`).first().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseDescription"]`).first().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseTags"]`).first().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseConnectors"]`).first().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="create-case-submit"]`).first().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="create-case-cancel"]`).first().exists()).toBeTruthy(); + expect( + wrapper.find(`[data-test-subj="case-creation-form-steps"]`).first().exists() + ).toBeTruthy(); + }); + it('should post case on submit click', async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + const wrapper = mount( <TestProviders> <Router history={mockHistory}> @@ -120,7 +204,13 @@ describe('Create case', () => { </Router> </TestProviders> ); - wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + + await fillForm(wrapper); + wrapper.update(); + + await act(async () => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + }); await waitFor(() => expect(postCase).toBeCalledWith(sampleData)); }); @@ -132,15 +222,18 @@ describe('Create case', () => { </Router> </TestProviders> ); + wrapper.find(`[data-test-subj="create-case-cancel"]`).first().simulate('click'); await waitFor(() => expect(mockHistory.push).toHaveBeenCalledWith('/')); }); + it('should redirect to new case when caseData is there', async () => { - const sampleId = '777777'; + const sampleId = 'case-id'; usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, caseData: { id: sampleId }, })); + mount( <TestProviders> <Router history={mockHistory}> @@ -148,11 +241,11 @@ describe('Create case', () => { </Router> </TestProviders> ); - await waitFor(() => expect(mockHistory.push).toHaveBeenNthCalledWith(1, '/777777')); + + await waitFor(() => expect(mockHistory.push).toHaveBeenNthCalledWith(1, '/case-id')); }); it('should render spinner when loading', async () => { - usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, isLoading: true })); const wrapper = mount( <TestProviders> <Router history={mockHistory}> @@ -160,11 +253,87 @@ describe('Create case', () => { </Router> </TestProviders> ); + + await fillForm(wrapper); + await act(async () => { + await wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + wrapper.update(); + expect( + wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists() + ).toBeTruthy(); + }); + }); + }); + + describe('Step 2 - Connector Fields', () => { + it(`it should submit a Jira connector`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + + await fillForm(wrapper); + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="connector-settings-jira"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click'); + wrapper.update(); + }); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-settings-jira"]`).exists()).toBeTruthy(); + }); + + act(() => { + wrapper + .find('select[data-test-subj="issueTypeSelect"]') + .first() + .simulate('change', { + target: { value: '10007' }, + }); + }); + + act(() => { + wrapper + .find('select[data-test-subj="prioritySelect"]') + .first() + .simulate('change', { + target: { value: '2' }, + }); + }); + + await act(async () => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + }); + await waitFor(() => - expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy() + expect(postCase).toBeCalledWith({ + ...sampleData, + connector: { + id: 'jira-1', + name: 'Jira', + type: '.jira', + fields: { issueType: '10007', parent: null, priority: '2' }, + }, + }) ); }); - it('Tag options render with new tags added', async () => { + + it(`it should submit a resilient connector`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + const wrapper = mount( <TestProviders> <Router history={mockHistory}> @@ -172,63 +341,109 @@ describe('Create case', () => { </Router> </TestProviders> ); - await waitFor(() => + + await fillForm(wrapper); + await waitFor(() => { expect( - wrapper - .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`) - .first() - .prop('options') - ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]) + wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists() + ).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click'); + wrapper.update(); + }); + + await waitFor(() => { + wrapper.update(); + expect( + wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists() + ).toBeTruthy(); + }); + + act(() => { + ((wrapper.find(EuiComboBox).at(1).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ value: '19', label: 'Denial of Service' }]); + }); + + act(() => { + wrapper + .find('select[data-test-subj="severitySelect"]') + .first() + .simulate('change', { + target: { value: '4' }, + }); + }); + + await act(async () => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + }); + + await waitFor(() => + expect(postCase).toBeCalledWith({ + ...sampleData, + connector: { + id: 'resilient-2', + name: 'My Connector 2', + type: '.resilient', + fields: { incidentTypes: ['19'], severityCode: '4' }, + }, + }) ); }); - }); - // FAILED ES PROMOTION: https://github.com/elastic/kibana/issues/84145 - describe.skip('Step 2 - Connector Fields', () => { - const connectorTypes = [ - { - label: 'Jira', - testId: 'jira-1', - dataTestSubj: 'connector-settings-jira', - }, - { - label: 'Resilient', - testId: 'resilient-2', - dataTestSubj: 'connector-settings-resilient', - }, - { - label: 'ServiceNow', - testId: 'servicenow-1', - dataTestSubj: 'connector-settings-sn', - }, - ]; - connectorTypes.forEach(({ label, testId, dataTestSubj }) => { - it(`should change from none to ${label} connector fields`, async () => { - useConnectorsMock.mockReturnValue({ - ...sampleConnectorData, - connectors: connectorsMock, - }); + it(`it should submit a servicenow connector`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <Create /> - </Router> - </TestProviders> - ); - - await waitFor(() => { - expect(wrapper.find(`[data-test-subj="${dataTestSubj}"]`).exists()).toBeFalsy(); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.find(`button[data-test-subj="dropdown-connector-${testId}"]`).simulate('click'); - wrapper.update(); - }); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + + await fillForm(wrapper); + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-servicenow-1"]`).simulate('click'); + wrapper.update(); + }); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeTruthy(); + }); - await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="${dataTestSubj}"]`).exists()).toBeTruthy(); + ['severitySelect', 'urgencySelect', 'impactSelect'].forEach((subj) => { + act(() => { + wrapper + .find(`select[data-test-subj="${subj}"]`) + .first() + .simulate('change', { + target: { value: '2' }, + }); }); }); + + await act(async () => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + }); + + await waitFor(() => + expect(postCase).toBeCalledWith({ + ...sampleData, + connector: { + id: 'servicenow-1', + name: 'My Connector', + type: '.servicenow', + fields: { impact: '2', severity: '2', urgency: '2' }, + }, + }) + ); }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx index 42633c5d2ccf8..5c50c37723083 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx @@ -3,319 +3,81 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPanel, - EuiSteps, -} from '@elastic/eui'; -import styled, { css } from 'styled-components'; + +import React, { useCallback } from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; -import { isEqual } from 'lodash/fp'; -import { - Field, - Form, - FormDataProvider, - getUseField, - UseField, - useForm, - useFormData, -} from '../../../shared_imports'; -import { usePostCase } from '../../containers/use_post_case'; -import { schema, FormProps } from './schema'; -import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; -import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form'; -import { useGetTags } from '../../containers/use_get_tags'; +import { Field, getUseField, useFormContext } from '../../../shared_imports'; import { getCaseDetailsUrl } from '../../../common/components/link_to'; -import { useTimelineClick } from '../../../common/utils/timeline/use_timeline_click'; -import { SettingFieldsForm } from '../settings/fields_form'; -import { useConnectors } from '../../containers/configure/use_connectors'; -import { ConnectorSelector } from '../connector_selector/form'; -import { useCaseConfigure } from '../../containers/configure/use_configure'; -import { - normalizeCaseConnector, - getConnectorById, - getNoneConnector, - normalizeActionConnector, -} from '../configure_cases/utils'; -import { ActionConnector } from '../../containers/types'; -import { ConnectorFields } from '../../../../../case/common/api/connectors'; import * as i18n from './translations'; +import { CreateCaseForm } from './form'; +import { FormContext } from './form_context'; +import { useInsertTimeline } from '../use_insert_timeline'; +import { fieldName as descriptionFieldName } from './description'; +import { SubmitCaseButton } from './submit_button'; export const CommonUseField = getUseField({ component: Field }); -interface ContainerProps { - big?: boolean; -} - -const Container = styled.div.attrs((props) => props)<ContainerProps>` - ${({ big, theme }) => css` - margin-top: ${big ? theme.eui.euiSizeXL : theme.eui.euiSize}; +const Container = styled.div` + ${({ theme }) => ` + margin-top: ${theme.eui.euiSize}; `} `; -const MySpinner = styled(EuiLoadingSpinner)` - position: absolute; - top: 50%; - left: 50%; - z-index: 99; -`; - -const initialCaseValue: FormProps = { - description: '', - tags: [], - title: '', - connectorId: 'none', +const InsertTimeline = () => { + const { setFieldValue, getFormData } = useFormContext(); + const formData = getFormData(); + const onTimelineAttached = useCallback( + (newValue: string) => setFieldValue(descriptionFieldName, newValue), + [setFieldValue] + ); + useInsertTimeline(formData[descriptionFieldName] ?? '', onTimelineAttached); + return null; }; export const Create = React.memo(() => { const history = useHistory(); - const { caseData, isLoading, postCase } = usePostCase(); - const { loading: isLoadingConnectors, connectors } = useConnectors(); - const { connector: configureConnector, loading: isLoadingCaseConfigure } = useCaseConfigure(); - const { tags: tagOptions } = useGetTags(); - - const [connector, setConnector] = useState<ActionConnector | null>(null); - const [options, setOptions] = useState( - tagOptions.map((label) => ({ - label, - })) - ); - - // This values uses useEffect to update, not useMemo, - // because we need to setState on it from the jsx - useEffect( - () => - setOptions( - tagOptions.map((label) => ({ - label, - })) - ), - [tagOptions] - ); - - const [fields, setFields] = useState<ConnectorFields>(null); - - const { form } = useForm<FormProps>({ - defaultValue: initialCaseValue, - options: { stripEmptyFields: false }, - schema, - }); - const currentConnectorId = useMemo( - () => - !isLoadingCaseConfigure - ? normalizeCaseConnector(connectors, configureConnector)?.id ?? 'none' - : null, - [configureConnector, connectors, isLoadingCaseConfigure] - ); - const { submit, setFieldValue } = form; - const [{ description }] = useFormData<{ - description: string; - }>({ - form, - watch: ['description'], - }); - const onChangeConnector = useCallback( - (newConnectorId) => { - if (connector == null || connector.id !== newConnectorId) { - setConnector(getConnectorById(newConnectorId, connectors) ?? null); - // Reset setting fields when changing connector - setFields(null); - } + const onSuccess = useCallback( + ({ id }) => { + history.push(getCaseDetailsUrl({ id })); }, - [connector, connectors] + [history] ); - const onDescriptionChange = useCallback((newValue) => setFieldValue('description', newValue), [ - setFieldValue, - ]); - - const { handleCursorChange } = useInsertTimeline(description, onDescriptionChange); - - const handleTimelineClick = useTimelineClick(); - - const onSubmit = useCallback(async () => { - const { isValid, data } = await submit(); - if (isValid) { - const { connectorId: dataConnectorId, ...dataWithoutConnectorId } = data; - const caseConnector = getConnectorById(dataConnectorId, connectors); - const connectorToUpdate = caseConnector - ? normalizeActionConnector(caseConnector, fields) - : getNoneConnector(); - - await postCase({ ...dataWithoutConnectorId, connector: connectorToUpdate }); - } - }, [submit, postCase, fields, connectors]); - const handleSetIsCancel = useCallback(() => { history.push('/'); }, [history]); - const firstStep = useMemo( - () => ({ - title: i18n.STEP_ONE_TITLE, - children: ( - <> - <CommonUseField - path="title" - componentProps={{ - idAria: 'caseTitle', - 'data-test-subj': 'caseTitle', - euiFieldProps: { - fullWidth: false, - disabled: isLoading, - }, - }} - /> - <Container> - <CommonUseField - path="tags" - componentProps={{ - idAria: 'caseTags', - 'data-test-subj': 'caseTags', - euiFieldProps: { - fullWidth: true, - placeholder: '', - disabled: isLoading, - options, - noSuggestions: false, - }, - }} - /> - <FormDataProvider pathsToWatch="tags"> - {({ tags: anotherTags }) => { - const current: string[] = options.map((opt) => opt.label); - const newOptions = anotherTags.reduce((acc: string[], item: string) => { - if (!acc.includes(item)) { - return [...acc, item]; - } - return acc; - }, current); - if (!isEqual(current, newOptions)) { - setOptions( - newOptions.map((label: string) => ({ - label, - })) - ); - } - return null; - }} - </FormDataProvider> - </Container> - <Container big> - <UseField - path={'description'} - component={MarkdownEditorForm} - componentProps={{ - dataTestSubj: 'caseDescription', - idAria: 'caseDescription', - isDisabled: isLoading, - onClickTimeline: handleTimelineClick, - onCursorPositionUpdate: handleCursorChange, - }} - /> - </Container> - </> - ), - }), - [isLoading, options, handleCursorChange, handleTimelineClick] - ); - - const secondStep = useMemo( - () => ({ - title: i18n.STEP_TWO_TITLE, - children: ( - <EuiFlexGroup> - <EuiFlexItem> - <Container> - <UseField - path="connectorId" - component={ConnectorSelector} - componentProps={{ - connectors, - dataTestSubj: 'caseConnectors', - defaultValue: currentConnectorId, - disabled: isLoadingConnectors, - idAria: 'caseConnectors', - isLoading, - }} - onChange={onChangeConnector} - /> - </Container> - </EuiFlexItem> - <EuiFlexItem> - <Container> - <SettingFieldsForm - connector={connector} - fields={fields} - isEdit={true} - onChange={setFields} - /> - </Container> - </EuiFlexItem> - </EuiFlexGroup> - ), - }), - [ - connector, - connectors, - currentConnectorId, - fields, - isLoading, - isLoadingConnectors, - onChangeConnector, - ] - ); - - const allSteps = useMemo(() => [firstStep, secondStep], [firstStep, secondStep]); - - if (caseData != null && caseData.id) { - history.push(getCaseDetailsUrl({ id: caseData.id })); - return null; - } - return ( <EuiPanel> - {isLoading && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />} - <Form form={form}> - <EuiSteps headingElement="h2" steps={allSteps} /> - </Form> - <Container> - <EuiFlexGroup - alignItems="center" - justifyContent="flexEnd" - gutterSize="xs" - responsive={false} - > - <EuiFlexItem grow={false}> - <EuiButtonEmpty - data-test-subj="create-case-cancel" - size="s" - onClick={handleSetIsCancel} - iconType="cross" - > - {i18n.CANCEL} - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - data-test-subj="create-case-submit" - fill - iconType="plusInCircle" - isDisabled={isLoading} - isLoading={isLoading} - onClick={onSubmit} - > - {i18n.CREATE_CASE} - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </Container> + <FormContext onSuccess={onSuccess}> + <CreateCaseForm /> + <Container> + <EuiFlexGroup + alignItems="center" + justifyContent="flexEnd" + gutterSize="xs" + responsive={false} + > + <EuiFlexItem grow={false}> + <EuiButtonEmpty + data-test-subj="create-case-cancel" + size="s" + onClick={handleSetIsCancel} + iconType="cross" + > + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <SubmitCaseButton /> + </EuiFlexItem> + </EuiFlexGroup> + </Container> + <InsertTimeline /> + </FormContext> </EuiPanel> ); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.test.tsx new file mode 100644 index 0000000000000..3bbdb1eafd47c --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.test.tsx @@ -0,0 +1,18 @@ +/* + * 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 { mount } from 'enzyme'; + +import { OptionalFieldLabel } from '.'; + +describe('OptionalFieldLabel', () => { + it('it renders correctly', async () => { + const wrapper = mount(OptionalFieldLabel); + expect(wrapper.find('[data-test-subj="form-optional-field-label"]').first().text()).toBe( + 'Optional' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.tsx index b86198e09ceac..4a491eac35d90 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/optional_field_label/index.tsx @@ -10,7 +10,7 @@ import React from 'react'; import * as i18n from '../../../translations'; export const OptionalFieldLabel = ( - <EuiText color="subdued" size="xs"> + <EuiText color="subdued" size="xs" data-test-subj="form-optional-field-label"> {i18n.OPTIONAL} </EuiText> ); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx b/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx index 5abac04d6ef37..a336860121c94 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CasePostRequest } from '../../../../../case/common/api'; +import { CasePostRequest, ConnectorTypeFields } from '../../../../../case/common/api'; import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../shared_imports'; import * as i18n from '../../translations'; @@ -18,7 +18,10 @@ export const schemaTags = { labelAppend: OptionalFieldLabel, }; -export type FormProps = Omit<CasePostRequest, 'connector'> & { connectorId: string }; +export type FormProps = Omit<CasePostRequest, 'connector'> & { + connectorId: string; + fields: ConnectorTypeFields['fields']; +}; export const schema: FormSchema<FormProps> = { title: { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/submit_button.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/submit_button.test.tsx new file mode 100644 index 0000000000000..c8f6ebc05582f --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/submit_button.test.tsx @@ -0,0 +1,83 @@ +/* + * 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 { mount } from 'enzyme'; +import { act, waitFor } from '@testing-library/react'; + +import { useForm, Form } from '../../../shared_imports'; +import { SubmitCaseButton } from './submit_button'; + +describe('SubmitCaseButton', () => { + const onSubmit = jest.fn(); + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm<{ title: string }>({ + defaultValue: { title: 'My title' }, + onSubmit, + }); + + return <Form form={form}>{children}</Form>; + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('it renders', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <SubmitCaseButton /> + </MockHookWrapperComponent> + ); + + expect(wrapper.find(`[data-test-subj="create-case-submit"]`).exists()).toBeTruthy(); + }); + + it('it submits', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <SubmitCaseButton /> + </MockHookWrapperComponent> + ); + + await act(async () => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + }); + + await waitFor(() => expect(onSubmit).toBeCalled()); + }); + + it('it disables when submitting', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <SubmitCaseButton /> + </MockHookWrapperComponent> + ); + + await waitFor(() => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + expect( + wrapper.find(`[data-test-subj="create-case-submit"]`).first().prop('isDisabled') + ).toBeTruthy(); + }); + }); + + it('it is loading when submitting', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <SubmitCaseButton /> + </MockHookWrapperComponent> + ); + + await waitFor(() => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + expect( + wrapper.find(`[data-test-subj="create-case-submit"]`).first().prop('isLoading') + ).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/submit_button.tsx b/x-pack/plugins/security_solution/public/cases/components/create/submit_button.tsx new file mode 100644 index 0000000000000..8cffce290ff11 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/submit_button.tsx @@ -0,0 +1,30 @@ +/* + * 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, { memo } from 'react'; +import { EuiButton } from '@elastic/eui'; + +import { useFormContext } from '../../../shared_imports'; +import * as i18n from './translations'; + +const SubmitCaseButtonComponent: React.FC = () => { + const { submit, isSubmitting } = useFormContext(); + + return ( + <EuiButton + data-test-subj="create-case-submit" + fill + iconType="plusInCircle" + isDisabled={isSubmitting} + isLoading={isSubmitting} + onClick={submit} + > + {i18n.CREATE_CASE} + </EuiButton> + ); +}; + +export const SubmitCaseButton = memo(SubmitCaseButtonComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/tags.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/tags.test.tsx new file mode 100644 index 0000000000000..c06ac011a035b --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/tags.test.tsx @@ -0,0 +1,77 @@ +/* + * 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 { mount } from 'enzyme'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { waitFor } from '@testing-library/react'; + +import { useForm, Form, FormHook, FIELD_TYPES } from '../../../shared_imports'; +import { useGetTags } from '../../containers/use_get_tags'; +import { Tags } from './tags'; + +jest.mock('../../containers/use_get_tags'); +const useGetTagsMock = useGetTags as jest.Mock; + +describe('Tags', () => { + let globalForm: FormHook; + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm<{ tags: string[] }>({ + defaultValue: { tags: [] }, + schema: { + tags: { type: FIELD_TYPES.COMBO_BOX }, + }, + }); + + globalForm = form; + + return <Form form={form}>{children}</Form>; + }; + + beforeEach(() => { + jest.resetAllMocks(); + useGetTagsMock.mockReturnValue({ tags: ['test'] }); + }); + + it('it renders', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Tags isLoading={false} /> + </MockHookWrapperComponent> + ); + + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="caseTags"]`).exists()).toBeTruthy(); + }); + }); + + it('it disables the input when loading', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Tags isLoading={true} /> + </MockHookWrapperComponent> + ); + + expect(wrapper.find(EuiComboBox).prop('disabled')).toBeTruthy(); + }); + + it('it changes the tags', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Tags isLoading={false} /> + </MockHookWrapperComponent> + ); + + await waitFor(() => { + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange(['test', 'case'].map((tag) => ({ label: tag }))); + }); + + expect(globalForm.getFormData()).toEqual({ tags: ['test', 'case'] }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/tags.tsx b/x-pack/plugins/security_solution/public/cases/components/create/tags.tsx new file mode 100644 index 0000000000000..8a7b4a6e5f760 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/tags.tsx @@ -0,0 +1,48 @@ +/* + * 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, { memo, useMemo } from 'react'; + +import { Field, getUseField } from '../../../shared_imports'; +import { useGetTags } from '../../containers/use_get_tags'; + +const CommonUseField = getUseField({ component: Field }); + +interface Props { + isLoading: boolean; +} + +const TagsComponent: React.FC<Props> = ({ isLoading }) => { + const { tags: tagOptions, isLoading: isLoadingTags } = useGetTags(); + const options = useMemo( + () => + tagOptions.map((label) => ({ + label, + })), + [tagOptions] + ); + + return ( + <CommonUseField + path="tags" + componentProps={{ + idAria: 'caseTags', + 'data-test-subj': 'caseTags', + euiFieldProps: { + fullWidth: true, + placeholder: '', + disabled: isLoading || isLoadingTags, + options, + noSuggestions: false, + }, + }} + /> + ); +}; + +TagsComponent.displayName = 'TagsComponent'; + +export const Tags = memo(TagsComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/title.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/title.test.tsx new file mode 100644 index 0000000000000..54a4e665a56e5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/title.test.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 React from 'react'; +import { mount } from 'enzyme'; +import { act } from '@testing-library/react'; + +import { useForm, Form, FormHook } from '../../../shared_imports'; +import { Title } from './title'; + +describe('Title', () => { + let globalForm: FormHook; + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm<{ title: string }>({ + defaultValue: { title: 'My title' }, + }); + + globalForm = form; + + return <Form form={form}>{children}</Form>; + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('it renders', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Title isLoading={false} /> + </MockHookWrapperComponent> + ); + + expect(wrapper.find(`[data-test-subj="caseTitle"]`).exists()).toBeTruthy(); + }); + + it('it disables the input when loading', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Title isLoading={true} /> + </MockHookWrapperComponent> + ); + + expect(wrapper.find(`[data-test-subj="caseTitle"] input`).prop('disabled')).toBeTruthy(); + }); + + it('it changes the title', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Title isLoading={false} /> + </MockHookWrapperComponent> + ); + + await act(async () => { + wrapper + .find(`[data-test-subj="caseTitle"] input`) + .first() + .simulate('change', { target: { value: 'My new title' } }); + }); + + expect(globalForm.getFormData()).toEqual({ title: 'My new title' }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/title.tsx b/x-pack/plugins/security_solution/public/cases/components/create/title.tsx new file mode 100644 index 0000000000000..2daeb9b738e23 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/title.tsx @@ -0,0 +1,32 @@ +/* + * 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, { memo } from 'react'; +import { Field, getUseField } from '../../../shared_imports'; + +const CommonUseField = getUseField({ component: Field }); + +interface Props { + isLoading: boolean; +} + +const TitleComponent: React.FC<Props> = ({ isLoading }) => ( + <CommonUseField + path="title" + componentProps={{ + idAria: 'caseTitle', + 'data-test-subj': 'caseTitle', + euiFieldProps: { + fullWidth: true, + disabled: isLoading, + }, + }} + /> +); + +TitleComponent.displayName = 'TitleComponent'; + +export const Title = memo(TitleComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx index 344ca88f5ab37..f5be9740bc4f1 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx @@ -8,7 +8,7 @@ import React, { memo, useMemo } from 'react'; import { EuiCard, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; import styled from 'styled-components'; -import { connectorsConfiguration } from '../../../common/lib/connectors/config'; +import { connectorsConfiguration } from '../connectors'; import { ConnectorTypes } from '../../../../../case/common/api/connectors'; interface ConnectorCardProps { diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/fields_form.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/fields_form.tsx index 87536b62747e8..79ae87355b5fb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/fields_form.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/settings/fields_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, Suspense, useCallback } from 'react'; +import React, { memo, Suspense } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import { CaseSettingsConnector, SettingFieldsProps } from './types'; @@ -18,13 +18,6 @@ interface Props extends Omit<SettingFieldsProps<ConnectorTypeFields['fields']>, const SettingFieldsFormComponent: React.FC<Props> = ({ connector, isEdit, onChange, fields }) => { const { caseSettingsRegistry } = getCaseSettings(); - const onFieldsChange = useCallback( - (newFields) => { - onChange(newFields); - }, - [onChange] - ); - if (connector == null || connector.actionTypeId == null || connector.actionTypeId === '.none') { return null; } @@ -45,12 +38,14 @@ const SettingFieldsFormComponent: React.FC<Props> = ({ connector, isEdit, onChan </EuiFlexGroup> } > - <FieldsComponent - isEdit={isEdit} - fields={fields} - connector={connector} - onChange={onFieldsChange} - /> + <div data-test-subj={'connector-settings'}> + <FieldsComponent + isEdit={isEdit} + fields={fields} + connector={connector} + onChange={onChange} + /> + </div> </Suspense> ) : null} </> diff --git a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx index a04450b3c4198..83afa4c4f5ed3 100644 --- a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx @@ -18,13 +18,14 @@ import { import styled, { css } from 'styled-components'; import { isEqual } from 'lodash/fp'; import * as i18n from './translations'; -import { Form, FormDataProvider, useForm } from '../../../shared_imports'; +import { Form, FormDataProvider, useForm, getUseField, Field } from '../../../shared_imports'; import { schema } from './schema'; -import { CommonUseField } from '../create'; import { useGetTags } from '../../containers/use_get_tags'; import { Tags } from './tags'; +const CommonUseField = getUseField({ component: Field }); + interface TagListProps { disabled?: boolean; isLoading: boolean; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx index 7a12f9211e969..b5885b330a822 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx @@ -22,10 +22,7 @@ export interface AllCasesModalProps { onRowClick: (id?: string) => void; } -const AllCasesModalComponent: React.FC<AllCasesModalProps> = ({ - onCloseCaseModal, - onRowClick, -}: AllCasesModalProps) => { +const AllCasesModalComponent: React.FC<AllCasesModalProps> = ({ onCloseCaseModal, onRowClick }) => { const userPermissions = useGetUserSavedObjectPermissions(); const userCanCrud = userPermissions?.crud ?? false; return ( diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx new file mode 100644 index 0000000000000..68446fc5b3171 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx @@ -0,0 +1,68 @@ +/* + * 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, { memo, useCallback } from 'react'; +import styled from 'styled-components'; +import { + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, +} from '@elastic/eui'; + +import { FormContext } from '../create/form_context'; +import { CreateCaseForm } from '../create/form'; +import { SubmitCaseButton } from '../create/submit_button'; +import { Case } from '../../containers/types'; +import * as i18n from '../../translations'; + +export interface CreateCaseModalProps { + onCloseCaseModal: () => void; + onCaseCreated: (theCase: Case) => void; +} + +const Container = styled.div` + ${({ theme }) => ` + margin-top: ${theme.eui.euiSize}; + text-align: right; + `} +`; + +const CreateModalComponent: React.FC<CreateCaseModalProps> = ({ + onCloseCaseModal, + onCaseCreated, +}) => { + const onSuccess = useCallback( + (theCase) => { + onCaseCreated(theCase); + onCloseCaseModal(); + }, + [onCaseCreated, onCloseCaseModal] + ); + + return ( + <EuiOverlayMask data-test-subj="all-cases-modal"> + <EuiModal onClose={onCloseCaseModal}> + <EuiModalHeader> + <EuiModalHeaderTitle>{i18n.CREATE_TITLE}</EuiModalHeaderTitle> + </EuiModalHeader> + <EuiModalBody> + <FormContext onSuccess={onSuccess}> + <CreateCaseForm withSteps={false} /> + <Container> + <SubmitCaseButton /> + </Container> + </FormContext> + </EuiModalBody> + </EuiModal> + </EuiOverlayMask> + ); +}; + +export const CreateCaseModal = memo(CreateModalComponent); + +CreateCaseModal.displayName = 'CreateCaseModal'; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx new file mode 100644 index 0000000000000..f07be3cc60821 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx @@ -0,0 +1,45 @@ +/* + * 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, { useState, useCallback, useMemo } from 'react'; +import { Case } from '../../containers/types'; +import { CreateCaseModal } from './create_case_modal'; + +interface Props { + onCaseCreated: (theCase: Case) => void; +} +export interface UseAllCasesModalReturnedValues { + Modal: React.FC; + isModalOpen: boolean; + closeModal: () => void; + openModal: () => void; +} + +export const useCreateCaseModal = ({ onCaseCreated }: Props) => { + const [isModalOpen, setIsModalOpen] = useState<boolean>(false); + const closeModal = useCallback(() => setIsModalOpen(false), []); + const openModal = useCallback(() => setIsModalOpen(true), []); + + const Modal: React.FC = useCallback( + () => + isModalOpen ? ( + <CreateCaseModal onCloseCaseModal={closeModal} onCaseCreated={onCaseCreated} /> + ) : null, + [closeModal, isModalOpen, onCaseCreated] + ); + + const state = useMemo( + () => ({ + Modal, + isModalOpen, + closeModal, + openModal, + }), + [isModalOpen, closeModal, openModal, Modal] + ); + + return state; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_insert_timeline/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_insert_timeline/index.tsx new file mode 100644 index 0000000000000..c44193dc363a4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/use_insert_timeline/index.tsx @@ -0,0 +1,63 @@ +/* + * 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 { useCallback, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { isEmpty } from 'lodash/fp'; + +import { getTimelineUrl, useFormatUrl } from '../../../common/components/link_to'; +import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline'; +import { SecurityPageName } from '../../../app/types'; +import { setInsertTimeline } from '../../../timelines/store/timeline/actions'; + +interface UseInsertTimelineReturn { + handleOnTimelineChange: (title: string, id: string | null, graphEventId?: string) => void; +} + +export const useInsertTimeline = ( + value: string, + onChange: (newValue: string) => void +): UseInsertTimelineReturn => { + const dispatch = useDispatch(); + const { formatUrl } = useFormatUrl(SecurityPageName.timelines); + + const insertTimeline = useShallowEqualSelector(timelineSelectors.selectInsertTimeline); + + const handleOnTimelineChange = useCallback( + (title: string, id: string | null, graphEventId?: string) => { + const url = formatUrl(getTimelineUrl(id ?? '', graphEventId), { + absolute: true, + skipSearch: true, + }); + + let newValue = `[${title}](${url})`; + // Leave a space between the previous value and the timeline url if the value is not empty. + if (!isEmpty(value)) { + newValue = `${value} ${newValue}`; + } + + onChange(newValue); + }, + [value, onChange, formatUrl] + ); + + useEffect(() => { + if (insertTimeline != null && value != null) { + dispatch(timelineActions.showTimeline({ id: insertTimeline.timelineId, show: false })); + handleOnTimelineChange( + insertTimeline.timelineTitle, + insertTimeline.timelineSavedObjectId, + insertTimeline.graphEventId + ); + dispatch(setInsertTimeline(null)); + } + }, [insertTimeline, dispatch, handleOnTimelineChange, value]); + + return { + handleOnTimelineChange, + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap index d95e0300fe140..c7841f6d6bbcc 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap @@ -182,7 +182,9 @@ exports[`item_details_card ItemDetailsPropertySummary should render correctly 1` <Styled(EuiDescriptionListTitle)> name 1 </Styled(EuiDescriptionListTitle)> - <Styled(EuiDescriptionListDescription)> + <Styled(EuiDescriptionListDescription) + className="eui-textBreakWord" + > value 1 </Styled(EuiDescriptionListDescription)> </Fragment> diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx index 829d8db5a5a0f..8f32224f860e3 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx @@ -72,7 +72,7 @@ export const ItemDetailsPropertySummary = memo<ItemDetailsPropertySummaryProps>( ({ name, value }) => ( <> <DescriptionListTitle>{name}</DescriptionListTitle> - <DescriptionListDescription>{value}</DescriptionListDescription> + <DescriptionListDescription className="eui-textBreakWord">{value}</DescriptionListDescription> </> ) ); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/case/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/case/translations.ts deleted file mode 100644 index a39e04acc1bf3..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/case/translations.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const CASE_CONNECTOR_DESC = i18n.translate( - 'xpack.securitySolution.case.components.case.selectMessageText', - { - defaultMessage: 'Create or update a case.', - } -); - -export const CASE_CONNECTOR_TITLE = i18n.translate( - 'xpack.securitySolution.case.components.case.actionTypeTitle', - { - defaultMessage: 'Cases', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 6b7cc8167ede6..92657df7f9bb5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -45,13 +45,13 @@ describe('alert actions', () => { updateTimelineIsLoading = jest.fn() as jest.Mocked<UpdateTimelineLoading>; searchStrategyClient = { + ...dataPluginMock.createStartContract().search, aggs: {} as ISearchStart['aggs'], showError: jest.fn(), search: jest .fn() .mockImplementation(() => ({ toPromise: () => ({ data: mockTimelineDetails }) })), searchSource: {} as ISearchStart['searchSource'], - session: dataPluginMock.createStartContract().search.session, }; jest.spyOn(apolloClient, 'query').mockImplementation((obj) => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts index e99894afeb63c..e6482577f89ed 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts @@ -13,6 +13,6 @@ export const getStatusColor = (status: RuleStatusType | string | null) => ? 'success' : status === 'failed' ? 'danger' - : status === 'executing' || status === 'going to run' + : status === 'executing' || status === 'going to run' || status === 'partial failure' ? 'warning' : 'subdued'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx index 5203a630b72ae..de2d390ee6784 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx @@ -221,7 +221,7 @@ export const schema: FormSchema<DefineStepRule> = { message: i18n.translate( 'xpack.securitySolution.detectionEngine.validations.thresholdValueFieldData.numberGreaterThanOrEqualOneErrorMessage', { - defaultMessage: 'Value must be greater than or equal one.', + defaultMessage: 'Value must be greater than or equal to one.', } ), allowEquality: true, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index e9c89130736c0..d7908a6780ebb 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -70,6 +70,13 @@ const MetaRule = t.intersection([ }), ]); +const StatusTypes = t.union([ + t.literal('succeeded'), + t.literal('failed'), + t.literal('going to run'), + t.literal('partial failure'), +]); + export const RuleSchema = t.intersection([ t.type({ author, @@ -108,13 +115,15 @@ export const RuleSchema = t.intersection([ license, last_failure_at: t.string, last_failure_message: t.string, + last_success_message: t.string, + last_success_at: t.string, meta: MetaRule, machine_learning_job_id: t.string, output_index: t.string, query: t.string, rule_name_override, saved_id: t.string, - status: t.string, + status: StatusTypes, status_date: t.string, threshold, threat_query, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index ba676835d60f1..4866824f882cf 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -275,18 +275,33 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({ ), [ruleDetailTabs, ruleDetailTab, setRuleDetailTab] ); - const ruleError = useMemo( - () => + const ruleError = useMemo(() => { + if ( rule?.status === 'failed' && ruleDetailTab === RuleDetailTabs.alerts && - rule?.last_failure_at != null ? ( + rule?.last_failure_at != null + ) { + return ( <RuleStatusFailedCallOut message={rule?.last_failure_message ?? ''} date={rule?.last_failure_at} /> - ) : null, - [rule, ruleDetailTab] - ); + ); + } else if ( + rule?.status === 'partial failure' && + ruleDetailTab === RuleDetailTabs.alerts && + rule?.last_success_at != null + ) { + return ( + <RuleStatusFailedCallOut + message={rule?.last_success_message ?? ''} + date={rule?.last_success_at} + color="warning" + /> + ); + } + return null; + }, [rule, ruleDetailTab]); const updateDateRangeCallback = useCallback<UpdateDateRange>( ({ x }) => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx index 3394b0fc8c5c0..c743623402063 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.test.tsx @@ -13,6 +13,13 @@ describe('RuleStatusFailedCallOut', () => { it('renders correctly', () => { const wrapper = shallow(<RuleStatusFailedCallOut date="date" message="message" />); + expect(wrapper.find('EuiCallOut')).toHaveLength(1); + }); + it('renders correctly with optional params', () => { + const wrapper = shallow( + <RuleStatusFailedCallOut date="date" message="message" color="warning" /> + ); + expect(wrapper.find('EuiCallOut')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx index 5b5b96ace8670..121ff6b8686c3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/status_failed_callout.tsx @@ -13,22 +13,26 @@ import * as i18n from './translations'; interface RuleStatusFailedCallOutComponentProps { date: string; message: string; + color?: 'danger' | 'primary' | 'success' | 'warning'; } const RuleStatusFailedCallOutComponent: React.FC<RuleStatusFailedCallOutComponentProps> = ({ date, message, + color, }) => ( <EuiCallOut title={ <EuiFlexGroup gutterSize="xs" alignItems="center" justifyContent="flexStart"> - <EuiFlexItem grow={false}>{i18n.ERROR_CALLOUT_TITLE}</EuiFlexItem> + <EuiFlexItem grow={false}> + {color === 'warning' ? i18n.PARTIAL_FAILURE_CALLOUT_TITLE : i18n.ERROR_CALLOUT_TITLE} + </EuiFlexItem> <EuiFlexItem grow={true}> <FormattedDate value={date} fieldName="last_failure_at" /> </EuiFlexItem> </EuiFlexGroup> } - color="danger" + color={color ? color : 'danger'} iconType="alert" > <p>{message}</p> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts index 94dfdc3e9daa0..5fbe0a5b78671 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts @@ -48,6 +48,13 @@ export const ERROR_CALLOUT_TITLE = i18n.translate( } ); +export const PARTIAL_FAILURE_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.partialErrorCalloutTitle', + { + defaultMessage: 'Partial rule failure at', + } +); + export const FAILURE_HISTORY_TAB = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.failureHistoryTab', { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx similarity index 60% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx index 21fe14df81dd2..072f588663c5a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx @@ -14,6 +14,29 @@ import { isAntivirusRegistrationEnabled } from '../../../store/policy_details/se import { usePolicyDetailsSelector } from '../../policy_hooks'; import { ConfigForm } from '../../components/config_form'; +const TRANSLATIONS: Readonly<{ [K in 'title' | 'description' | 'label']: string }> = { + title: i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.type', + { + defaultMessage: 'Register as anti-virus', + } + ), + description: i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation', + { + defaultMessage: + 'Toggle on to register Elastic as an official Anti-Virus solution for Windows OS. ' + + 'This will also disable Windows Defender.', + } + ), + label: i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.toggle', + { + defaultMessage: 'Register as anti-virus', + } + ), +}; + export const AntivirusRegistrationForm = memo(() => { const antivirusRegistrationEnabled = usePolicyDetailsSelector(isAntivirusRegistrationEnabled); const dispatch = useDispatch(); @@ -30,31 +53,11 @@ export const AntivirusRegistrationForm = memo(() => { ); return ( - <ConfigForm - type={i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.type', - { - defaultMessage: 'Register as anti-virus', - } - )} - supportedOss={[OperatingSystem.WINDOWS]} - > - <EuiText size="s"> - {i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation', - { - defaultMessage: 'Switch the toggle to on to register Elastic anti-virus', - } - )} - </EuiText> + <ConfigForm type={TRANSLATIONS.title} supportedOss={[OperatingSystem.WINDOWS]}> + <EuiText size="s">{TRANSLATIONS.description}</EuiText> <EuiSpacer size="s" /> <EuiSwitch - label={i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.toggle', - { - defaultMessage: 'Register as anti-virus', - } - )} + label={TRANSLATIONS.label} checked={antivirusRegistrationEnabled} onChange={handleSwitchChange} /> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx new file mode 100644 index 0000000000000..7cdf54316e4e2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx @@ -0,0 +1,87 @@ +/* + * 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 { EuiCheckbox, EuiSpacer, EuiText, htmlIdGenerator } from '@elastic/eui'; +import { OperatingSystem, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { OS } from '../../../types'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; + +const OPERATING_SYSTEM_TO_TEST_SUBJ: { [K in OperatingSystem]: string } = { + [OperatingSystem.WINDOWS]: 'Windows', + [OperatingSystem.LINUX]: 'Linux', + [OperatingSystem.MAC]: 'Mac', +}; + +interface OperatingSystemToOsMap { + [OperatingSystem.WINDOWS]: OS.windows; + [OperatingSystem.LINUX]: OS.linux; + [OperatingSystem.MAC]: OS.mac; +} + +export type ProtectionField< + T extends OperatingSystem +> = keyof UIPolicyConfig[OperatingSystemToOsMap[T]]['events']; + +export type EventFormSelection<T extends OperatingSystem> = { [K in ProtectionField<T>]: boolean }; + +export interface EventFormOption<T extends OperatingSystem> { + name: string; + protectionField: ProtectionField<T>; +} + +export interface EventsFormProps<T extends OperatingSystem> { + os: T; + options: ReadonlyArray<EventFormOption<T>>; + selection: EventFormSelection<T>; + onValueSelection: (value: ProtectionField<T>, selected: boolean) => void; +} + +const countSelected = <T extends OperatingSystem>(selection: EventFormSelection<T>) => { + return Object.values(selection).filter((value) => value).length; +}; + +export const EventsForm = <T extends OperatingSystem>({ + os, + options, + selection, + onValueSelection, +}: EventsFormProps<T>) => ( + <ConfigForm + type={i18n.translate('xpack.securitySolution.endpoint.policy.details.eventCollection', { + defaultMessage: 'Event Collection', + })} + supportedOss={[os]} + rightCorner={ + <EuiText size="s" color="subdued"> + {i18n.translate('xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled', { + defaultMessage: '{selected} / {total} event collections enabled', + values: { selected: countSelected(selection), total: options.length }, + })} + </EuiText> + } + > + <ConfigFormHeading> + {i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents', { + defaultMessage: 'Events', + })} + </ConfigFormHeading> + <EuiSpacer size="s" /> + {options.map(({ name, protectionField }) => ( + <EuiCheckbox + key={String(protectionField)} + id={htmlIdGenerator()()} + label={name} + data-test-subj={`policy${OPERATING_SYSTEM_TO_TEST_SUBJ[os]}Event_${protectionField}`} + checked={selection[protectionField]} + onChange={(event) => onValueSelection(protectionField, event.target.checked)} + /> + ))} + </ConfigForm> +); + +EventsForm.displayName = 'EventsForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 9c11bc6f5a4d1..1ce099c494cf0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -36,7 +36,7 @@ import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; -import { AntivirusRegistrationForm } from './policy_forms/antivirus_registration'; +import { AntivirusRegistrationForm } from './components/antivirus_registration_form'; import { useToasts } from '../../../../common/lib/kibana'; import { AppAction } from '../../../../common/store/actions'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx deleted file mode 100644 index 76077831c670b..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/checkbox.tsx +++ /dev/null @@ -1,55 +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 React, { useCallback, useMemo } from 'react'; -import { EuiCheckbox, EuiCheckboxProps, htmlIdGenerator } from '@elastic/eui'; -import { useDispatch } from 'react-redux'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { policyConfig } from '../../../store/policy_details/selectors'; -import { PolicyDetailsAction } from '../../../store/policy_details'; -import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; - -type EventsCheckboxProps = Omit<EuiCheckboxProps, 'id' | 'label' | 'checked' | 'onChange'> & { - name: string; - setter: (config: UIPolicyConfig, checked: boolean) => UIPolicyConfig; - getter: (config: UIPolicyConfig) => boolean; -}; - -export const EventsCheckbox = React.memo(function ({ - name, - setter, - getter, - ...otherProps -}: EventsCheckboxProps) { - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const selected = getter(policyDetailsConfig); - const dispatch = useDispatch<(action: PolicyDetailsAction) => void>(); - const checkboxId = useMemo(() => htmlIdGenerator()(), []); - - const handleCheckboxChange = useCallback( - (event: React.ChangeEvent<HTMLInputElement>) => { - if (policyDetailsConfig) { - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: setter(policyDetailsConfig, event.target.checked) }, - }); - } - }, - [dispatch, policyDetailsConfig, setter] - ); - - return ( - <EuiCheckbox - id={checkboxId} - label={name} - checked={selected} - onChange={handleCheckboxChange} - {...otherProps} - /> - ); -}); - -EventsCheckbox.displayName = 'EventsCheckbox'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx index 999e3bac5653a..f9532eaecf701 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx @@ -4,96 +4,59 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText } from '@elastic/eui'; -import { EventsCheckbox } from './checkbox'; -import { OS } from '../../../types'; +import { useDispatch } from 'react-redux'; +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { policyConfig } from '../../../store/policy_details/selectors'; +import { setIn } from '../../../models/policy_details_config'; import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { selectedLinuxEvents, totalLinuxEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; -import { getIn, setIn } from '../../../models/policy_details_config'; -import { OperatingSystem, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; -import { - COLLECTIONS_ENABLED_MESSAGE, - EVENTS_FORM_TYPE_LABEL, - EVENTS_HEADING, -} from './translations'; +import { EventFormOption, EventsForm } from '../../components/events_form'; -export const LinuxEvents = React.memo(() => { - const selected = usePolicyDetailsSelector(selectedLinuxEvents); - const total = usePolicyDetailsSelector(totalLinuxEvents); - - const checkboxes = useMemo(() => { - const items: Array<{ - name: string; - os: 'linux'; - protectionField: keyof UIPolicyConfig['linux']['events']; - }> = [ - { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.file', - { - defaultMessage: 'File', - } - ), - os: OS.linux, - protectionField: 'file', - }, +const OPTIONS: ReadonlyArray<EventFormOption<OperatingSystem.LINUX>> = [ + { + name: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.file', { + defaultMessage: 'File', + }), + protectionField: 'file', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.process', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.process', - { - defaultMessage: 'Process', - } - ), - os: OS.linux, - protectionField: 'process', - }, + defaultMessage: 'Process', + } + ), + protectionField: 'process', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.network', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.network', - { - defaultMessage: 'Network', - } - ), - os: OS.linux, - protectionField: 'network', - }, - ]; - return ( - <> - <ConfigFormHeading>{EVENTS_HEADING}</ConfigFormHeading> - <EuiSpacer size="s" /> - {items.map((item, index) => { - return ( - <EventsCheckbox - name={item.name} - key={index} - data-test-subj={`policyLinuxEvent_${item.protectionField}`} - setter={(config, checked) => - setIn(config)(item.os)('events')(item.protectionField)(checked) - } - getter={(config) => getIn(config)(item.os)('events')(item.protectionField)} - /> - ); - })} - </> - ); - }, []); + defaultMessage: 'Network', + } + ), + protectionField: 'network', + }, +]; + +export const LinuxEvents = memo(() => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch(); return ( - <ConfigForm - type={EVENTS_FORM_TYPE_LABEL} - supportedOss={[OperatingSystem.LINUX]} - dataTestSubj="linuxEventingForm" - rightCorner={ - <EuiText size="s" color="subdued"> - {COLLECTIONS_ENABLED_MESSAGE(selected, total)} - </EuiText> + <EventsForm<OperatingSystem.LINUX> + os={OperatingSystem.LINUX} + selection={policyDetailsConfig.linux.events} + options={OPTIONS} + onValueSelection={(value, selected) => + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: setIn(policyDetailsConfig)('linux')('events')(value)(selected) }, + }) } - > - {checkboxes} - </ConfigForm> + /> ); }); + +LinuxEvents.displayName = 'LinuxEvents'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx index 6e15a3c4cd43b..ac6ae531ba172 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx @@ -4,96 +4,53 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText } from '@elastic/eui'; -import { EventsCheckbox } from './checkbox'; -import { OS } from '../../../types'; +import { useDispatch } from 'react-redux'; +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { policyConfig } from '../../../store/policy_details/selectors'; +import { setIn } from '../../../models/policy_details_config'; import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { selectedMacEvents, totalMacEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; -import { getIn, setIn } from '../../../models/policy_details_config'; -import { OperatingSystem, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; -import { - COLLECTIONS_ENABLED_MESSAGE, - EVENTS_FORM_TYPE_LABEL, - EVENTS_HEADING, -} from './translations'; +import { EventFormOption, EventsForm } from '../../components/events_form'; -export const MacEvents = React.memo(() => { - const selected = usePolicyDetailsSelector(selectedMacEvents); - const total = usePolicyDetailsSelector(totalMacEvents); +const OPTIONS: ReadonlyArray<EventFormOption<OperatingSystem.MAC>> = [ + { + name: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.file', { + defaultMessage: 'File', + }), + protectionField: 'file', + }, + { + name: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.process', { + defaultMessage: 'Process', + }), + protectionField: 'process', + }, + { + name: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.network', { + defaultMessage: 'Network', + }), + protectionField: 'network', + }, +]; - const checkboxes = useMemo(() => { - const items: Array<{ - name: string; - os: 'mac'; - protectionField: keyof UIPolicyConfig['mac']['events']; - }> = [ - { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.file', - { - defaultMessage: 'File', - } - ), - os: OS.mac, - protectionField: 'file', - }, - { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.process', - { - defaultMessage: 'Process', - } - ), - os: OS.mac, - protectionField: 'process', - }, - { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.network', - { - defaultMessage: 'Network', - } - ), - os: OS.mac, - protectionField: 'network', - }, - ]; - return ( - <> - <ConfigFormHeading>{EVENTS_HEADING}</ConfigFormHeading> - <EuiSpacer size="s" /> - {items.map((item, index) => { - return ( - <EventsCheckbox - name={item.name} - key={index} - data-test-subj={`policyMacEvent_${item.protectionField}`} - setter={(config, checked) => - setIn(config)(item.os)('events')(item.protectionField)(checked) - } - getter={(config) => getIn(config)(item.os)('events')(item.protectionField)} - /> - ); - })} - </> - ); - }, []); +export const MacEvents = memo(() => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch(); return ( - <ConfigForm - type={EVENTS_FORM_TYPE_LABEL} - supportedOss={[OperatingSystem.MAC]} - dataTestSubj="macEventingForm" - rightCorner={ - <EuiText size="s" color="subdued"> - {COLLECTIONS_ENABLED_MESSAGE(selected, total)} - </EuiText> + <EventsForm<OperatingSystem.MAC> + os={OperatingSystem.MAC} + selection={policyDetailsConfig.mac.events} + options={OPTIONS} + onValueSelection={(value, selected) => + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: setIn(policyDetailsConfig)('mac')('events')(value)(selected) }, + }) } - > - {checkboxes} - </ConfigForm> + /> ); }); + +MacEvents.displayName = 'MacEvents'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts deleted file mode 100644 index 3b48b7969a8ce..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts +++ /dev/null @@ -1,28 +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 { i18n } from '@kbn/i18n'; - -export const EVENTS_HEADING = i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents', - { - defaultMessage: 'Events', - } -); - -export const EVENTS_FORM_TYPE_LABEL = i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.eventCollection', - { - defaultMessage: 'Event Collection', - } -); - -export const COLLECTIONS_ENABLED_MESSAGE = (selected: number, total: number) => { - return i18n.translate('xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled', { - defaultMessage: '{selected} / {total} event collections enabled', - values: { selected, total }, - }); -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx index c381249cf24b9..c99f2a6b72ac3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx @@ -4,142 +4,97 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText } from '@elastic/eui'; -import { EventsCheckbox } from './checkbox'; -import { OS } from '../../../types'; +import { useDispatch } from 'react-redux'; +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { policyConfig } from '../../../store/policy_details/selectors'; +import { setIn } from '../../../models/policy_details_config'; import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { selectedWindowsEvents, totalWindowsEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; -import { getIn, setIn } from '../../../models/policy_details_config'; -import { - Immutable, - OperatingSystem, - UIPolicyConfig, -} from '../../../../../../../common/endpoint/types'; -import { - COLLECTIONS_ENABLED_MESSAGE, - EVENTS_FORM_TYPE_LABEL, - EVENTS_HEADING, -} from './translations'; +import { EventFormOption, EventsForm } from '../../components/events_form'; -export const WindowsEvents = React.memo(() => { - const selected = usePolicyDetailsSelector(selectedWindowsEvents); - const total = usePolicyDetailsSelector(totalWindowsEvents); - - const checkboxes = useMemo(() => { - const items: Immutable< - Array<{ - name: string; - os: 'windows'; - protectionField: keyof UIPolicyConfig['windows']['events']; - }> - > = [ - { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.dllDriverLoad', - { - defaultMessage: 'DLL and Driver Load', - } - ), - os: OS.windows, - protectionField: 'dll_and_driver_load', - }, +const OPTIONS: ReadonlyArray<EventFormOption<OperatingSystem.WINDOWS>> = [ + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.dllDriverLoad', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.dns', - { - defaultMessage: 'DNS', - } - ), - os: OS.windows, - protectionField: 'dns', - }, + defaultMessage: 'DLL and Driver Load', + } + ), + protectionField: 'dll_and_driver_load', + }, + { + name: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.dns', { + defaultMessage: 'DNS', + }), + protectionField: 'dns', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.file', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.file', - { - defaultMessage: 'File', - } - ), - os: OS.windows, - protectionField: 'file', - }, + defaultMessage: 'File', + } + ), + protectionField: 'file', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.network', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.network', - { - defaultMessage: 'Network', - } - ), - os: OS.windows, - protectionField: 'network', - }, + defaultMessage: 'Network', + } + ), + protectionField: 'network', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.process', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.process', - { - defaultMessage: 'Process', - } - ), - os: OS.windows, - protectionField: 'process', - }, + defaultMessage: 'Process', + } + ), + protectionField: 'process', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.registry', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.registry', - { - defaultMessage: 'Registry', - } - ), - os: OS.windows, - protectionField: 'registry', - }, + defaultMessage: 'Registry', + } + ), + protectionField: 'registry', + }, + { + name: i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.security', { - name: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetailsConfig.windows.events.security', - { - defaultMessage: 'Security', - } - ), - os: OS.windows, - protectionField: 'security', - }, - ]; - return ( - <> - <ConfigFormHeading>{EVENTS_HEADING}</ConfigFormHeading> - <EuiSpacer size="s" /> - {items.map((item, index) => { - return ( - <EventsCheckbox - name={item.name} - key={index} - data-test-subj={`policyWindowsEvent_${item.protectionField}`} - setter={(config, checked) => - setIn(config)(item.os)('events')(item.protectionField)(checked) - } - getter={(config) => getIn(config)(item.os)('events')(item.protectionField)} - /> - ); - })} - </> - ); - }, []); + defaultMessage: 'Security', + } + ), + protectionField: 'security', + }, +]; + +export const WindowsEvents = memo(() => { + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const dispatch = useDispatch(); return ( - <ConfigForm - type={EVENTS_FORM_TYPE_LABEL} - supportedOss={[OperatingSystem.WINDOWS]} - dataTestSubj="windowsEventingForm" - rightCorner={ - <EuiText size="s" color="subdued"> - {COLLECTIONS_ENABLED_MESSAGE(selected, total)} - </EuiText> + <EventsForm<OperatingSystem.WINDOWS> + os={OperatingSystem.WINDOWS} + selection={policyDetailsConfig.windows.events} + options={OPTIONS} + onValueSelection={(value, selected) => + dispatch({ + type: 'userChangedPolicyConfig', + payload: { + policyConfig: setIn(policyDetailsConfig)('windows')('events')(value)(selected), + }, + }) } - > - {checkboxes} - </ConfigForm> + /> ); }); + +WindowsEvents.displayName = 'WindowsEvents'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap index 71b103949a80a..eb3a8f2132e7f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap @@ -138,7 +138,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -154,7 +154,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -170,7 +170,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -180,7 +180,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -196,7 +196,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -390,7 +390,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -406,7 +406,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -422,7 +422,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -432,7 +432,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -448,7 +448,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -642,7 +642,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -658,7 +658,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -674,7 +674,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -684,7 +684,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -700,7 +700,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -894,7 +894,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -910,7 +910,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -926,7 +926,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -936,7 +936,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -952,7 +952,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1146,7 +1146,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1162,7 +1162,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1178,7 +1178,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -1188,7 +1188,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1204,7 +1204,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1398,7 +1398,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1414,7 +1414,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1430,7 +1430,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -1440,7 +1440,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1456,7 +1456,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1650,7 +1650,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1666,7 +1666,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1682,7 +1682,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -1692,7 +1692,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1708,7 +1708,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1902,7 +1902,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1918,7 +1918,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1934,7 +1934,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -1944,7 +1944,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -1960,7 +1960,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2154,7 +2154,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2170,7 +2170,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2186,7 +2186,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -2196,7 +2196,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2212,7 +2212,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2406,7 +2406,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2422,7 +2422,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2438,7 +2438,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -2448,7 +2448,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2464,7 +2464,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2948,7 +2948,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2964,7 +2964,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -2980,7 +2980,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -2990,7 +2990,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3006,7 +3006,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3200,7 +3200,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3216,7 +3216,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3232,7 +3232,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -3242,7 +3242,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3258,7 +3258,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3452,7 +3452,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3468,7 +3468,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3484,7 +3484,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -3494,7 +3494,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3510,7 +3510,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3704,7 +3704,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3720,7 +3720,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3736,7 +3736,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -3746,7 +3746,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3762,7 +3762,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3956,7 +3956,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3972,7 +3972,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -3988,7 +3988,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -3998,7 +3998,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4014,7 +4014,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4208,7 +4208,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4224,7 +4224,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4240,7 +4240,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -4250,7 +4250,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4266,7 +4266,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4460,7 +4460,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4476,7 +4476,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4492,7 +4492,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -4502,7 +4502,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4518,7 +4518,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4712,7 +4712,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4728,7 +4728,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4744,7 +4744,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -4754,7 +4754,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4770,7 +4770,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4964,7 +4964,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4980,7 +4980,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -4996,7 +4996,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -5006,7 +5006,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5022,7 +5022,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5216,7 +5216,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5232,7 +5232,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5248,7 +5248,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -5258,7 +5258,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5274,7 +5274,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5716,7 +5716,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5732,7 +5732,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5748,7 +5748,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -5758,7 +5758,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5774,7 +5774,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5968,7 +5968,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -5984,7 +5984,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6000,7 +6000,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -6010,7 +6010,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6026,7 +6026,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6220,7 +6220,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6236,7 +6236,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6252,7 +6252,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -6262,7 +6262,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6278,7 +6278,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6472,7 +6472,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6488,7 +6488,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6504,7 +6504,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -6514,7 +6514,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6530,7 +6530,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6724,7 +6724,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6740,7 +6740,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6756,7 +6756,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -6766,7 +6766,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6782,7 +6782,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6976,7 +6976,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -6992,7 +6992,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7008,7 +7008,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -7018,7 +7018,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7034,7 +7034,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7228,7 +7228,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7244,7 +7244,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7260,7 +7260,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -7270,7 +7270,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7286,7 +7286,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7480,7 +7480,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7496,7 +7496,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7512,7 +7512,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -7522,7 +7522,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7538,7 +7538,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7732,7 +7732,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7748,7 +7748,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7764,7 +7764,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -7774,7 +7774,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7790,7 +7790,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -7984,7 +7984,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -8000,7 +8000,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -8016,7 +8016,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -8026,7 +8026,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -8042,7 +8042,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap index 4a8f984b59a01..8f70c61ba4afc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap @@ -793,7 +793,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -809,7 +809,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` OS </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -825,7 +825,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Date Created </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > 1 minute ago </dd> @@ -835,7 +835,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Created By </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" @@ -851,7 +851,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Description </dt> <dd - class="euiDescriptionList__description c2" + class="euiDescriptionList__description c2 eui-textBreakWord" > <span class="euiToolTipAnchor" diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index f97bec65d269a..b81be3249953e 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -59,7 +59,7 @@ import { IndexFieldsStrategyResponse, } from '../common/search_strategy/index_fields'; import { SecurityAppStore } from './common/store/store'; -import { getCaseConnectorUI } from './common/lib/connectors'; +import { getCaseConnectorUI } from './cases/components/connectors'; import { licenseService } from './common/hooks/use_license'; import { LazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx deleted file mode 100644 index 6a65819a764eb..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx +++ /dev/null @@ -1,70 +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 { useCallback, useState, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; - -import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; -import { SecurityPageName } from '../../../../../common/constants'; -import { getTimelineUrl, useFormatUrl } from '../../../../common/components/link_to'; -import { CursorPosition } from '../../../../common/components/markdown_editor'; -import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; -import { setInsertTimeline } from '../../../store/timeline/actions'; - -export const useInsertTimeline = (value: string, onChange: (newValue: string) => void) => { - const dispatch = useDispatch(); - const { formatUrl } = useFormatUrl(SecurityPageName.timelines); - const [cursorPosition, setCursorPosition] = useState<CursorPosition>({ - start: 0, - end: 0, - }); - - const insertTimeline = useShallowEqualSelector(timelineSelectors.selectInsertTimeline); - - const handleOnTimelineChange = useCallback( - (title: string, id: string | null, graphEventId?: string) => { - const url = formatUrl(getTimelineUrl(id ?? '', graphEventId), { - absolute: true, - skipSearch: true, - }); - - const newValue: string = [ - value.slice(0, cursorPosition.start), - cursorPosition.start === cursorPosition.end - ? `[${title}](${url})` - : `[${value.slice(cursorPosition.start, cursorPosition.end)}](${url})`, - value.slice(cursorPosition.end), - ].join(''); - - onChange(newValue); - }, - [value, onChange, cursorPosition, formatUrl] - ); - - const handleCursorChange = useCallback((cp: CursorPosition) => { - setCursorPosition(cp); - }, []); - - // insertTimeline selector is defined to attached a timeline to a case outside of the case page. - // FYI, if you are in the case page we only use handleOnTimelineChange to attach a timeline to a case. - useEffect(() => { - if (insertTimeline != null && value != null) { - dispatch(timelineActions.showTimeline({ id: insertTimeline.timelineId, show: false })); - handleOnTimelineChange( - insertTimeline.timelineTitle, - insertTimeline.timelineSavedObjectId, - insertTimeline.graphEventId - ); - dispatch(setInsertTimeline(null)); - } - }, [insertTimeline, dispatch, handleOnTimelineChange, value]); - - return { - cursorPosition, - handleCursorChange, - handleOnTimelineChange, - }; -}; 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 92e6b9562d970..0a38bdc790b41 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 @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { set } from '@elastic/safer-lodash-set'; import { SignalSourceHit, SignalSearchResponse, @@ -189,9 +190,25 @@ export const sampleDocNoSortId = ( sort: [], }); -export const sampleDocSeverity = ( - severity?: Array<string | number | null> | string | number | null -): SignalSourceHit => ({ +export const sampleDocSeverity = (severity?: unknown, fieldName?: string): SignalSourceHit => { + const doc = { + _index: 'myFakeSignalIndex', + _type: 'doc', + _score: 100, + _version: 1, + _id: sampleIdGuid, + _source: { + someKey: 'someValue', + '@timestamp': '2020-04-20T21:27:45+0000', + }, + sort: [], + }; + + set(doc._source, fieldName ?? 'event.severity', severity); + return doc; +}; + +export const sampleDocRiskScore = (riskScore?: unknown): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, @@ -201,7 +218,7 @@ export const sampleDocSeverity = ( someKey: 'someValue', '@timestamp': '2020-04-20T21:27:45+0000', event: { - severity: severity ?? 100, + risk: riskScore, }, }, sort: [], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts index ff50c2634dfd1..9395085dd6e99 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts @@ -4,23 +4,218 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sampleDocNoSortId } from '../__mocks__/es_results'; -import { buildRiskScoreFromMapping } from './build_risk_score_from_mapping'; +import { + RiskScore, + RiskScoreMappingOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { sampleDocRiskScore } from '../__mocks__/es_results'; +import { + buildRiskScoreFromMapping, + BuildRiskScoreFromMappingReturn, +} from './build_risk_score_from_mapping'; describe('buildRiskScoreFromMapping', () => { beforeEach(() => { jest.clearAllMocks(); }); - test('risk score defaults to provided if mapping is incomplete', () => { - const riskScore = buildRiskScoreFromMapping({ - eventSource: sampleDocNoSortId()._source, - riskScore: 57, - riskScoreMapping: undefined, + describe('base cases: when mapping is undefined', () => { + test('returns the provided default score', () => { + testIt({ + fieldValue: 42, + scoreDefault: 57, + scoreMapping: undefined, + expected: scoreOf(57), + }); }); + }); + + describe('base cases: when mapping to a field of type number', () => { + test(`returns that number if it's integer and within the range [0;100]`, () => { + testIt({ + fieldValue: 42, + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(42), + }); + }); + + test(`returns that number if it's float and within the range [0;100]`, () => { + testIt({ + fieldValue: 3.14, + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(3.14), + }); + }); + + test(`returns default score if the number is < 0`, () => { + testIt({ + fieldValue: -0.0000000000001, + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: scoreOf(57), + }); + }); + + test(`returns default score if the number is > 100`, () => { + testIt({ + fieldValue: 100.0000000000001, + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: scoreOf(57), + }); + }); + }); + + describe('base cases: when mapping to a field of type string', () => { + test(`returns the number casted from string if it's integer and within the range [0;100]`, () => { + testIt({ + fieldValue: '42', + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(42), + }); + }); + + test(`returns the number casted from string if it's float and within the range [0;100]`, () => { + testIt({ + fieldValue: '3.14', + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(3.14), + }); + }); + + test(`returns default score if the "number" is < 0`, () => { + testIt({ + fieldValue: '-1', + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: scoreOf(57), + }); + }); + + test(`returns default score if the "number" is > 100`, () => { + testIt({ + fieldValue: '101', + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: scoreOf(57), + }); + }); + }); - expect(riskScore).toEqual({ riskScore: 57, riskScoreMeta: {} }); + describe('base cases: when mapping to an array of numbers or strings', () => { + test(`returns that number if it's a single element and it's within the range [0;100]`, () => { + testIt({ + fieldValue: [3.14], + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(3.14), + }); + }); + + test(`returns the max number of those that are within the range [0;100]`, () => { + testIt({ + fieldValue: [42, -42, 17, 87, 87.5, '86.5', 110, 66], + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(87.5), + }); + }); + + test(`supports casting strings to numbers`, () => { + testIt({ + fieldValue: [-1, 1, '3', '1.5', '3.14', 2], + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(3.14), + }); + }); }); - // TODO: Enhance... + describe('edge cases: when mapping to a single junk value', () => { + describe('ignores it and returns the default score', () => { + const cases = [ + undefined, + null, + NaN, + Infinity, + -Infinity, + Number.MAX_VALUE, + -Number.MAX_VALUE, + -Number.MIN_VALUE, + 'string', + [], + {}, + new Date(), + ]; + + test.each(cases)('%p', (value) => { + testIt({ + fieldValue: value, + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: scoreOf(57), + }); + }); + }); + }); + + describe('edge cases: when mapping to an array of junk values', () => { + describe('ignores junk, extracts valid numbers and returns the max number within the range [0;100]', () => { + type Case = [unknown[], number]; + const cases: Case[] = [ + [[undefined, null, 1.5, 1, -Infinity], 1.5], + [['42', NaN, '44', '43', 42, {}], 44], + [[Infinity, '101', 100, 99, Number.MIN_VALUE], 100], + [[Number.MIN_VALUE, -0], Number.MIN_VALUE], + ]; + + test.each(cases)('%p', (value, expectedScore) => { + testIt({ + fieldValue: value, + scoreDefault: 57, + scoreMapping: mappingToSingleField(), + expected: overriddenScoreOf(expectedScore), + }); + }); + }); + }); }); + +interface TestCase { + fieldValue: unknown; + scoreDefault: RiskScore; + scoreMapping: RiskScoreMappingOrUndefined; + expected: BuildRiskScoreFromMappingReturn; +} + +function testIt({ fieldValue, scoreDefault, scoreMapping, expected }: TestCase) { + const result = buildRiskScoreFromMapping({ + eventSource: sampleDocRiskScore(fieldValue)._source, + riskScore: scoreDefault, + riskScoreMapping: scoreMapping, + }); + + expect(result).toEqual(expected); +} + +function mappingToSingleField() { + return [{ field: 'event.risk', operator: 'equals' as const, value: '', risk_score: undefined }]; +} + +function scoreOf(value: number) { + return { + riskScore: value, + riskScoreMeta: {}, + }; +} + +function overriddenScoreOf(value: number) { + return { + riskScore: value, + riskScoreMeta: { riskScoreOverridden: true }, + }; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts index c358339e66cd9..cb3fcba102350 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts @@ -11,35 +11,78 @@ import { } from '../../../../../common/detection_engine/schemas/common/schemas'; import { SignalSource } from '../types'; -interface BuildRiskScoreFromMappingProps { +export interface BuildRiskScoreFromMappingProps { eventSource: SignalSource; riskScore: RiskScore; riskScoreMapping: RiskScoreMappingOrUndefined; } -interface BuildRiskScoreFromMappingReturn { +export interface BuildRiskScoreFromMappingReturn { riskScore: RiskScore; riskScoreMeta: Meta; // TODO: Stricter types } +/** + * Calculates the final risk score for a detection alert based on: + * - source event object that can potentially contain fields representing risk score + * - the default score specified by the user + * - (optional) score mapping specified by the user ("map this field to the score") + * + * NOTE: Current MVP support is for mapping from a single field. + */ export const buildRiskScoreFromMapping = ({ eventSource, riskScore, riskScoreMapping, }: BuildRiskScoreFromMappingProps): BuildRiskScoreFromMappingReturn => { - // MVP support is for mapping from a single field - if (riskScoreMapping != null && riskScoreMapping.length > 0) { - const mappedField = riskScoreMapping[0].field; - // TODO: Expand by verifying fieldType from index via doc._index - const mappedValue = get(mappedField, eventSource); - if ( - typeof mappedValue === 'number' && - Number.isSafeInteger(mappedValue) && - mappedValue >= 0 && - mappedValue <= 100 - ) { - return { riskScore: mappedValue, riskScoreMeta: { riskScoreOverridden: true } }; + if (!riskScoreMapping || !riskScoreMapping.length) { + return defaultScore(riskScore); + } + + // TODO: Expand by verifying fieldType from index via doc._index + const eventField = riskScoreMapping[0].field; + const eventValue = get(eventField, eventSource); + const eventValues = Array.isArray(eventValue) ? eventValue : [eventValue]; + + const validNumbers = eventValues.map(toValidNumberOrMinusOne).filter((n) => n > -1); + + if (validNumbers.length > 0) { + const maxNumber = getMaxOf(validNumbers); + return overriddenScore(maxNumber); + } + + return defaultScore(riskScore); +}; + +function toValidNumberOrMinusOne(value: unknown): number { + if (typeof value === 'number' && isValidNumber(value)) { + return value; + } + + if (typeof value === 'string') { + const num = Number(value); + if (isValidNumber(num)) { + return num; } } + + return -1; +} + +function isValidNumber(value: number): boolean { + return Number.isFinite(value) && value >= 0 && value <= 100; +} + +function getMaxOf(array: number[]) { + // NOTE: It's safer to use reduce rather than Math.max(...array). The latter won't handle large input. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max + return array.reduce((a, b) => Math.max(a, b)); +} + +function defaultScore(riskScore: RiskScore): BuildRiskScoreFromMappingReturn { return { riskScore, riskScoreMeta: {} }; -}; +} + +function overriddenScore(riskScore: RiskScore): BuildRiskScoreFromMappingReturn { + return { riskScore, riskScoreMeta: { riskScoreOverridden: true } }; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts index 430564cd985c2..cfb5c56d7cd23 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts @@ -4,63 +4,169 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sampleDocNoSortId, sampleDocSeverity } from '../__mocks__/es_results'; -import { buildSeverityFromMapping } from './build_severity_from_mapping'; +import { + Severity, + SeverityMappingOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { sampleDocSeverity } from '../__mocks__/es_results'; +import { + buildSeverityFromMapping, + BuildSeverityFromMappingReturn, +} from './build_severity_from_mapping'; + +const ECS_FIELD = 'event.severity'; +const ANY_FIELD = 'event.my_custom_severity'; describe('buildSeverityFromMapping', () => { beforeEach(() => { jest.clearAllMocks(); }); - test('severity defaults to provided if mapping is undefined', () => { - const severity = buildSeverityFromMapping({ - eventSource: sampleDocNoSortId()._source, - severity: 'low', - severityMapping: undefined, + describe('base cases: when mapping is undefined', () => { + test('returns the provided default severity', () => { + testIt({ + fieldValue: 23, + severityDefault: 'low', + severityMapping: undefined, + expected: severityOf('low'), + }); + }); + }); + + describe('base cases: when mapping to the "event.severity" field from ECS', () => { + test(`severity is overridden if there's a match to a number`, () => { + testIt({ + fieldValue: 23, + severityDefault: 'low', + severityMapping: [ + { field: ECS_FIELD, operator: 'equals', value: '13', severity: 'low' }, + { field: ECS_FIELD, operator: 'equals', value: '23', severity: 'medium' }, + { field: ECS_FIELD, operator: 'equals', value: '33', severity: 'high' }, + { field: ECS_FIELD, operator: 'equals', value: '43', severity: 'critical' }, + ], + expected: overriddenSeverityOf('medium'), + }); }); - expect(severity).toEqual({ severity: 'low', severityMeta: {} }); + test(`returns the default severity if there's a match to a string (ignores strings)`, () => { + testIt({ + fieldValue: 'hackerman', + severityDefault: 'low', + severityMapping: [ + { field: ECS_FIELD, operator: 'equals', value: 'hackerman', severity: 'critical' }, + ], + expected: severityOf('low'), + }); + }); }); - test('severity is overridden to highest matched mapping', () => { - const severity = buildSeverityFromMapping({ - eventSource: sampleDocSeverity(23)._source, - severity: 'low', - severityMapping: [ - { field: 'event.severity', operator: 'equals', value: '23', severity: 'critical' }, - { field: 'event.severity', operator: 'equals', value: '23', severity: 'low' }, - { field: 'event.severity', operator: 'equals', value: '11', severity: 'critical' }, - { field: 'event.severity', operator: 'equals', value: '23', severity: 'medium' }, - ], + describe('base cases: when mapping to any other field containing a single value', () => { + test(`severity is overridden if there's a match to a number`, () => { + testIt({ + fieldName: ANY_FIELD, + fieldValue: 23, + severityDefault: 'low', + severityMapping: [ + { field: ANY_FIELD, operator: 'equals', value: '13', severity: 'low' }, + { field: ANY_FIELD, operator: 'equals', value: '23', severity: 'medium' }, + { field: ANY_FIELD, operator: 'equals', value: '33', severity: 'high' }, + { field: ANY_FIELD, operator: 'equals', value: '43', severity: 'critical' }, + ], + expected: overriddenSeverityOf('medium', ANY_FIELD), + }); }); - expect(severity).toEqual({ - severity: 'critical', - severityMeta: { - severityOverrideField: 'event.severity', - }, + test(`severity is overridden if there's a match to a string`, () => { + testIt({ + fieldName: ANY_FIELD, + fieldValue: 'hackerman', + severityDefault: 'low', + severityMapping: [ + { field: ANY_FIELD, operator: 'equals', value: 'anything', severity: 'medium' }, + { field: ANY_FIELD, operator: 'equals', value: 'hackerman', severity: 'critical' }, + ], + expected: overriddenSeverityOf('critical', ANY_FIELD), + }); }); }); - test('severity is overridden when field is event.severity and source value is number', () => { - const severity = buildSeverityFromMapping({ - eventSource: sampleDocSeverity(23)._source, - severity: 'low', - severityMapping: [ - { field: 'event.severity', operator: 'equals', value: '13', severity: 'low' }, - { field: 'event.severity', operator: 'equals', value: '23', severity: 'medium' }, - { field: 'event.severity', operator: 'equals', value: '33', severity: 'high' }, - { field: 'event.severity', operator: 'equals', value: '43', severity: 'critical' }, - ], + describe('base cases: when mapping to an array', () => { + test(`severity is overridden to highest matched mapping (works for "event.severity" field)`, () => { + testIt({ + fieldValue: [23, 'some string', 43, 33], + severityDefault: 'low', + severityMapping: [ + { field: ECS_FIELD, operator: 'equals', value: '13', severity: 'low' }, + { field: ECS_FIELD, operator: 'equals', value: '23', severity: 'medium' }, + { field: ECS_FIELD, operator: 'equals', value: '33', severity: 'high' }, + { field: ECS_FIELD, operator: 'equals', value: '43', severity: 'critical' }, + ], + expected: overriddenSeverityOf('critical'), + }); }); - expect(severity).toEqual({ - severity: 'medium', - severityMeta: { - severityOverrideField: 'event.severity', - }, + test(`severity is overridden to highest matched mapping (works for any custom field)`, () => { + testIt({ + fieldName: ANY_FIELD, + fieldValue: ['foo', 'bar', 'baz', 'boo'], + severityDefault: 'low', + severityMapping: [ + { field: ANY_FIELD, operator: 'equals', value: 'bar', severity: 'high' }, + { field: ANY_FIELD, operator: 'equals', value: 'baz', severity: 'critical' }, + { field: ANY_FIELD, operator: 'equals', value: 'foo', severity: 'low' }, + { field: ANY_FIELD, operator: 'equals', value: 'boo', severity: 'medium' }, + ], + expected: overriddenSeverityOf('critical', ANY_FIELD), + }); }); }); - // TODO: Enhance... + describe('edge cases: when mapping the same numerical value to different severities multiple times', () => { + test('severity is overridden to highest matched mapping', () => { + testIt({ + fieldValue: 23, + severityDefault: 'low', + severityMapping: [ + { field: ECS_FIELD, operator: 'equals', value: '23', severity: 'medium' }, + { field: ECS_FIELD, operator: 'equals', value: '23', severity: 'critical' }, + { field: ECS_FIELD, operator: 'equals', value: '23', severity: 'high' }, + ], + expected: overriddenSeverityOf('critical'), + }); + }); + }); }); + +interface TestCase { + fieldName?: string; + fieldValue: unknown; + severityDefault: Severity; + severityMapping: SeverityMappingOrUndefined; + expected: BuildSeverityFromMappingReturn; +} + +function testIt({ fieldName, fieldValue, severityDefault, severityMapping, expected }: TestCase) { + const result = buildSeverityFromMapping({ + eventSource: sampleDocSeverity(fieldValue, fieldName)._source, + severity: severityDefault, + severityMapping, + }); + + expect(result).toEqual(expected); +} + +function severityOf(value: Severity) { + return { + severity: value, + severityMeta: {}, + }; +} + +function overriddenSeverityOf(value: Severity, field = ECS_FIELD) { + return { + severity: value, + severityMeta: { + severityOverrideField: field, + }, + }; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts index 52ebd67f257af..1560bbb48f0ba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts @@ -11,15 +11,16 @@ import { severity as SeverityIOTS, SeverityMappingOrUndefined, } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { SearchTypes } from '../../../../../common/detection_engine/types'; import { SignalSource } from '../types'; -interface BuildSeverityFromMappingProps { +export interface BuildSeverityFromMappingProps { eventSource: SignalSource; severity: Severity; severityMapping: SeverityMappingOrUndefined; } -interface BuildSeverityFromMappingReturn { +export interface BuildSeverityFromMappingReturn { severity: Severity; severityMeta: Meta; // TODO: Stricter types } @@ -31,41 +32,89 @@ const severitySortMapping = { critical: 3, }; +const ECS_SEVERITY_FIELD = 'event.severity'; + export const buildSeverityFromMapping = ({ eventSource, severity, severityMapping, }: BuildSeverityFromMappingProps): BuildSeverityFromMappingReturn => { - if (severityMapping != null && severityMapping.length > 0) { - let severityMatch: SeverityMappingItem | undefined; - - // Sort the SeverityMapping from low to high, so last match (highest severity) is used - const severityMappingSorted = severityMapping.sort( - (a, b) => severitySortMapping[a.severity] - severitySortMapping[b.severity] - ); - - severityMappingSorted.forEach((mapping) => { - const docValue = get(mapping.field, eventSource); - // TODO: Expand by verifying fieldType from index via doc._index - // Till then, explicit parsing of event.severity (long) to number. If not ECS, this could be - // another datatype, but until we can lookup datatype we must assume number for the Elastic - // Endpoint Security rule to function correctly - let parsedMappingValue: string | number = mapping.value; - if (mapping.field === 'event.severity') { - parsedMappingValue = Math.floor(Number(parsedMappingValue)); - } - - if (parsedMappingValue === docValue) { - severityMatch = { ...mapping }; - } - }); - - if (severityMatch != null && SeverityIOTS.is(severityMatch.severity)) { - return { - severity: severityMatch.severity, - severityMeta: { severityOverrideField: severityMatch.field }, - }; + if (!severityMapping || !severityMapping.length) { + return defaultSeverity(severity); + } + + let severityMatch: SeverityMappingItem | undefined; + + // Sort the SeverityMapping from low to high, so last match (highest severity) is used + const severityMappingSorted = severityMapping.sort( + (a, b) => severitySortMapping[a.severity] - severitySortMapping[b.severity] + ); + + severityMappingSorted.forEach((mapping) => { + const mappingField = mapping.field; + const mappingValue = mapping.value; + const eventValue = get(mappingField, eventSource); + + const normalizedEventValues = normalizeEventValue(mappingField, eventValue); + const normalizedMappingValue = normalizeMappingValue(mappingField, mappingValue); + + if (normalizedEventValues.has(normalizedMappingValue)) { + severityMatch = { ...mapping }; } + }); + + if (severityMatch != null && SeverityIOTS.is(severityMatch.severity)) { + return overriddenSeverity(severityMatch.severity, severityMatch.field); } - return { severity, severityMeta: {} }; + + return defaultSeverity(severity); }; + +function normalizeMappingValue(eventField: string, mappingValue: string): string | number { + // TODO: Expand by verifying fieldType from index via doc._index + // Till then, explicit parsing of event.severity (long) to number. If not ECS, this could be + // another datatype, but until we can lookup datatype we must assume number for the Elastic + // Endpoint Security rule to function correctly + if (eventField === ECS_SEVERITY_FIELD) { + return Math.floor(Number(mappingValue)); + } + + return mappingValue; +} + +function normalizeEventValue(eventField: string, eventValue: SearchTypes): Set<string | number> { + const eventValues = Array.isArray(eventValue) ? eventValue : [eventValue]; + const validValues = eventValues.filter((v): v is string | number => isValidValue(eventField, v)); + const finalValues = eventField === ECS_SEVERITY_FIELD ? validValues : validValues.map(String); + return new Set(finalValues); +} + +function isValidValue(eventField: string, value: unknown): value is string | number { + return eventField === ECS_SEVERITY_FIELD + ? isValidNumber(value) + : isValidNumber(value) || isValidString(value); +} + +function isValidString(value: unknown): value is string { + return typeof value === 'string'; +} + +function isValidNumber(value: unknown): value is number { + return typeof value === 'number' && Number.isSafeInteger(value); +} + +function defaultSeverity(value: Severity): BuildSeverityFromMappingReturn { + return { + severity: value, + severityMeta: {}, + }; +} + +function overriddenSeverity(value: Severity, field: string): BuildSeverityFromMappingReturn { + return { + severity: value, + severityMeta: { + severityOverrideField: field, + }, + }; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts index 1d6a8227ebc13..f7b1790e127d9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts @@ -22,6 +22,8 @@ export const ruleStatusServiceFactoryMock = async ({ success: jest.fn(), + partialFailure: jest.fn(), + error: jest.fn(), }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts index cde6a506c657d..449ecd11257d7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts @@ -53,6 +53,21 @@ describe('buildRuleStatusAttributes', () => { expect(result.statusDate).toEqual(result.lastSuccessAt); }); + it('returns partial failure fields if "partial failure"', () => { + const result = buildRuleStatusAttributes( + 'partial failure', + 'some indices missing timestamp override field' + ); + expect(result).toEqual({ + status: 'partial failure', + statusDate: expectIsoDateString, + lastSuccessAt: expectIsoDateString, + lastSuccessMessage: 'some indices missing timestamp override field', + }); + + expect(result.statusDate).toEqual(result.lastSuccessAt); + }); + it('returns failure fields if "failed"', () => { const result = buildRuleStatusAttributes('failed', 'failure message'); expect(result).toEqual({ 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 433ad4e2affea..debc329bf40d7 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 @@ -23,6 +23,7 @@ interface Attributes { export interface RuleStatusService { goingToRun: () => Promise<void>; success: (message: string, attributes?: Attributes) => Promise<void>; + partialFailure: (message: string, attributes?: Attributes) => Promise<void>; error: (message: string, attributes?: Attributes) => Promise<void>; } @@ -46,6 +47,13 @@ export const buildRuleStatusAttributes: ( lastSuccessMessage: message, }; } + case 'partial failure': { + return { + ...baseAttributes, + lastSuccessAt: now, + lastSuccessMessage: message, + }; + } case 'failed': { return { ...baseAttributes, @@ -93,6 +101,18 @@ export const ruleStatusServiceFactory = async ({ }); }, + partialFailure: async (message, attributes) => { + const [currentStatus] = await getOrCreateRuleStatuses({ + alertId, + ruleStatusClient, + }); + + await ruleStatusClient.update(currentStatus.id, { + ...currentStatus.attributes, + ...buildRuleStatusAttributes('partial failure', message, attributes), + }); + }, + error: async (message, attributes) => { const ruleStatuses = await getOrCreateRuleStatuses({ alertId, diff --git a/x-pack/plugins/telemetry_collection_xpack/server/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/index.ts index 249d16c331c39..de39089fe0e03 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/index.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'kibana/server'; import { TelemetryCollectionXpackPlugin } from './plugin'; +export { ESLicense } from './telemetry_collection'; + // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. -export function plugin(initializerContext: PluginInitializerContext) { - return new TelemetryCollectionXpackPlugin(initializerContext); +export function plugin() { + return new TelemetryCollectionXpackPlugin(); } diff --git a/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts b/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts index 524b4c5616c73..e6d72f5813163 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts @@ -4,16 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - PluginInitializerContext, - CoreSetup, - CoreStart, - Plugin, - IClusterClient, - SavedObjectsServiceStart, -} from 'kibana/server'; +import { CoreSetup, CoreStart, Plugin } from 'src/core/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; -import { getClusterUuids, getLocalLicense } from '../../../../src/plugins/telemetry/server'; +import { getClusterUuids } from '../../../../src/plugins/telemetry/server'; import { getStatsWithXpack } from './telemetry_collection'; interface TelemetryCollectionXpackDepsSetup { @@ -21,25 +14,16 @@ interface TelemetryCollectionXpackDepsSetup { } export class TelemetryCollectionXpackPlugin implements Plugin { - private elasticsearchClient?: IClusterClient; - private savedObjectsService?: SavedObjectsServiceStart; - constructor(initializerContext: PluginInitializerContext) {} + constructor() {} public setup(core: CoreSetup, { telemetryCollectionManager }: TelemetryCollectionXpackDepsSetup) { - telemetryCollectionManager.setCollection({ - esCluster: core.elasticsearch.legacy.client, - esClientGetter: () => this.elasticsearchClient, - soServiceGetter: () => this.savedObjectsService, + telemetryCollectionManager.setCollectionStrategy({ title: 'local_xpack', priority: 1, statsGetter: getStatsWithXpack, clusterDetailsGetter: getClusterUuids, - licenseGetter: getLocalLicense, }); } - public start(core: CoreStart) { - this.elasticsearchClient = core.elasticsearch.client; - this.savedObjectsService = core.savedObjects; - } + public start(core: CoreStart) {} } diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap index b68186c0c343d..836b5276615ef 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap @@ -104,6 +104,9 @@ Object { }, "cluster_uuid": "test", "collection": "local", + "license": Object { + "type": "basic", + }, "stack_stats": Object { "data": Array [], "kibana": Object { @@ -183,6 +186,9 @@ Object { }, "cluster_uuid": "test", "collection": "local", + "license": Object { + "type": "basic", + }, "stack_stats": Object { "data": Array [], "kibana": Object { diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.test.ts new file mode 100644 index 0000000000000..c5c6832aa84ac --- /dev/null +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.test.ts @@ -0,0 +1,82 @@ +/* + * 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 { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { getLicenseFromLocalOrMaster } from './get_license'; + +describe('getLicenseFromLocalOrMaster', () => { + test('return an undefined license if it fails to get the license on the first attempt and it does not have a cached license yet', async () => { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + // The local fetch fails + esClient.license.get.mockRejectedValue(new Error('Something went terribly wrong')); + + const license = await getLicenseFromLocalOrMaster(esClient); + + expect(license).toBeUndefined(); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledTimes(1); + }); + + test('returns the license it fetches from Elasticsearch', async () => { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + // The local fetch succeeds + esClient.license.get.mockResolvedValue({ body: { license: { type: 'basic' } } } as any); + + const license = await getLicenseFromLocalOrMaster(esClient); + + expect(license).toStrictEqual({ type: 'basic' }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledTimes(1); + }); + + test('after the first successful attempt, if the local request fails, it will try with the master request (failed case)', async () => { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const error = new Error('Something went terribly wrong'); + // The requests fail with an error + esClient.license.get.mockRejectedValue(error); + + await expect(getLicenseFromLocalOrMaster(esClient)).rejects.toStrictEqual(error); + + expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: false, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledTimes(2); + }); + + test('after the first successful attempt, if the local request fails, it will try with the master request (success case)', async () => { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + // The local fetch fails + esClient.license.get.mockRejectedValueOnce(new Error('Something went terribly wrong')); + // The master fetch succeeds + esClient.license.get.mockResolvedValue({ body: { license: { type: 'basic' } } } as any); + + const license = await getLicenseFromLocalOrMaster(esClient); + + expect(license).toStrictEqual({ type: 'basic' }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: false, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledTimes(2); + }); + + test('after the first successful attempt, if the local request fails, it will try with the master request (clearing cached license)', async () => { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + // The requests fail with 400 + esClient.license.get.mockRejectedValue({ statusCode: 400 }); + + // First attempt goes through 2 requests: local and master + const license = await getLicenseFromLocalOrMaster(esClient); + + expect(license).toBeUndefined(); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: false, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledTimes(2); + + // Now the cached license is cleared, next request only goes for local and gives up when failed + esClient.license.get.mockClear(); + await expect(getLicenseFromLocalOrMaster(esClient)).resolves.toBeUndefined(); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_license.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts similarity index 50% rename from src/plugins/telemetry/server/telemetry_collection/get_local_license.ts rename to x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts index 879416cda62fc..9ffbf5d1bf6d7 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_license.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts @@ -1,29 +1,30 @@ /* - * 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. + * 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 { ESLicense, LicenseGetter } from 'src/plugins/telemetry_collection_manager/server'; import { ElasticsearchClient } from 'src/core/server'; +// From https://www.elastic.co/guide/en/elasticsearch/reference/current/get-license.html +export interface ESLicense { + status: string; + uid: string; + type: string; + issue_date: string; + issue_date_in_millis: number; + expiry_date: string; + expirty_date_in_millis: number; + max_nodes: number; + issued_to: string; + issuer: string; + start_date_in_millis: number; +} + let cachedLicense: ESLicense | undefined; async function fetchLicense(esClient: ElasticsearchClient, local: boolean) { - const { body } = await esClient.license.get({ + const { body } = await esClient.license.get<{ license: ESLicense }>({ local, // For versions >= 7.6 and < 8.0, this flag is needed otherwise 'platinum' is returned for 'enterprise' license. accept_enterprise: true, @@ -39,7 +40,7 @@ async function fetchLicense(esClient: ElasticsearchClient, local: boolean) { * * In OSS we'll get a 400 response using the new elasticsearch client. */ -async function getLicenseFromLocalOrMaster(esClient: ElasticsearchClient) { +export async function getLicenseFromLocalOrMaster(esClient: ElasticsearchClient) { // Fetching the local license is cheaper than getting it from the master node and good enough const { license } = await fetchLicense(esClient, true).catch(async (err) => { if (cachedLicense) { @@ -64,9 +65,3 @@ async function getLicenseFromLocalOrMaster(esClient: ElasticsearchClient) { } return license; } - -export const getLocalLicense: LicenseGetter = async (clustersDetails, { esClient }) => { - const license = await getLicenseFromLocalOrMaster(esClient); - // It should be called only with 1 cluster element in the clustersDetails array, but doing reduce just in case. - return clustersDetails.reduce((acc, { clusterUuid }) => ({ ...acc, [clusterUuid]: license }), {}); -}; diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts index c0e55274b08df..bf1e7c3aaae17 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts @@ -4,33 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ +import { StatsGetter } from 'src/plugins/telemetry_collection_manager/server'; import { TelemetryLocalStats, getLocalStats } from '../../../../../src/plugins/telemetry/server'; -import { StatsGetter } from '../../../../../src/plugins/telemetry_collection_manager/server'; import { getXPackUsage } from './get_xpack'; +import { ESLicense, getLicenseFromLocalOrMaster } from './get_license'; export type TelemetryAggregatedStats = TelemetryLocalStats & { stack_stats: { xpack?: object }; + license?: ESLicense; }; -export const getStatsWithXpack: StatsGetter<{}, TelemetryAggregatedStats> = async function ( +export const getStatsWithXpack: StatsGetter<TelemetryAggregatedStats> = async function ( clustersDetails, config, context ) { const { esClient } = config; - const clustersLocalStats = await getLocalStats(clustersDetails, config, context); - const xpack = await getXPackUsage(esClient).catch(() => undefined); // We want to still report something (and do not lose the license) even when this method fails. + const [clustersLocalStats, license, xpack] = await Promise.all([ + getLocalStats(clustersDetails, config, context), + getLicenseFromLocalOrMaster(esClient), + getXPackUsage(esClient).catch(() => undefined), // We want to still report something (and do not lose the license) even when this method fails. + ]); return clustersLocalStats .map((localStats) => { + const localStatsWithLicense: TelemetryAggregatedStats = { + ...localStats, + ...(license && { license }), + }; if (xpack) { return { - ...localStats, - stack_stats: { ...localStats.stack_stats, xpack }, + ...localStatsWithLicense, + stack_stats: { ...localStatsWithLicense.stack_stats, xpack }, }; } - return localStats; + return localStatsWithLicense; }) .reduce((acc, stats) => { // Concatenate the telemetry reported via monitoring as additional payloads instead of reporting it inside of stack_stats.kibana.plugins.monitoringTelemetry diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts index 553f8dc0c4188..bcd011ae750a6 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +export { ESLicense } from './get_license'; export { getStatsWithXpack } from './get_stats_with_xpack'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 261458a8e7793..4991c4d16099c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4311,6 +4311,8 @@ "visTypeVega.vegaParser.baseView.externalUrlsAreNotEnabledErrorMessage": "外部 URL が無効です。{enableExternalUrls} を {kibanaConfigFileName} に追加します", "visTypeVega.vegaParser.baseView.functionIsNotDefinedForGraphErrorMessage": "このグラフには {funcName} が定義されていません", "visTypeVega.vegaParser.baseView.timeValuesTypeErrorMessage": "時間フィルターの設定エラー: 両方の時間の値は相対的または絶対的な日付である必要があります。 {start}、{end}", + "visTypeVega.vegaParser.baseView.indexNotFoundErrorMessage": "インデックス {index} が見つかりません", + "visTypeVega.vegaParser.baseView.unableToFindDefaultIndexErrorMessage": "デフォルトのインデックスが見つかりません", "visTypeVega.vegaParser.centerOnMarkConfigValueTypeErrorMessage": "{configName} は {trueValue}、{falseValue}、または数字でなければなりません", "visTypeVega.vegaParser.dataExceedsSomeParamsUseTimesLimitErrorMessage": "データには {urlParam}、{valuesParam}、 {sourceParam} の内複数を含めることができません", "visTypeVega.vegaParser.hostConfigIsDeprecatedWarningMessage": "{deprecatedConfigName} は廃止されました。代わりに {newConfigName} を使用してください。", @@ -4329,9 +4331,7 @@ "visTypeVega.vegaParser.unrecognizedControlsLocationValueErrorMessage": "認識されない {controlsLocationParam} 値[{locToDirMap}] のいずれかである必要があります", "visTypeVega.vegaParser.unrecognizedDirValueErrorMessage": "認識されない {dirParam} 値[{expectedValues}] のいずれかである必要があります", "visTypeVega.vegaParser.VLCompilerShouldHaveGeneratedSingleProtectionObjectErrorMessage": "内部エラー:Vega-Lite コンパイラーがシングルプロジェクションオブジェクトを生成したはずです", - "visTypeVega.visualization.indexNotFoundErrorMessage": "インデックス {index} が見つかりません", "visTypeVega.visualization.renderErrorTitle": "Vega エラー", - "visTypeVega.visualization.unableToFindDefaultIndexErrorMessage": "デフォルトのインデックスが見つかりません", "visTypeVega.visualization.unableToRenderWithoutDataWarningMessage": "データなしにはレンダリングできません", "visTypeVislib.advancedSettings.visualization.dimmingOpacityText": "チャートの別のエレメントが選択された時に暗くなるチャート項目の透明度です。この数字が小さければ小さいほど、ハイライトされたエレメントが目立ちます。0と1の間の数字で設定します。", "visTypeVislib.advancedSettings.visualization.dimmingOpacityTitle": "減光透明度", @@ -7175,16 +7175,10 @@ "xpack.fleet.agentEnrollment.stepRunAgentDescription": "エージェントのディレクトリから、このコマンドを実行し、Elasticエージェントを、インストール、登録、起動します。このコマンドを再利用すると、複数のホストでエージェントを設定できます。管理者権限が必要です。", "xpack.fleet.agentEnrollment.stepRunAgentTitle": "エージェントの起動", "xpack.fleet.agentHealth.checkInTooltipText": "前回のチェックイン {lastCheckIn}", - "xpack.fleet.agentHealth.degradedStatusText": "劣化", - "xpack.fleet.agentHealth.enrollingStatusText": "登録中", - "xpack.fleet.agentHealth.errorStatusText": "エラー", "xpack.fleet.agentHealth.inactiveStatusText": "非アクティブ", "xpack.fleet.agentHealth.noCheckInTooltipText": "チェックインしない", "xpack.fleet.agentHealth.offlineStatusText": "オフライン", - "xpack.fleet.agentHealth.onlineStatusText": "オンライン", - "xpack.fleet.agentHealth.unenrollingStatusText": "登録解除中", "xpack.fleet.agentHealth.updatingStatusText": "更新中", - "xpack.fleet.agentHealth.warningStatusText": "エラー", "xpack.fleet.agentList.actionsColumnTitle": "アクション", "xpack.fleet.agentList.addButton": "エージェントの追加", "xpack.fleet.agentList.agentUpgradeLabel": "アップグレードが利用可能です", @@ -19606,7 +19600,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField": "説明が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField": "電子メールアドレスまたはユーザー名が必要です", "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField": "プロジェクトキーが必要です", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredTitleTextField": "タイトルが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldHelp": "JIRAは、このアクションを、Kibanaの保存されたオブジェクトのIDに関連付けます。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldLabel": "オブジェクトID(任意)", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel": "親問題を選択", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f18c899f67652..59ab890cfd9db 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4312,6 +4312,8 @@ "visTypeVega.vegaParser.baseView.externalUrlsAreNotEnabledErrorMessage": "未启用外部 URL。将 {enableExternalUrls} 添加到 {kibanaConfigFileName}", "visTypeVega.vegaParser.baseView.functionIsNotDefinedForGraphErrorMessage": "没有为此图表定义 {funcName}", "visTypeVega.vegaParser.baseView.timeValuesTypeErrorMessage": "设置时间筛选时出错:时间值必须为相对日期或绝对日期。{start}、{end}", + "visTypeVega.vegaParser.baseView.indexNotFoundErrorMessage": "找不到索引 {index}", + "visTypeVega.vegaParser.baseView.unableToFindDefaultIndexErrorMessage": "找不到默认索引", "visTypeVega.vegaParser.centerOnMarkConfigValueTypeErrorMessage": "{configName} 应为 {trueValue}、{falseValue} 或数字", "visTypeVega.vegaParser.dataExceedsSomeParamsUseTimesLimitErrorMessage": "数据不得包含 {urlParam}、{valuesParam} 和 {sourceParam} 中的多个值", "visTypeVega.vegaParser.hostConfigIsDeprecatedWarningMessage": "{deprecatedConfigName} 已弃用。请改用 {newConfigName}。", @@ -4331,9 +4333,7 @@ "visTypeVega.vegaParser.unrecognizedDirValueErrorMessage": "{dirParam} 值无法识别。应为 [{expectedValues}] 之一", "visTypeVega.vegaParser.VLCompilerShouldHaveGeneratedSingleProtectionObjectErrorMessage": "内部错误:Vega-Lite 编译器应已生成单个投影对象", "visTypeVega.vegaParser.widthAndHeightParamsAreIgnored": "{widthParam} 和 {heightParam} 参数已忽略,因为 {autoSizeParam} 已启用。将 {autoSizeParam} 设置为 {noneParam} 可禁用", - "visTypeVega.visualization.indexNotFoundErrorMessage": "找不到索引 {index}", "visTypeVega.visualization.renderErrorTitle": "Vega 错误", - "visTypeVega.visualization.unableToFindDefaultIndexErrorMessage": "找不到默认索引", "visTypeVega.visualization.unableToRenderWithoutDataWarningMessage": "没有数据时无法渲染", "visTypeVislib.advancedSettings.visualization.dimmingOpacityText": "突出显示图表的其他元素时变暗图表项的透明度。此数字越低,突出显示的元素越突出。必须是介于 0 和 1 之间的数字。", "visTypeVislib.advancedSettings.visualization.dimmingOpacityTitle": "变暗透明度", @@ -7181,16 +7181,10 @@ "xpack.fleet.agentEnrollment.stepRunAgentDescription": "从代理目录运行此命令,以安装、注册并启动 Elastic 代理。您可以重复使用此命令在多个主机上设置代理。需要管理员权限。", "xpack.fleet.agentEnrollment.stepRunAgentTitle": "启动代理", "xpack.fleet.agentHealth.checkInTooltipText": "上次签入时间 {lastCheckIn}", - "xpack.fleet.agentHealth.degradedStatusText": "已降级", - "xpack.fleet.agentHealth.enrollingStatusText": "正在注册", - "xpack.fleet.agentHealth.errorStatusText": "错误", "xpack.fleet.agentHealth.inactiveStatusText": "非活动", "xpack.fleet.agentHealth.noCheckInTooltipText": "未签入", "xpack.fleet.agentHealth.offlineStatusText": "脱机", - "xpack.fleet.agentHealth.onlineStatusText": "联机", - "xpack.fleet.agentHealth.unenrollingStatusText": "正在取消注册", "xpack.fleet.agentHealth.updatingStatusText": "正在更新", - "xpack.fleet.agentHealth.warningStatusText": "错误", "xpack.fleet.agentList.actionsColumnTitle": "操作", "xpack.fleet.agentList.addButton": "添加代理", "xpack.fleet.agentList.agentUpgradeLabel": "升级可用", @@ -19625,7 +19619,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField": "“描述”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField": "“电子邮件”或“用户名”必填", "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField": "“项目键”必填", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredTitleTextField": "“标题”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldHelp": "JIRA 将此操作与 Kibana 已保存对象的 ID 关联。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.savedObjectIdFieldLabel": "对象 ID(可选)", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel": "选择父问题", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index f476522c2bf5a..b10341fa00f1b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -93,7 +93,7 @@ describe('jira action params validation', () => { expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { - title: ['Title is required.'], + title: ['Summary is required.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx index 81f0bbfe8a02f..20374cfbe3a3b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx @@ -64,8 +64,8 @@ export function getActionType(): ActionTypeModel<JiraConfig, JiraSecrets, JiraAc title: new Array<string>(), }; validationResult.errors = errors; - if (actionParams.subActionParams && !actionParams.subActionParams.title?.length) { - errors.title.push(i18n.TITLE_REQUIRED); + if (!actionParams.subActionParams?.title?.length) { + errors.title.push(i18n.SUMMARY_REQUIRED); } return validationResult; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 6f45316ff4433..c9642da9ba440 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -127,10 +127,10 @@ export const DESCRIPTION_REQUIRED = i18n.translate( } ); -export const TITLE_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredTitleTextField', +export const SUMMARY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredSummaryTextField', { - defaultMessage: 'Title is required.', + defaultMessage: 'Summary is required.', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx index 937fe61e887ea..17e9b42e7878e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx @@ -93,7 +93,7 @@ describe('resilient action params validation', () => { expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { - title: ['Title is required.'], + title: ['Name is required.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx index 6d57fc98fe20f..251274a08ba6c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx @@ -72,8 +72,9 @@ export function getActionType(): ActionTypeModel< title: new Array<string>(), }; validationResult.errors = errors; - if (actionParams.subActionParams && !actionParams.subActionParams.title?.length) { - errors.title.push(i18n.TITLE_REQUIRED); + + if (!actionParams.subActionParams?.title?.length) { + errors.title.push(i18n.NAME_REQUIRED); } return validationResult; }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts index 65d08c9f7de68..7483ba2f461df 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts @@ -134,16 +134,16 @@ export const MAPPING_FIELD_COMMENTS = i18n.translate( ); export const DESCRIPTION_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredDescriptionTextField', + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredDescriptionTextField', { defaultMessage: 'Description is required.', } ); -export const TITLE_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredTitleTextField', +export const NAME_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredNameTextField', { - defaultMessage: 'Title is required.', + defaultMessage: 'Name is required.', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx index 5e70bc20f5c51..c29ddbf385de6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx @@ -90,7 +90,7 @@ describe('servicenow action params validation', () => { expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { - title: ['Title is required.'], + title: ['Short description is required.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx index 9cc689d8f48b1..8eca7f3ef3120 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx @@ -67,7 +67,7 @@ export function getActionType(): ActionTypeModel< title: new Array<string>(), }; validationResult.errors = errors; - if (actionParams.subActionParams && !actionParams.subActionParams.title?.length) { + if (!actionParams.subActionParams?.title?.length) { errors.title.push(i18n.TITLE_REQUIRED); } return validationResult; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts index 312cb9844bd75..91a5c0a54397b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts @@ -157,6 +157,6 @@ export const DESCRIPTION_REQUIRED = i18n.translate( export const TITLE_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredTitleTextField', { - defaultMessage: 'Title is required.', + defaultMessage: 'Short description is required.', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index 7af8e5ba88300..156f65f094342 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -16,10 +16,10 @@ export const routeToConnectors = `/connectors`; export const routeToAlerts = `/alerts`; export const routeToAlertDetails = `/alert/:alertId`; -export const resolvedActionGroupMessage = i18n.translate( - 'xpack.triggersActionsUI.sections.actionForm.ResolvedMessage', +export const recoveredActionGroupMessage = i18n.translate( + 'xpack.triggersActionsUI.sections.actionForm.RecoveredMessage', { - defaultMessage: 'Resolved', + defaultMessage: 'Recovered', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.test.ts index c1f7cbb9fafed..57cc45786b2da 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ResolvedActionGroup } from '../../../../alerts/common'; +import { RecoveredActionGroup } from '../../../../alerts/common'; import { AlertProvidedActionVariables } from './action_variables'; import { getDefaultsForActionParams } from './get_defaults_for_action_params'; @@ -16,8 +16,8 @@ describe('getDefaultsForActionParams', () => { }); }); - test('pagerduty defaults for resolved action group', async () => { - expect(getDefaultsForActionParams('.pagerduty', ResolvedActionGroup.id)).toEqual({ + test('pagerduty defaults for recovered action group', async () => { + expect(getDefaultsForActionParams('.pagerduty', RecoveredActionGroup.id)).toEqual({ dedupKey: `{{${AlertProvidedActionVariables.alertId}}}:{{${AlertProvidedActionVariables.alertInstanceId}}}`, eventAction: 'resolve', }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.ts index c2143553e63c6..36c054977ac30 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/get_defaults_for_action_params.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertActionParam, ResolvedActionGroup } from '../../../../alerts/common'; +import { AlertActionParam, RecoveredActionGroup } from '../../../../alerts/common'; import { AlertProvidedActionVariables } from './action_variables'; export const getDefaultsForActionParams = ( @@ -17,7 +17,7 @@ export const getDefaultsForActionParams = ( dedupKey: `{{${AlertProvidedActionVariables.alertId}}}:{{${AlertProvidedActionVariables.alertInstanceId}}}`, eventAction: 'trigger', }; - if (actionGroupId === ResolvedActionGroup.id) { + if (actionGroupId === RecoveredActionGroup.id) { pagerDutyDefaults.eventAction = 'resolve'; } return pagerDutyDefaults; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 5b56720737b7e..ddbf933078043 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -10,8 +10,9 @@ import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult, Alert, AlertAction } from '../../../types'; import ActionForm from './action_form'; -import { ResolvedActionGroup } from '../../../../../alerts/common'; +import { RecoveredActionGroup } from '../../../../../alerts/common'; import { useKibana } from '../../../common/lib/kibana'; +import { EuiScreenReaderOnly } from '@elastic/eui'; jest.mock('../../../common/lib/kibana'); jest.mock('../../lib/action_connector_api', () => ({ loadAllActions: jest.fn(), @@ -228,7 +229,7 @@ describe('action_form', () => { }} actionGroups={[ { id: 'default', name: 'Default' }, - { id: 'resolved', name: 'Resolved' }, + { id: 'recovered', name: 'Recovered' }, ]} setActionGroupIdByIndex={(group: string, index: number) => { initialAlert.actions[index].group = group; @@ -347,18 +348,18 @@ describe('action_form', () => { "value": "default", }, Object { - "data-test-subj": "addNewActionConnectorActionGroup-0-option-resolved", - "inputDisplay": "Resolved", - "value": "resolved", + "data-test-subj": "addNewActionConnectorActionGroup-0-option-recovered", + "inputDisplay": "Recovered", + "value": "recovered", }, ] `); }); - it('renders selected Resolved action group', async () => { + it('renders selected Recovered action group', async () => { const wrapper = await setup([ { - group: ResolvedActionGroup.id, + group: RecoveredActionGroup.id, id: 'test', actionTypeId: actionType.id, params: { @@ -381,15 +382,17 @@ describe('action_form', () => { "value": "default", }, Object { - "data-test-subj": "addNewActionConnectorActionGroup-0-option-resolved", - "inputDisplay": "Resolved", - "value": "resolved", + "data-test-subj": "addNewActionConnectorActionGroup-0-option-recovered", + "inputDisplay": "Recovered", + "value": "recovered", }, ] `); - expect(actionGroupsSelect.first().text()).toEqual( - 'Select an option: Resolved, is selectedResolved' + + expect(actionGroupsSelect.first().find(EuiScreenReaderOnly).text()).toEqual( + 'Select an option: Recovered, is selected' ); + expect(actionGroupsSelect.first().find('button').first().text()).toEqual('Recovered'); }); it('renders available connectors for the selected action type', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index 7e38805957931..fffc3bd32125e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -26,7 +26,7 @@ import { EuiBadge, EuiErrorBoundary, } from '@elastic/eui'; -import { AlertActionParam, ResolvedActionGroup } from '../../../../../alerts/common'; +import { AlertActionParam, RecoveredActionGroup } from '../../../../../alerts/common'; import { IErrorObject, AlertAction, @@ -40,7 +40,7 @@ import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_en import { hasSaveActionsCapability } from '../../lib/capabilities'; import { ActionAccordionFormProps } from './action_form'; import { transformActionVariables } from '../../lib/action_variables'; -import { resolvedActionGroupMessage } from '../../constants'; +import { recoveredActionGroupMessage } from '../../constants'; import { useKibana } from '../../../common/lib/kibana'; import { getDefaultsForActionParams } from '../../lib/get_defaults_for_action_params'; @@ -105,8 +105,8 @@ export const ActionTypeForm = ({ useEffect(() => { setAvailableActionVariables(getAvailableActionVariables(messageVariables, actionItem.group)); const res = - actionItem.group === ResolvedActionGroup.id - ? resolvedActionGroupMessage + actionItem.group === RecoveredActionGroup.id + ? recoveredActionGroupMessage : defaultActionMessage; setAvailableDefaultActionMessage(res); const paramsDefaults = getDefaultsForActionParams(actionItem.actionTypeId, actionItem.group); @@ -374,7 +374,7 @@ function getAvailableActionVariables( return []; } const filteredActionVariables = - actionGroup === ResolvedActionGroup.id + actionGroup === RecoveredActionGroup.id ? { params: actionVariables.params, state: actionVariables.state } : actionVariables; diff --git a/x-pack/plugins/ui_actions_enhanced/README.md b/x-pack/plugins/ui_actions_enhanced/README.md index a4a37b559ff8d..cd2a34a2f7536 100644 --- a/x-pack/plugins/ui_actions_enhanced/README.md +++ b/x-pack/plugins/ui_actions_enhanced/README.md @@ -3,3 +3,66 @@ Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work. - [__Dashboard drilldown user docs__](https://www.elastic.co/guide/en/kibana/master/drilldowns.html) + +## Dynamic Actions Telemetry + +Dynamic actions (drilldowns) report telemetry. Below is the summary of dynamic action metrics that are reported using telemetry. + +### Dynamic action count + +Total count of dynamic actions (drilldowns) on a saved object. + +``` +dynamicActions.count +``` + +### Count by factory ID + +Count of active dynamic actions (drilldowns) on a saved object by factory ID (drilldown type). + +``` +dynamicActions.actions.<factory_id>.count +``` + +For example: + +``` +dynamicActions.actions.DASHBOARD_TO_DASHBOARD_DRILLDOWN.count +dynamicActions.actions.URL_DRILLDOWN.count +``` + +### Count by trigger + +Count of active dynamic actions (drilldowns) on a saved object by a trigger to which they are attached. + +``` +dynamicActions.triggers.<trigger>.count +``` + +For example: + +``` +dynamicActions.triggers.VALUE_CLICK_TRIGGER.count +dynamicActions.triggers.RANGE_SELECT_TRIGGER.count +``` + +### Count by factory and trigger + +Count of active dynamic actions (drilldowns) on a saved object by a factory ID and trigger ID. + +``` +dynamicActions.action_triggers.<factory_id>_<trigger>.count +``` + +For example: + +``` +dynamicActions.action_triggers.DASHBOARD_TO_DASHBOARD_DRILLDOWN_VALUE_CLICK_TRIGGER.count +dynamicActions.action_triggers.DASHBOARD_TO_DASHBOARD_DRILLDOWN_RANGE_SELECT_TRIGGER.count +dynamicActions.action_triggers.URL_DRILLDOWN_VALUE_CLICK_TRIGGER.count +``` + +### Factory metrics + +Each dynamic action factory (drilldown type) can report its own stats, which is +done using the `.telemetry()` method on dynamic action factories. diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts index 4cea7ddf4854a..16e7e7967838d 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts @@ -8,19 +8,20 @@ import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddabl import { SavedObjectReference } from '../../../../src/core/types'; import { ActionFactory, DynamicActionsState, SerializedEvent } from './types'; import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; +import { dynamicActionsCollector } from './telemetry/dynamic_actions_collector'; +import { dynamicActionFactoriesCollector } from './telemetry/dynamic_action_factories_collector'; export const dynamicActionEnhancement = ( getActionFactory: (id: string) => undefined | ActionFactory ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', - telemetry: (state: SerializableState, telemetry: Record<string, any>) => { - let telemetryData = telemetry; - (state as DynamicActionsState).events.forEach((event: SerializedEvent) => { - const factory = getActionFactory(event.action.factoryId); - if (factory) telemetryData = factory.telemetry(event, telemetryData); - }); - return telemetryData; + telemetry: (serializableState: SerializableState, stats: Record<string, any>) => { + const state = serializableState as DynamicActionsState; + stats = dynamicActionsCollector(state, stats); + stats = dynamicActionFactoriesCollector(getActionFactory, state, stats); + + return stats; }, extract: (state: SerializableState) => { const references: SavedObjectReference[] = []; diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts new file mode 100644 index 0000000000000..9d38fd9d302a4 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts @@ -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. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { dynamicActionFactoriesCollector } from './dynamic_action_factories_collector'; +import { DynamicActionsState } from '../../common'; +import { ActionFactory } from '../types'; + +type GetActionFactory = (id: string) => undefined | ActionFactory; + +const factories: Record<string, ActionFactory> = { + FACTORY_ID_1: ({ + id: 'FACTORY_ID_1', + telemetry: jest.fn((state: DynamicActionsState, stats: Record<string, any>) => { + stats.myStat_1 = 1; + stats.myStat_2 = 123; + return stats; + }), + } as unknown) as ActionFactory, + FACTORY_ID_2: ({ + id: 'FACTORY_ID_2', + telemetry: jest.fn((state: DynamicActionsState, stats: Record<string, any>) => stats), + } as unknown) as ActionFactory, + FACTORY_ID_3: ({ + id: 'FACTORY_ID_3', + telemetry: jest.fn((state: DynamicActionsState, stats: Record<string, any>) => { + stats.myStat_1 = 2; + stats.stringStat = 'abc'; + return stats; + }), + } as unknown) as ActionFactory, +}; + +const getActionFactory: GetActionFactory = (id: string) => factories[id]; + +const state: DynamicActionsState = { + events: [ + { + eventId: 'eventId-1', + triggers: ['TRIGGER_1'], + action: { + factoryId: 'FACTORY_ID_1', + name: 'Click me!', + config: {}, + }, + }, + { + eventId: 'eventId-2', + triggers: ['TRIGGER_2', 'TRIGGER_3'], + action: { + factoryId: 'FACTORY_ID_2', + name: 'Click me, too!', + config: { + doCleanup: true, + }, + }, + }, + { + eventId: 'eventId-3', + triggers: ['TRIGGER_4', 'TRIGGER_1'], + action: { + factoryId: 'FACTORY_ID_3', + name: 'Go to documentation', + config: { + url: 'http://google.com', + iamFeelingLucky: true, + }, + }, + }, + ], +}; + +beforeEach(() => { + Object.values(factories).forEach((factory) => { + ((factory.telemetry as unknown) as jest.SpyInstance).mockClear(); + }); +}); + +describe('dynamicActionFactoriesCollector', () => { + test('returns empty stats when there are not dynamic actions', () => { + const stats = dynamicActionFactoriesCollector( + getActionFactory, + { + events: [], + }, + {} + ); + + expect(stats).toEqual({}); + }); + + test('calls .telemetry() method of a supplied factory', () => { + const currentState = { + events: [state.events[0]], + }; + dynamicActionFactoriesCollector(getActionFactory, currentState, {}); + + const spy1 = (factories.FACTORY_ID_1.telemetry as unknown) as jest.SpyInstance; + const spy2 = (factories.FACTORY_ID_2.telemetry as unknown) as jest.SpyInstance; + + expect(spy1).toHaveBeenCalledTimes(1); + expect(spy2).toHaveBeenCalledTimes(0); + + expect(spy1.mock.calls[0][0]).toEqual(currentState.events[0]); + expect(typeof spy1.mock.calls[0][1]).toBe('object'); + expect(!!spy1.mock.calls[0][1]).toBe(true); + }); + + test('returns stats received from factory', () => { + const currentState = { + events: [state.events[0]], + }; + const stats = dynamicActionFactoriesCollector(getActionFactory, currentState, {}); + + expect(stats).toEqual({ + myStat_1: 1, + myStat_2: 123, + }); + }); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts new file mode 100644 index 0000000000000..2ece6102c27a4 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts @@ -0,0 +1,24 @@ +/* + * 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 { DynamicActionsState } from '../../common'; +import { ActionFactory } from '../types'; + +export const dynamicActionFactoriesCollector = ( + getActionFactory: (id: string) => undefined | ActionFactory, + state: DynamicActionsState, + stats: Record<string, any> +): Record<string, any> => { + for (const event of state.events) { + const factory = getActionFactory(event.action.factoryId); + + if (factory) { + stats = factory.telemetry(event, stats); + } + } + + return stats; +}; diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts new file mode 100644 index 0000000000000..99217cd98fa01 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.test.ts @@ -0,0 +1,271 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { dynamicActionsCollector } from './dynamic_actions_collector'; +import { DynamicActionsState } from '../../common'; + +const state: DynamicActionsState = { + events: [ + { + eventId: 'eventId-1', + triggers: ['TRIGGER_1'], + action: { + factoryId: 'FACTORY_ID_1', + name: 'Click me!', + config: {}, + }, + }, + { + eventId: 'eventId-2', + triggers: ['TRIGGER_2', 'TRIGGER_3'], + action: { + factoryId: 'FACTORY_ID_2', + name: 'Click me, too!', + config: { + doCleanup: true, + }, + }, + }, + { + eventId: 'eventId-3', + triggers: ['TRIGGER_4', 'TRIGGER_1'], + action: { + factoryId: 'FACTORY_ID_1', + name: 'Go to documentation', + config: { + url: 'http://google.com', + iamFeelingLucky: true, + }, + }, + }, + ], +}; + +describe('dynamicActionsCollector', () => { + describe('dynamic action count', () => { + test('equal to zero when there are no dynamic actions', () => { + const stats = dynamicActionsCollector( + { + events: [], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.count': 0, + }); + }); + + test('does not update existing count if there are no dynamic actions', () => { + const stats = dynamicActionsCollector( + { + events: [], + }, + { + 'dynamicActions.count': 25, + } + ); + + expect(stats).toMatchObject({ + 'dynamicActions.count': 25, + }); + }); + + test('equal to one when there is one dynamic action', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.count': 1, + }); + }); + + test('adds one to the current dynamic action count', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + { + 'dynamicActions.count': 2, + } + ); + + expect(stats).toMatchObject({ + 'dynamicActions.count': 3, + }); + }); + + test('equal to three when there are three dynamic action', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0], state.events[1], state.events[2]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.count': 3, + }); + }); + }); + + describe('registered action counts', () => { + test('for single action sets count to one', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.actions.FACTORY_ID_1.count': 1, + }); + }); + + test('adds count to existing action counts', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + { + 'dynamicActions.actions.FACTORY_ID_1.count': 5, + 'dynamicActions.actions.FACTORY_ID_2.count': 1, + } + ); + + expect(stats).toMatchObject({ + 'dynamicActions.actions.FACTORY_ID_1.count': 6, + 'dynamicActions.actions.FACTORY_ID_2.count': 1, + }); + }); + + test('aggregates count factory count', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0], state.events[2]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.actions.FACTORY_ID_1.count': 2, + }); + }); + + test('returns counts for every factory type', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0], state.events[2], state.events[1]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.actions.FACTORY_ID_1.count': 2, + 'dynamicActions.actions.FACTORY_ID_2.count': 1, + }); + }); + }); + + describe('action trigger counts', () => { + test('for single action sets count to one', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.triggers.TRIGGER_1.count': 1, + }); + }); + + test('adds count to existing stats', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + { + 'dynamicActions.triggers.TRIGGER_1.count': 123, + } + ); + + expect(stats).toMatchObject({ + 'dynamicActions.triggers.TRIGGER_1.count': 124, + }); + }); + + test('aggregates trigger counts from all dynamic actions', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0], state.events[2], state.events[1]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.triggers.TRIGGER_1.count': 2, + 'dynamicActions.triggers.TRIGGER_2.count': 1, + 'dynamicActions.triggers.TRIGGER_3.count': 1, + 'dynamicActions.triggers.TRIGGER_4.count': 1, + }); + }); + }); + + describe('action x trigger counts', () => { + test('returns single action (factoryId x trigger) stat', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.action_triggers.FACTORY_ID_1_TRIGGER_1.count': 1, + }); + }); + + test('adds count to existing stats', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0]], + }, + { + 'dynamicActions.action_triggers.FACTORY_ID_1_TRIGGER_1.count': 3, + } + ); + + expect(stats).toMatchObject({ + 'dynamicActions.action_triggers.FACTORY_ID_1_TRIGGER_1.count': 4, + }); + }); + + test('aggregates actions x triggers counts for all events', () => { + const stats = dynamicActionsCollector( + { + events: [state.events[0], state.events[2], state.events[1]], + }, + {} + ); + + expect(stats).toMatchObject({ + 'dynamicActions.action_triggers.FACTORY_ID_1_TRIGGER_1.count': 2, + 'dynamicActions.action_triggers.FACTORY_ID_2_TRIGGER_2.count': 1, + 'dynamicActions.action_triggers.FACTORY_ID_2_TRIGGER_3.count': 1, + 'dynamicActions.action_triggers.FACTORY_ID_1_TRIGGER_4.count': 1, + }); + }); + }); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts new file mode 100644 index 0000000000000..ae595776fda58 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DynamicActionsState } from '../../common'; +import { getMetricKey } from './get_metric_key'; + +export const dynamicActionsCollector = ( + state: DynamicActionsState, + stats: Record<string, any> +): Record<string, any> => { + const countMetricKey = getMetricKey('count'); + + stats[countMetricKey] = state.events.length + (stats[countMetricKey] || 0); + + for (const event of state.events) { + const factoryId = event.action.factoryId; + const actionCountMetric = getMetricKey(`actions.${factoryId}.count`); + + stats[actionCountMetric] = 1 + (stats[actionCountMetric] || 0); + + for (const trigger of event.triggers) { + const triggerCountMetric = getMetricKey(`triggers.${trigger}.count`); + const actionXTriggerCountMetric = getMetricKey( + `action_triggers.${factoryId}_${trigger}.count` + ); + + stats[triggerCountMetric] = 1 + (stats[triggerCountMetric] || 0); + stats[actionXTriggerCountMetric] = 1 + (stats[actionXTriggerCountMetric] || 0); + } + } + + return stats; +}; diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.ts new file mode 100644 index 0000000000000..6d3ae370c5200 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/get_metric_key.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. + */ + +const prefix = 'dynamicActions.'; + +/** Returns prefixed telemetry metric key for all dynamic action metrics. */ +export const getMetricKey = (path: string) => `${prefix}${path}`; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts index 703351c45ba5a..af55cc9968b14 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts @@ -29,17 +29,17 @@ describe('Upgrade Assistant Telemetry SavedObject UIOpen', () => { expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_open.overview` + [`ui_open.overview`] ); expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_open.cluster` + [`ui_open.cluster`] ); expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_open.indices` + [`ui_open.indices`] ); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts index 64e9b0f217555..45cae937fb466 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts @@ -23,11 +23,9 @@ async function incrementUIOpenOptionCounter({ }: IncrementUIOpenDependencies) { const internalRepository = savedObjects.createInternalRepository(); - await internalRepository.incrementCounter( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - `ui_open.${uiOpenOptionCounter}` - ); + await internalRepository.incrementCounter(UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, [ + `ui_open.${uiOpenOptionCounter}`, + ]); } type UpsertUIOpenOptionDependencies = UIOpen & { savedObjects: SavedObjectsServiceStart }; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts index 31e4e3f07b5de..c157d8860de12 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts @@ -28,22 +28,22 @@ describe('Upgrade Assistant Telemetry SavedObject UIReindex', () => { expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.close` + [`ui_reindex.close`] ); expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.open` + [`ui_reindex.open`] ); expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.start` + [`ui_reindex.start`] ); expect(internalRepo.incrementCounter).toHaveBeenCalledWith( UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.stop` + [`ui_reindex.stop`] ); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts index 0aaaf63196d67..4c57b586a46cd 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts @@ -23,11 +23,9 @@ async function incrementUIReindexOptionCounter({ }: IncrementUIReindexOptionDependencies) { const internalRepository = savedObjects.createInternalRepository(); - await internalRepository.incrementCounter( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - `ui_reindex.${uiReindexOptionCounter}` - ); + await internalRepository.incrementCounter(UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, [ + `ui_reindex.${uiReindexOptionCounter}`, + ]); } type UpsertUIReindexOptionDepencies = UIReindex & { savedObjects: SavedObjectsServiceStart }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx index 9a3e045017f9a..0c47e4c73e976 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx @@ -57,29 +57,31 @@ interface ExecutedJourneyProps { journey: JourneyState; } -export const ExecutedJourney: FC<ExecutedJourneyProps> = ({ journey }) => ( - <div> - <EuiText> - <h3> - <FormattedMessage - id="xpack.uptime.synthetics.executedJourney.heading" - defaultMessage="Summary information" - /> - </h3> - <p> - {statusMessage( - journey.steps - .filter(isStepEnd) - .reduce(reduceStepStatus, { failed: 0, skipped: 0, succeeded: 0 }) - )} - </p> - </EuiText> - <EuiSpacer /> - <EuiFlexGroup direction="column"> - {journey.steps.filter(isStepEnd).map((step, index) => ( - <ExecutedStep key={index} index={index} step={step} /> - ))} - <EuiSpacer size="s" /> - </EuiFlexGroup> - </div> -); +export const ExecutedJourney: FC<ExecutedJourneyProps> = ({ journey }) => { + return ( + <div> + <EuiText> + <h3> + <FormattedMessage + id="xpack.uptime.synthetics.executedJourney.heading" + defaultMessage="Summary information" + /> + </h3> + <p> + {statusMessage( + journey.steps + .filter(isStepEnd) + .reduce(reduceStepStatus, { failed: 0, skipped: 0, succeeded: 0 }) + )} + </p> + </EuiText> + <EuiSpacer /> + <EuiFlexGroup direction="column"> + {journey.steps.filter(isStepEnd).map((step, index) => ( + <ExecutedStep key={index} index={index} step={step} /> + ))} + <EuiSpacer size="s" /> + </EuiFlexGroup> + </div> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/README.md b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/README.md new file mode 100644 index 0000000000000..cf8d3b5345eaa --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/README.md @@ -0,0 +1,123 @@ +# Waterfall chart + +## Introduction + +The waterfall chart component aims to be agnostic in it's approach, so that a variety of consumers / solutions can use it. Some of Elastic Chart's features are used in a non-standard way to facilitate this flexibility, this README aims to cover some of the things that might be less obvious, and also provides a high level overview of implementation. + +## Requirements for usage + +The waterfall chart component asssumes that the consumer is making use of `KibanaReactContext`, and as such things like `useKibana` can be called. + +Consumers are also expected to be using the `<EuiThemeProvider />` so that the waterfall chart can apply styled-component styles based on the EUI theme. + +These are the two hard requirements, but almost all plugins will be using these. + +## Rendering + +At it's core the watefall chart is a stacked bar chart that has been rotated through 90 degrees. As such it's important to understand that `x` is now represented as `y` and vice versa. + +## Flexibility + +This section aims to cover some things that are non-standard. + +### Tooltip + +By default the formatting of tooltip values is very basic, but for a waterfall chart there needs to be a great deal of flexibility to represent whatever breakdown you're trying to show. + +As such a custom tooltip component is used. This custom component would usually only have access to some basic props that pertain to the values of the hovered bar. The waterfall chart component extends this by making us of a waterfall chart context. + +The custom tooltip component can use the context to access the full set of chart data, find the relevant items (those with the same `x` value) and call a custom `renderTooltipItem` for each item, `renderTooltipItem` will be passed `item.config.tooltipProps`. Every consumer can choose what they use for their `tooltipProps`. + +Some consumers might need colours, some might need iconography and so on. The waterfall chart doesn't make assumptions, and will render out the React content returned by `renderTooltipItem`. + +IMPORTANT: `renderTooltipItem` is provided via context and not as a direct prop due to the fact the custom tooltip component would usually only have access to the props provided directly to it from Elastic Charts. + +### Colours + +The easiest way to facilitate specific colours for each stack (let's say your colours are mapped to a constraint like mime type) is to assign the colour directly on your datum `config` property, and then access this directly in the `barStyleAccessor` function, e.g. + +``` +barStyleAccessor={(datum) => { + return datum.datum.config.colour; +}) +``` + +### Config + +The notion of `config` has been mentioned already. But this is a place that consumers can store their solution specific properties. `renderTooltipItem` will make use of `config.tooltipProps`, and `barStyleAccessor` can make use of anything on `config`. + +### Sticky top axis + +By default there is no "sticky" axis functionality in Elastic Charts, therefore a second chart is rendered, this contains a replica of the top axis, and renders one empty data point (as a chart can't only have an axis). This second chart is then positioned in such a way that it covers the top of the real axis, and remains fixed. + +## Data + +The waterfall chart expects data in a relatively simple format, there are the usual plot properties (`x`, `y0`, and `y`) and then `config`. E.g. + +``` +const series = [ + {x: 0, y: 0, y: 100, config: { tooltipProps: { type: 'dns' }}}, + {x: 0, y0: 300, y: 500, config: { tooltipProps: { type: 'ssl' }}}, + {x: 1, y0: 250, y: 300, config: { tooltipProps: { propA: 'somethingBreakdownRelated' }}}, + {x: 1, y0: 500, y: 600, config: { tooltipProps: { propA: 'anotherBreakdown' }}}, +] +``` + +Gaps in bars are fine, and to be expected for certain solutions. + +## Sidebar items + +The waterfall chart component again doesn't make assumptions about consumer's sidebar items' content, but the waterfall chart does handle the rendering so the sidebar can be aligned and rendered properly alongside the chart itself. + +`sidebarItems` should be provided to the context, and a `renderSidebarItem` prop should be provided to the chart. + +A sidebar is optional. + +There is a great deal of flexibility here so that solutions can make use of this in the way they need. For example, if you'd like to add a toggle functionality, so that clicking an item shows / hides it's children, this would involve rendering your toggle in `renderSidebarItem` and then when clicked you can handle adjusting your data as necessary. + +IMPORTANT: It is important to understand that the chart itself makes use of a fixed height. The sidebar will create a space that has a matching height. Each item is assigned equal space vertically via Flexbox, so that the items align with the relevant bar to the right (these are two totally different rendering contexts, with the chart itself sitting within a `canvas` element). So it's important that whatever content you choose to render here doesn't exceed the available height available to each item. The chart's height is calculated as `numberOfBars * 32`, so content should be kept within that `32px` threshold. + +## Legend items + +Much the same as with the sidebar items, no assumptions are made here, solutions will have different aims. + +`legendItems` should be provided to the context, and a `renderLegendItem` prop should be provided to the chart. + +A legend is optional. + +## Overall usage + +Pulling all of this together, things look like this (for a specific solution): + +``` +const renderSidebarItem: RenderItem<SidebarItem> = (item, index) => { + return <MiddleTruncatedText text={`${index + 1}. ${item.url}`} />; +}; + +const renderLegendItem: RenderItem<LegendItem> = (item) => { + return <EuiHealth color={item.colour}>{item.name}</EuiHealth>; +}; + +<WaterfallProvider + data={series} + sidebarItems={sidebarItems} + legendItems={legendItems} + renderTooltipItem={(tooltipProps) => { + return <EuiHealth color={String(tooltipProps.colour)}>{tooltipProps.value}</EuiHealth>; + }} +> + <WaterfallChart + tickFormat={(d: number) => `${Number(d).toFixed(0)} ms`} + domain={{ min: domain.min, max: domain.max }} + barStyleAccessor={(datum) => { + return datum.datum.config.colour; + }} + renderSidebarItem={renderSidebarItem} + renderLegendItem={renderLegendItem} + /> +</WaterfallProvider> +``` + +A solution could easily forego a sidebar and legend for a more minimalistic view, e.g. maybe a mini waterfall within a table column. + + diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts new file mode 100644 index 0000000000000..ac650c5ef0ddd --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +// Pixel value +export const BAR_HEIGHT = 32; +// Flex grow value +export const MAIN_GROW_SIZE = 8; +// Flex grow value +export const SIDEBAR_GROW_SIZE = 2; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx new file mode 100644 index 0000000000000..85a205a7256f3 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx @@ -0,0 +1,28 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { IWaterfallContext } from '../context/waterfall_chart'; +import { WaterfallChartLegendContainer } from './styles'; +import { WaterfallChartProps } from './waterfall_chart'; + +interface LegendProps { + items: Required<IWaterfallContext>['legendItems']; + render: Required<WaterfallChartProps>['renderLegendItem']; +} + +export const Legend: React.FC<LegendProps> = ({ items, render }) => { + return ( + <WaterfallChartLegendContainer> + <EuiFlexGroup gutterSize="none"> + {items.map((item, index) => { + return <EuiFlexItem key={index}>{render(item, index)}</EuiFlexItem>; + })} + </EuiFlexGroup> + </WaterfallChartLegendContainer> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx new file mode 100644 index 0000000000000..4f54a347d22d2 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx @@ -0,0 +1,39 @@ +/* + * 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 { getChunks, MiddleTruncatedText } from './middle_truncated_text'; +import { shallowWithIntl } from '@kbn/test/jest'; +import React from 'react'; + +const longString = + 'this-is-a-really-really-really-really-really-really-really-really-long-string.madeup.extension'; + +describe('getChunks', () => { + it('Calculates chunks correctly', () => { + const result = getChunks(longString); + expect(result).toEqual({ + first: 'this-is-a-really-really-really-really-really-really-really-really-long-string.made', + last: 'up.extension', + }); + }); +}); + +describe('Component', () => { + it('Renders correctly', () => { + expect(shallowWithIntl(<MiddleTruncatedText text={longString} />)).toMatchInlineSnapshot(` + <styled.div> + <styled.div> + <styled.span> + this-is-a-really-really-really-really-really-really-really-really-long-string.made + </styled.span> + <styled.span> + up.extension + </styled.span> + </styled.div> + </styled.div> + `); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx new file mode 100644 index 0000000000000..519927d7db28b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx @@ -0,0 +1,61 @@ +/* + * 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, { useMemo } from 'react'; +import styled from 'styled-components'; + +const OuterContainer = styled.div` + width: 100%; + height: 100%; + position: relative; +`; + +const InnerContainer = styled.div` + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + overflow: hidden; + display: flex; + min-width: 0; +`; // NOTE: min-width: 0 ensures flexbox and no-wrap children can co-exist + +const FirstChunk = styled.span` + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +`; + +const LastChunk = styled.span` + flex-shrink: 0; +`; + +export const getChunks = (text: string) => { + const END_CHARS = 12; + const chars = text.split(''); + const splitPoint = chars.length - END_CHARS > 0 ? chars.length - END_CHARS : null; + const endChars = splitPoint ? chars.splice(splitPoint) : []; + return { first: chars.join(''), last: endChars.join('') }; +}; + +// Helper component for adding middle text truncation, e.g. +// really-really-really-long....ompressed.js +// Can be used to accomodate content in sidebar item rendering. +export const MiddleTruncatedText = ({ text }: { text: string }) => { + const chunks = useMemo(() => { + return getChunks(text); + }, [text]); + + return ( + <OuterContainer> + <InnerContainer> + <FirstChunk>{chunks.first}</FirstChunk> + <LastChunk>{chunks.last}</LastChunk> + </InnerContainer> + </OuterContainer> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx new file mode 100644 index 0000000000000..9ff544fc1946b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx @@ -0,0 +1,43 @@ +/* + * 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 { EuiFlexItem } from '@elastic/eui'; +import { SIDEBAR_GROW_SIZE } from './constants'; +import { IWaterfallContext } from '../context/waterfall_chart'; +import { + WaterfallChartSidebarContainer, + WaterfallChartSidebarContainerInnerPanel, + WaterfallChartSidebarContainerFlexGroup, + WaterfallChartSidebarFlexItem, +} from './styles'; +import { WaterfallChartProps } from './waterfall_chart'; + +interface SidebarProps { + items: Required<IWaterfallContext>['sidebarItems']; + height: number; + render: Required<WaterfallChartProps>['renderSidebarItem']; +} + +export const Sidebar: React.FC<SidebarProps> = ({ items, height, render }) => { + return ( + <EuiFlexItem grow={SIDEBAR_GROW_SIZE}> + <WaterfallChartSidebarContainer height={height}> + <WaterfallChartSidebarContainerInnerPanel paddingSize="none"> + <WaterfallChartSidebarContainerFlexGroup direction="column" gutterSize="none"> + {items.map((item, index) => { + return ( + <WaterfallChartSidebarFlexItem key={index}> + {render(item, index)} + </WaterfallChartSidebarFlexItem> + ); + })} + </WaterfallChartSidebarContainerFlexGroup> + </WaterfallChartSidebarContainerInnerPanel> + </WaterfallChartSidebarContainer> + </EuiFlexItem> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts new file mode 100644 index 0000000000000..25f5e5f8f5cc9 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.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. + */ + +import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../observability/public'; + +// NOTE: This isn't a perfect solution - changes in font size etc within charts could change the ideal height here. +const FIXED_AXIS_HEIGHT = 33; + +interface WaterfallChartOuterContainerProps { + height?: number; +} + +export const WaterfallChartOuterContainer = euiStyled.div<WaterfallChartOuterContainerProps>` + height: ${(props) => (props.height ? `${props.height}px` : 'auto')}; + overflow-y: ${(props) => (props.height ? 'scroll' : 'visible')}; + overflow-x: hidden; +`; + +export const WaterfallChartFixedTopContainer = euiStyled.div` + position: sticky; + top: 0; + z-index: ${(props) => props.theme.eui.euiZLevel4}; +`; + +export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` + height: 100%; + border-radius: 0 !important; + border: none; +`; // NOTE: border-radius !important is here as the "border" prop isn't working + +export const WaterfallChartFixedAxisContainer = euiStyled.div` + height: ${FIXED_AXIS_HEIGHT}px; +`; + +interface WaterfallChartSidebarContainer { + height: number; +} + +export const WaterfallChartSidebarContainer = euiStyled.div<WaterfallChartSidebarContainer>` + height: ${(props) => `${props.height - FIXED_AXIS_HEIGHT}px`}; + overflow-y: hidden; +`; + +export const WaterfallChartSidebarContainerInnerPanel = euiStyled(EuiPanel)` + border: 0; + height: 100%; +`; + +export const WaterfallChartSidebarContainerFlexGroup = euiStyled(EuiFlexGroup)` + height: 100%; +`; + +// Ensures flex items honour no-wrap of children, rather than trying to extend to the full width of children. +export const WaterfallChartSidebarFlexItem = euiStyled(EuiFlexItem)` + min-width: 0; + padding-left: ${(props) => props.theme.eui.paddingSizes.m}; + padding-right: ${(props) => props.theme.eui.paddingSizes.m}; +`; + +interface WaterfallChartChartContainer { + height: number; +} + +export const WaterfallChartChartContainer = euiStyled.div<WaterfallChartChartContainer>` + width: 100%; + height: ${(props) => `${props.height}px`}; + margin-top: -${FIXED_AXIS_HEIGHT}px; +`; + +export const WaterfallChartLegendContainer = euiStyled.div` + position: sticky; + bottom: 0; + z-index: ${(props) => props.theme.eui.euiZLevel4}; + background-color: ${(props) => props.theme.eui.euiColorLightestShade}; + padding: ${(props) => props.theme.eui.paddingSizes.xs}; + font-size: ${(props) => props.theme.eui.euiFontSizeXS}; + box-shadow: 0px -1px 4px 0px ${(props) => props.theme.eui.euiColorLightShade}; +`; // NOTE: EuiShadowColor is a little too dark to work with the background-color + +export const WaterfallChartTooltip = euiStyled.div` + background-color: ${(props) => props.theme.eui.euiColorDarkestShade}; + border-radius: ${(props) => props.theme.eui.euiBorderRadius}; + color: ${(props) => props.theme.eui.euiColorLightestShade}; + padding: ${(props) => props.theme.eui.paddingSizes.s}; +`; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx new file mode 100644 index 0000000000000..de4be0ea34b2c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -0,0 +1,194 @@ +/* + * 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, { useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + Axis, + BarSeries, + Chart, + Position, + ScaleType, + Settings, + TickFormatter, + DomainRange, + BarStyleAccessor, + TooltipInfo, + TooltipType, +} from '@elastic/charts'; +import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; +// NOTE: The WaterfallChart has a hard requirement that consumers / solutions are making use of KibanaReactContext, and useKibana etc +// can therefore be accessed. +import { useUiSetting$ } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { useWaterfallContext } from '../context/waterfall_chart'; +import { + WaterfallChartOuterContainer, + WaterfallChartFixedTopContainer, + WaterfallChartFixedTopContainerSidebarCover, + WaterfallChartFixedAxisContainer, + WaterfallChartChartContainer, + WaterfallChartTooltip, +} from './styles'; +import { WaterfallData } from '../types'; +import { BAR_HEIGHT, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants'; +import { Sidebar } from './sidebar'; +import { Legend } from './legend'; + +const Tooltip = ({ header }: TooltipInfo) => { + const { data, renderTooltipItem } = useWaterfallContext(); + const relevantItems = data.filter((item) => { + return item.x === header?.value; + }); + return ( + <WaterfallChartTooltip> + <EuiFlexGroup direction="column" gutterSize="none"> + {relevantItems.map((item, index) => { + return ( + <EuiFlexItem key={index}>{renderTooltipItem(item.config.tooltipProps)}</EuiFlexItem> + ); + })} + </EuiFlexGroup> + </WaterfallChartTooltip> + ); +}; + +export type RenderItem<I = any> = (item: I, index: number) => JSX.Element; + +export interface WaterfallChartProps { + tickFormat: TickFormatter; + domain: DomainRange; + barStyleAccessor: BarStyleAccessor; + renderSidebarItem?: RenderItem; + renderLegendItem?: RenderItem; + maxHeight?: number; +} + +const getUniqueBars = (data: WaterfallData) => { + return data.reduce<Set<number>>((acc, item) => { + if (!acc.has(item.x)) { + acc.add(item.x); + return acc; + } else { + return acc; + } + }, new Set()); +}; + +const getChartHeight = (data: WaterfallData): number => getUniqueBars(data).size * BAR_HEIGHT; + +export const WaterfallChart = ({ + tickFormat, + domain, + barStyleAccessor, + renderSidebarItem, + renderLegendItem, + maxHeight = 600, +}: WaterfallChartProps) => { + const { data, sidebarItems, legendItems } = useWaterfallContext(); + + const generatedHeight = useMemo(() => { + return getChartHeight(data); + }, [data]); + + const [darkMode] = useUiSetting$<boolean>('theme:darkMode'); + + const theme = useMemo(() => { + return darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; + }, [darkMode]); + + const shouldRenderSidebar = + sidebarItems && sidebarItems.length > 0 && renderSidebarItem ? true : false; + const shouldRenderLegend = + legendItems && legendItems.length > 0 && renderLegendItem ? true : false; + + return ( + <WaterfallChartOuterContainer height={maxHeight}> + <> + <WaterfallChartFixedTopContainer> + <EuiFlexGroup gutterSize="none"> + {shouldRenderSidebar && ( + <EuiFlexItem grow={SIDEBAR_GROW_SIZE}> + <WaterfallChartFixedTopContainerSidebarCover paddingSize="none" /> + </EuiFlexItem> + )} + <EuiFlexItem grow={shouldRenderSidebar ? MAIN_GROW_SIZE : true}> + <WaterfallChartFixedAxisContainer> + <Chart className="axis-only-chart"> + <Settings + showLegend={false} + rotation={90} + tooltip={{ type: TooltipType.None }} + theme={theme} + /> + + <Axis + id="time" + position={Position.Top} + tickFormat={tickFormat} + domain={domain} + showGridLines={true} + /> + + <Axis id="values" position={Position.Left} tickFormat={() => ''} /> + + <BarSeries + id="waterfallItems" + xScaleType={ScaleType.Linear} + yScaleType={ScaleType.Linear} + xAccessor="x" + yAccessors={['y']} + y0Accessors={['y0']} + styleAccessor={barStyleAccessor} + data={[{ x: 0, y0: 0, y1: 0 }]} + /> + </Chart> + </WaterfallChartFixedAxisContainer> + </EuiFlexItem> + </EuiFlexGroup> + </WaterfallChartFixedTopContainer> + <EuiFlexGroup gutterSize="none"> + {shouldRenderSidebar && ( + <Sidebar items={sidebarItems!} height={generatedHeight} render={renderSidebarItem!} /> + )} + <EuiFlexItem grow={shouldRenderSidebar ? MAIN_GROW_SIZE : true}> + <WaterfallChartChartContainer height={generatedHeight}> + <Chart className="data-chart"> + <Settings + showLegend={false} + rotation={90} + tooltip={{ customTooltip: Tooltip }} + theme={theme} + /> + + <Axis + id="time" + position={Position.Top} + tickFormat={tickFormat} + domain={domain} + showGridLines={true} + /> + + <Axis id="values" position={Position.Left} tickFormat={() => ''} /> + + <BarSeries + id="waterfallItems" + xScaleType={ScaleType.Linear} + yScaleType={ScaleType.Linear} + xAccessor="x" + yAccessors={['y']} + y0Accessors={['y0']} + styleAccessor={barStyleAccessor} + data={data} + /> + </Chart> + </WaterfallChartChartContainer> + </EuiFlexItem> + </EuiFlexGroup> + {shouldRenderLegend && <Legend items={legendItems!} render={renderLegendItem!} />} + </> + </WaterfallChartOuterContainer> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.test.ts new file mode 100644 index 0000000000000..698e6b4be0c4c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.test.ts @@ -0,0 +1,687 @@ +/* + * 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 { colourPalette } from './data_formatting'; + +// const TEST_DATA = [ +// { +// '@timestamp': '2020-10-29T14:55:01.055Z', +// ecs: { +// version: '1.6.0', +// }, +// agent: { +// type: 'heartbeat', +// version: '7.10.0', +// hostname: 'docker-desktop', +// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', +// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', +// name: 'docker-desktop', +// }, +// synthetics: { +// index: 7, +// payload: { +// request: { +// url: 'https://unpkg.com/director@1.2.8/build/director.js', +// method: 'GET', +// headers: { +// referer: '', +// user_agent: +// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', +// }, +// mixed_content_type: 'none', +// initial_priority: 'High', +// referrer_policy: 'no-referrer-when-downgrade', +// }, +// status: 200, +// method: 'GET', +// end: 13902.944973, +// url: 'https://unpkg.com/director@1.2.8/build/director.js', +// type: 'Script', +// is_navigation_request: false, +// start: 13902.752946, +// response: { +// encoded_data_length: 179, +// protocol: 'h2', +// headers: { +// content_encoding: 'br', +// server: 'cloudflare', +// age: '94838', +// cf_cache_status: 'HIT', +// x_content_type_options: 'nosniff', +// last_modified: 'Wed, 04 Feb 2015 03:25:28 GMT', +// cf_ray: '5e9dbc2bdda2e5a7-MAN', +// content_type: 'application/javascript; charset=utf-8', +// x_cloud_trace_context: 'eec7acc7a6f96b5353ef0d648bf437ac', +// expect_ct: +// 'max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"', +// access_control_allow_origin: '*', +// vary: 'Accept-Encoding', +// cache_control: 'public, max-age=31536000', +// date: 'Thu, 29 Oct 2020 14:55:00 GMT', +// cf_request_id: '061673ef6b0000e5a7cd07a000000001', +// etag: 'W/"4f70-NHpXdyWxnckEaeiXalAnXQ+oh4Q"', +// strict_transport_security: 'max-age=31536000; includeSubDomains; preload', +// }, +// remote_i_p_address: '104.16.125.175', +// connection_reused: true, +// timing: { +// dns_start: -1, +// push_end: 0, +// worker_fetch_start: -1, +// worker_respond_with_settled: -1, +// proxy_end: -1, +// worker_start: -1, +// worker_ready: -1, +// send_end: 158.391, +// connect_end: -1, +// connect_start: -1, +// send_start: 157.876, +// proxy_start: -1, +// push_start: 0, +// ssl_end: -1, +// receive_headers_end: 186.885, +// ssl_start: -1, +// request_time: 13902.757525, +// dns_end: -1, +// }, +// connection_id: 17, +// status_text: '', +// remote_port: 443, +// status: 200, +// security_details: { +// valid_to: 1627905600, +// certificate_id: 0, +// key_exchange_group: 'X25519', +// valid_from: 1596326400, +// protocol: 'TLS 1.3', +// issuer: 'Cloudflare Inc ECC CA-3', +// key_exchange: '', +// san_list: ['unpkg.com', '*.unpkg.com', 'sni.cloudflaressl.com'], +// signed_certificate_timestamp_list: [], +// certificate_transparency_compliance: 'unknown', +// cipher: 'AES_128_GCM', +// subject_name: 'sni.cloudflaressl.com', +// }, +// mime_type: 'application/javascript', +// url: 'https://unpkg.com/director@1.2.8/build/director.js', +// from_prefetch_cache: false, +// from_disk_cache: false, +// security_state: 'secure', +// response_time: 1.603983300513211e12, +// from_service_worker: false, +// }, +// }, +// journey: { +// name: 'check that title is present', +// id: 'check that title is present', +// }, +// type: 'journey/network_info', +// package_version: '0.0.1', +// }, +// monitor: { +// status: 'up', +// duration: { +// us: 24, +// }, +// id: 'check that title is present', +// name: 'check that title is present', +// type: 'browser', +// timespan: { +// gte: '2020-10-29T14:55:01.055Z', +// lt: '2020-10-29T14:56:01.055Z', +// }, +// check_group: '948d3b6b-19f6-11eb-b237-025000000001', +// }, +// event: { +// dataset: 'uptime', +// }, +// }, +// { +// '@timestamp': '2020-10-29T14:55:01.055Z', +// ecs: { +// version: '1.6.0', +// }, +// agent: { +// version: '7.10.0', +// hostname: 'docker-desktop', +// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', +// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', +// name: 'docker-desktop', +// type: 'heartbeat', +// }, +// monitor: { +// check_group: '948d3b6b-19f6-11eb-b237-025000000001', +// status: 'up', +// duration: { +// us: 13, +// }, +// id: 'check that title is present', +// name: 'check that title is present', +// type: 'browser', +// timespan: { +// gte: '2020-10-29T14:55:01.055Z', +// lt: '2020-10-29T14:56:01.055Z', +// }, +// }, +// synthetics: { +// journey: { +// name: 'check that title is present', +// id: 'check that title is present', +// }, +// type: 'journey/network_info', +// package_version: '0.0.1', +// index: 9, +// payload: { +// start: 13902.76168, +// url: 'file:///opt/examples/todos/app/app.js', +// method: 'GET', +// is_navigation_request: false, +// end: 13902.770133, +// request: { +// headers: { +// referer: '', +// user_agent: +// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', +// }, +// mixed_content_type: 'none', +// initial_priority: 'High', +// referrer_policy: 'no-referrer-when-downgrade', +// url: 'file:///opt/examples/todos/app/app.js', +// method: 'GET', +// }, +// status: 0, +// type: 'Script', +// response: { +// protocol: 'file', +// connection_reused: false, +// mime_type: 'text/javascript', +// security_state: 'secure', +// from_disk_cache: false, +// url: 'file:///opt/examples/todos/app/app.js', +// status_text: '', +// connection_id: 0, +// from_prefetch_cache: false, +// encoded_data_length: -1, +// headers: {}, +// status: 0, +// from_service_worker: false, +// }, +// }, +// }, +// event: { +// dataset: 'uptime', +// }, +// }, +// { +// '@timestamp': '2020-10-29T14:55:01.000Z', +// monitor: { +// timespan: { +// lt: '2020-10-29T14:56:01.000Z', +// gte: '2020-10-29T14:55:01.000Z', +// }, +// id: 'check that title is present', +// name: 'check that title is present', +// check_group: '948d3b6b-19f6-11eb-b237-025000000001', +// status: 'up', +// duration: { +// us: 44365, +// }, +// type: 'browser', +// }, +// synthetics: { +// journey: { +// id: 'check that title is present', +// name: 'check that title is present', +// }, +// type: 'journey/network_info', +// package_version: '0.0.1', +// index: 5, +// payload: { +// status: 0, +// url: 'file:///opt/examples/todos/app/index.html', +// end: 13902.730261, +// request: { +// method: 'GET', +// headers: {}, +// mixed_content_type: 'none', +// initial_priority: 'VeryHigh', +// referrer_policy: 'no-referrer-when-downgrade', +// url: 'file:///opt/examples/todos/app/index.html', +// }, +// method: 'GET', +// response: { +// status: 0, +// connection_id: 0, +// from_disk_cache: false, +// headers: {}, +// encoded_data_length: -1, +// status_text: '', +// from_service_worker: false, +// connection_reused: false, +// url: 'file:///opt/examples/todos/app/index.html', +// remote_port: 0, +// security_state: 'secure', +// protocol: 'file', +// mime_type: 'text/html', +// remote_i_p_address: '', +// from_prefetch_cache: false, +// }, +// start: 13902.726626, +// type: 'Document', +// is_navigation_request: true, +// }, +// }, +// event: { +// dataset: 'uptime', +// }, +// ecs: { +// version: '1.6.0', +// }, +// agent: { +// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', +// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', +// name: 'docker-desktop', +// type: 'heartbeat', +// version: '7.10.0', +// hostname: 'docker-desktop', +// }, +// }, +// { +// '@timestamp': '2020-10-29T14:55:01.044Z', +// monitor: { +// type: 'browser', +// timespan: { +// lt: '2020-10-29T14:56:01.044Z', +// gte: '2020-10-29T14:55:01.044Z', +// }, +// check_group: '948d3b6b-19f6-11eb-b237-025000000001', +// status: 'up', +// duration: { +// us: 10524, +// }, +// id: 'check that title is present', +// name: 'check that title is present', +// }, +// synthetics: { +// package_version: '0.0.1', +// index: 6, +// payload: { +// status: 200, +// type: 'Stylesheet', +// url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', +// method: 'GET', +// start: 13902.75266, +// is_navigation_request: false, +// end: 13902.943835, +// response: { +// remote_i_p_address: '104.16.125.175', +// response_time: 1.603983300511892e12, +// url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', +// mime_type: 'text/css', +// protocol: 'h2', +// security_state: 'secure', +// encoded_data_length: 414, +// remote_port: 443, +// status_text: '', +// timing: { +// proxy_start: -1, +// worker_ready: -1, +// worker_fetch_start: -1, +// receive_headers_end: 189.169, +// worker_respond_with_settled: -1, +// connect_end: 160.311, +// worker_start: -1, +// send_start: 161.275, +// dns_start: 0.528, +// send_end: 161.924, +// ssl_end: 160.267, +// proxy_end: -1, +// ssl_start: 29.726, +// request_time: 13902.753988, +// dns_end: 5.212, +// push_end: 0, +// push_start: 0, +// connect_start: 5.212, +// }, +// connection_reused: false, +// from_service_worker: false, +// security_details: { +// san_list: ['unpkg.com', '*.unpkg.com', 'sni.cloudflaressl.com'], +// valid_from: 1596326400, +// cipher: 'AES_128_GCM', +// protocol: 'TLS 1.3', +// issuer: 'Cloudflare Inc ECC CA-3', +// valid_to: 1627905600, +// certificate_id: 0, +// key_exchange_group: 'X25519', +// certificate_transparency_compliance: 'unknown', +// key_exchange: '', +// subject_name: 'sni.cloudflaressl.com', +// signed_certificate_timestamp_list: [], +// }, +// connection_id: 17, +// status: 200, +// from_disk_cache: false, +// from_prefetch_cache: false, +// headers: { +// date: 'Thu, 29 Oct 2020 14:55:00 GMT', +// x_cloud_trace_context: '76a4f7b8be185f2ac9aa839de3d6f893', +// cache_control: 'public, max-age=31536000', +// expect_ct: +// 'max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"', +// content_type: 'text/css; charset=utf-8', +// age: '627638', +// x_content_type_options: 'nosniff', +// last_modified: 'Sat, 09 Jan 2016 00:57:37 GMT', +// access_control_allow_origin: '*', +// cf_request_id: '061673ef6a0000e5a75a309000000001', +// vary: 'Accept-Encoding', +// strict_transport_security: 'max-age=31536000; includeSubDomains; preload', +// cf_ray: '5e9dbc2bdda1e5a7-MAN', +// content_encoding: 'br', +// etag: 'W/"1921-kYwbQVnRAA2V/L9Gr4SCtUE5LHQ"', +// server: 'cloudflare', +// cf_cache_status: 'HIT', +// }, +// }, +// request: { +// headers: { +// referer: '', +// user_agent: +// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', +// }, +// mixed_content_type: 'none', +// initial_priority: 'VeryHigh', +// referrer_policy: 'no-referrer-when-downgrade', +// url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', +// method: 'GET', +// }, +// }, +// journey: { +// id: 'check that title is present', +// name: 'check that title is present', +// }, +// type: 'journey/network_info', +// }, +// event: { +// dataset: 'uptime', +// }, +// ecs: { +// version: '1.6.0', +// }, +// agent: { +// version: '7.10.0', +// hostname: 'docker-desktop', +// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', +// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', +// name: 'docker-desktop', +// type: 'heartbeat', +// }, +// }, +// { +// '@timestamp': '2020-10-29T14:55:01.055Z', +// agent: { +// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', +// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', +// name: 'docker-desktop', +// type: 'heartbeat', +// version: '7.10.0', +// hostname: 'docker-desktop', +// }, +// synthetics: { +// index: 8, +// payload: { +// method: 'GET', +// type: 'Script', +// response: { +// url: 'file:///opt/examples/todos/app/vue.min.js', +// protocol: 'file', +// connection_id: 0, +// headers: {}, +// mime_type: 'text/javascript', +// from_service_worker: false, +// status_text: '', +// connection_reused: false, +// encoded_data_length: -1, +// from_disk_cache: false, +// security_state: 'secure', +// from_prefetch_cache: false, +// status: 0, +// }, +// is_navigation_request: false, +// request: { +// mixed_content_type: 'none', +// initial_priority: 'High', +// referrer_policy: 'no-referrer-when-downgrade', +// url: 'file:///opt/examples/todos/app/vue.min.js', +// method: 'GET', +// headers: { +// referer: '', +// user_agent: +// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', +// }, +// }, +// end: 13902.772783, +// status: 0, +// start: 13902.760644, +// url: 'file:///opt/examples/todos/app/vue.min.js', +// }, +// journey: { +// name: 'check that title is present', +// id: 'check that title is present', +// }, +// type: 'journey/network_info', +// package_version: '0.0.1', +// }, +// monitor: { +// status: 'up', +// duration: { +// us: 82, +// }, +// name: 'check that title is present', +// type: 'browser', +// timespan: { +// gte: '2020-10-29T14:55:01.055Z', +// lt: '2020-10-29T14:56:01.055Z', +// }, +// id: 'check that title is present', +// check_group: '948d3b6b-19f6-11eb-b237-025000000001', +// }, +// event: { +// dataset: 'uptime', +// }, +// ecs: { +// version: '1.6.0', +// }, +// }, +// ]; + +// const toMillis = (seconds: number) => seconds * 1000; + +// describe('getTimings', () => { +// it('Calculates timings for network events correctly', () => { +// // NOTE: Uses these timings as the file protocol events don't have timing information +// const eventOneTimings = getTimings( +// TEST_DATA[0].synthetics.payload.response.timing!, +// toMillis(TEST_DATA[0].synthetics.payload.start), +// toMillis(TEST_DATA[0].synthetics.payload.end) +// ); +// expect(eventOneTimings).toEqual({ +// blocked: 162.4549999999106, +// connect: -1, +// dns: -1, +// receive: 0.5629999989271255, +// send: 0.5149999999999864, +// ssl: undefined, +// wait: 28.494, +// }); + +// const eventFourTimings = getTimings( +// TEST_DATA[3].synthetics.payload.response.timing!, +// toMillis(TEST_DATA[3].synthetics.payload.start), +// toMillis(TEST_DATA[3].synthetics.payload.end) +// ); +// expect(eventFourTimings).toEqual({ +// blocked: 1.8559999997466803, +// connect: 25.52200000000002, +// dns: 4.683999999999999, +// receive: 0.6780000009983667, +// send: 0.6490000000000009, +// ssl: 130.541, +// wait: 27.245000000000005, +// }); +// }); +// }); + +// describe('getSeriesAndDomain', () => { +// let seriesAndDomain: any; +// let NetworkItems: any; + +// beforeAll(() => { +// NetworkItems = extractItems(TEST_DATA); +// seriesAndDomain = getSeriesAndDomain(NetworkItems); +// }); + +// it('Correctly calculates the domain', () => { +// expect(seriesAndDomain.domain).toEqual({ max: 218.34699999913573, min: 0 }); +// }); + +// it('Correctly calculates the series', () => { +// expect(seriesAndDomain.series).toEqual([ +// { +// config: { colour: '#f3b3a6', tooltipProps: { colour: '#f3b3a6', value: '3.635ms' } }, +// x: 0, +// y: 3.6349999997764826, +// y0: 0, +// }, +// { +// config: { +// colour: '#b9a888', +// tooltipProps: { colour: '#b9a888', value: 'Queued / Blocked: 1.856ms' }, +// }, +// x: 1, +// y: 27.889999999731778, +// y0: 26.0339999999851, +// }, +// { +// config: { colour: '#54b399', tooltipProps: { colour: '#54b399', value: 'DNS: 4.684ms' } }, +// x: 1, +// y: 32.573999999731775, +// y0: 27.889999999731778, +// }, +// { +// config: { +// colour: '#da8b45', +// tooltipProps: { colour: '#da8b45', value: 'Connecting: 25.522ms' }, +// }, +// x: 1, +// y: 58.095999999731795, +// y0: 32.573999999731775, +// }, +// { +// config: { colour: '#edc5a2', tooltipProps: { colour: '#edc5a2', value: 'SSL: 130.541ms' } }, +// x: 1, +// y: 188.63699999973178, +// y0: 58.095999999731795, +// }, +// { +// config: { +// colour: '#d36086', +// tooltipProps: { colour: '#d36086', value: 'Sending request: 0.649ms' }, +// }, +// x: 1, +// y: 189.28599999973179, +// y0: 188.63699999973178, +// }, +// { +// config: { +// colour: '#b0c9e0', +// tooltipProps: { colour: '#b0c9e0', value: 'Waiting (TTFB): 27.245ms' }, +// }, +// x: 1, +// y: 216.5309999997318, +// y0: 189.28599999973179, +// }, +// { +// config: { +// colour: '#ca8eae', +// tooltipProps: { colour: '#ca8eae', value: 'Content downloading: 0.678ms' }, +// }, +// x: 1, +// y: 217.20900000073016, +// y0: 216.5309999997318, +// }, +// { +// config: { +// colour: '#b9a888', +// tooltipProps: { colour: '#b9a888', value: 'Queued / Blocked: 162.455ms' }, +// }, +// x: 2, +// y: 188.77500000020862, +// y0: 26.320000000298023, +// }, +// { +// config: { +// colour: '#d36086', +// tooltipProps: { colour: '#d36086', value: 'Sending request: 0.515ms' }, +// }, +// x: 2, +// y: 189.2900000002086, +// y0: 188.77500000020862, +// }, +// { +// config: { +// colour: '#b0c9e0', +// tooltipProps: { colour: '#b0c9e0', value: 'Waiting (TTFB): 28.494ms' }, +// }, +// x: 2, +// y: 217.7840000002086, +// y0: 189.2900000002086, +// }, +// { +// config: { +// colour: '#9170b8', +// tooltipProps: { colour: '#9170b8', value: 'Content downloading: 0.563ms' }, +// }, +// x: 2, +// y: 218.34699999913573, +// y0: 217.7840000002086, +// }, +// { +// config: { colour: '#9170b8', tooltipProps: { colour: '#9170b8', value: '12.139ms' } }, +// x: 3, +// y: 46.15699999965727, +// y0: 34.01799999922514, +// }, +// { +// config: { colour: '#9170b8', tooltipProps: { colour: '#9170b8', value: '8.453ms' } }, +// x: 4, +// y: 43.506999999284744, +// y0: 35.053999999538064, +// }, +// ]); +// }); +// }); + +describe('Palettes', () => { + it('A colour palette comprising timing and mime type colours is correctly generated', () => { + expect(colourPalette).toEqual({ + blocked: '#b9a888', + connect: '#da8b45', + dns: '#54b399', + font: '#aa6556', + html: '#f3b3a6', + media: '#d6bf57', + other: '#e7664c', + receive: '#54b399', + script: '#9170b8', + send: '#d36086', + ssl: '#edc5a2', + stylesheet: '#ca8eae', + wait: '#b0c9e0', + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts new file mode 100644 index 0000000000000..9c66ea638c942 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts @@ -0,0 +1,336 @@ +/* + * 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 { euiPaletteColorBlind } from '@elastic/eui'; + +import { + PayloadTimings, + CalculatedTimings, + NetworkItems, + FriendlyTimingLabels, + FriendlyMimetypeLabels, + MimeType, + MimeTypesMap, + Timings, + TIMING_ORDER, + SidebarItems, + LegendItems, +} from './types'; +import { WaterfallData } from '../../../waterfall'; + +const microToMillis = (micro: number): number => (micro === -1 ? -1 : micro * 1000); + +// The timing calculations here are based off several sources: +// https://github.com/ChromeDevTools/devtools-frontend/blob/2fe91adefb2921b4deb2b4b125370ef9ccdb8d1b/front_end/sdk/HARLog.js#L307 +// and +// https://chromium.googlesource.com/chromium/blink.git/+/master/Source/devtools/front_end/sdk/HAREntry.js#131 +// and +// https://github.com/cyrus-and/chrome-har-capturer/blob/master/lib/har.js#L195 +// Order of events: request_start = 0, [proxy], [dns], [connect [ssl]], [send], receive_headers_end + +export const getTimings = ( + timings: PayloadTimings, + requestSentTime: number, + responseReceivedTime: number +): CalculatedTimings => { + if (!timings) return { blocked: -1, dns: -1, connect: -1, send: 0, wait: 0, receive: 0, ssl: -1 }; + + const getLeastNonNegative = (values: number[]) => + values.reduce<number>((best, value) => (value >= 0 && value < best ? value : best), Infinity); + const getOptionalTiming = (_timings: PayloadTimings, key: keyof PayloadTimings) => + _timings[key] >= 0 ? _timings[key] : -1; + + // NOTE: Request sent and request start can differ due to queue times + const requestStartTime = microToMillis(timings.request_time); + + // Queued + const queuedTime = requestSentTime < requestStartTime ? requestStartTime - requestSentTime : -1; + + // Blocked + // "blocked" represents both queued time + blocked/stalled time + proxy time (ie: anything before the request was actually started). + let blocked = queuedTime; + + const blockedStart = getLeastNonNegative([ + timings.dns_start, + timings.connect_start, + timings.send_start, + ]); + + if (blockedStart !== Infinity) { + blocked += blockedStart; + } + + // Proxy + // Proxy is part of blocked, but it can be quirky in that blocked can be -1 even though there are proxy timings. This can happen with + // protocols like Quic. + if (timings.proxy_end !== -1) { + const blockedProxy = timings.proxy_end - timings.proxy_start; + + if (blockedProxy && blockedProxy > blocked) { + blocked = blockedProxy; + } + } + + // DNS + const dnsStart = timings.dns_end >= 0 ? blockedStart : 0; + const dnsEnd = getOptionalTiming(timings, 'dns_end'); + const dns = dnsEnd - dnsStart; + + // SSL + const sslStart = getOptionalTiming(timings, 'ssl_start'); + const sslEnd = getOptionalTiming(timings, 'ssl_end'); + let ssl; + + if (sslStart >= 0 && sslEnd >= 0) { + ssl = timings.ssl_end - timings.ssl_start; + } + + // Connect + let connect = -1; + if (timings.connect_start >= 0) { + connect = timings.send_start - timings.connect_start; + } + + // Send + const send = timings.send_end - timings.send_start; + + // Wait + const wait = timings.receive_headers_end - timings.send_end; + + // Receive + const receive = responseReceivedTime - (requestStartTime + timings.receive_headers_end); + + // SSL connection is a part of the overall connection time + if (connect && ssl) { + connect = connect - ssl; + } + + return { blocked, dns, connect, send, wait, receive, ssl }; +}; + +// TODO: Switch to real API data, and type data as the payload response (if server response isn't preformatted) +export const extractItems = (data: any): NetworkItems => { + const items = data + .map((entry: any) => { + const requestSentTime = microToMillis(entry.synthetics.payload.start); + const responseReceivedTime = microToMillis(entry.synthetics.payload.end); + const requestStartTime = + entry.synthetics.payload.response && entry.synthetics.payload.response.timing + ? microToMillis(entry.synthetics.payload.response.timing.request_time) + : null; + + return { + timestamp: entry['@timestamp'], + method: entry.synthetics.payload.method, + url: entry.synthetics.payload.url, + status: entry.synthetics.payload.status, + mimeType: entry.synthetics.payload?.response?.mime_type, + requestSentTime, + responseReceivedTime, + earliestRequestTime: requestStartTime + ? Math.min(requestSentTime, requestStartTime) + : requestSentTime, + timings: + entry.synthetics.payload.response && entry.synthetics.payload.response.timing + ? getTimings( + entry.synthetics.payload.response.timing, + requestSentTime, + responseReceivedTime + ) + : null, + }; + }) + .sort((a: any, b: any) => { + return a.earliestRequestTime - b.earliestRequestTime; + }); + + return items; +}; + +const formatValueForDisplay = (value: number, points: number = 3) => { + return Number(value).toFixed(points); +}; + +const getColourForMimeType = (mimeType?: string) => { + const key = mimeType && MimeTypesMap[mimeType] ? MimeTypesMap[mimeType] : MimeType.Other; + return colourPalette[key]; +}; + +export const getSeriesAndDomain = (items: NetworkItems) => { + // The earliest point in time a request is sent or started. This will become our notion of "0". + const zeroOffset = items.reduce<number>((acc, item) => { + const { earliestRequestTime } = item; + return earliestRequestTime < acc ? earliestRequestTime : acc; + }, Infinity); + + const series = items.reduce<WaterfallData>((acc, item, index) => { + const { earliestRequestTime } = item; + + // Entries without timings should be handled differently: + // https://github.com/ChromeDevTools/devtools-frontend/blob/ed2a064ac194bfae4e25c4748a9fa3513b3e9f7d/front_end/network/RequestTimingView.js#L140 + // If there are no concrete timings just plot one block via start and end + if (!item.timings || item.timings === null) { + const duration = item.responseReceivedTime - item.earliestRequestTime; + const colour = getColourForMimeType(item.mimeType); + return [ + ...acc, + { + x: index, + y0: item.earliestRequestTime - zeroOffset, + y: item.responseReceivedTime - zeroOffset, + config: { + colour, + tooltipProps: { + value: `${formatValueForDisplay(duration)}ms`, + colour, + }, + }, + }, + ]; + } + + let currentOffset = earliestRequestTime - zeroOffset; + + TIMING_ORDER.forEach((timing) => { + const value = item.timings![timing]; + const colour = + timing === Timings.Receive ? getColourForMimeType(item.mimeType) : colourPalette[timing]; + if (value && value >= 0) { + const y = currentOffset + value; + + acc.push({ + x: index, + y0: currentOffset, + y, + config: { + colour, + tooltipProps: { + value: `${FriendlyTimingLabels[timing]}: ${formatValueForDisplay( + y - currentOffset + )}ms`, + colour, + }, + }, + }); + currentOffset = y; + } + }); + return acc; + }, []); + + const yValues = series.map((serie) => serie.y); + const domain = { min: 0, max: Math.max(...yValues) }; + return { series, domain }; +}; + +export const getSidebarItems = (items: NetworkItems): SidebarItems => { + return items.map((item) => { + const { url, status, method } = item; + return { url, status, method }; + }); +}; + +export const getLegendItems = (): LegendItems => { + let timingItems: LegendItems = []; + Object.values(Timings).forEach((timing) => { + // The "receive" timing is mapped to a mime type colour, so we don't need to show this in the legend + if (timing === Timings.Receive) { + return; + } + timingItems = [ + ...timingItems, + { name: FriendlyTimingLabels[timing], colour: TIMING_PALETTE[timing] }, + ]; + }); + + let mimeTypeItems: LegendItems = []; + Object.values(MimeType).forEach((mimeType) => { + mimeTypeItems = [ + ...mimeTypeItems, + { name: FriendlyMimetypeLabels[mimeType], colour: MIME_TYPE_PALETTE[mimeType] }, + ]; + }); + return [...timingItems, ...mimeTypeItems]; +}; + +// Timing colour palette +type TimingColourPalette = { + [K in Timings]: string; +}; + +const SAFE_PALETTE = euiPaletteColorBlind({ rotations: 2 }); + +const buildTimingPalette = (): TimingColourPalette => { + const palette = Object.values(Timings).reduce<Partial<TimingColourPalette>>((acc, value) => { + switch (value) { + case Timings.Blocked: + acc[value] = SAFE_PALETTE[6]; + break; + case Timings.Dns: + acc[value] = SAFE_PALETTE[0]; + break; + case Timings.Connect: + acc[value] = SAFE_PALETTE[7]; + break; + case Timings.Ssl: + acc[value] = SAFE_PALETTE[17]; + break; + case Timings.Send: + acc[value] = SAFE_PALETTE[2]; + break; + case Timings.Wait: + acc[value] = SAFE_PALETTE[11]; + break; + case Timings.Receive: + acc[value] = SAFE_PALETTE[0]; + break; + } + return acc; + }, {}); + + return palette as TimingColourPalette; +}; + +const TIMING_PALETTE = buildTimingPalette(); + +// MimeType colour palette +type MimeTypeColourPalette = { + [K in MimeType]: string; +}; + +const buildMimeTypePalette = (): MimeTypeColourPalette => { + const palette = Object.values(MimeType).reduce<Partial<MimeTypeColourPalette>>((acc, value) => { + switch (value) { + case MimeType.Html: + acc[value] = SAFE_PALETTE[19]; + break; + case MimeType.Script: + acc[value] = SAFE_PALETTE[3]; + break; + case MimeType.Stylesheet: + acc[value] = SAFE_PALETTE[4]; + break; + case MimeType.Media: + acc[value] = SAFE_PALETTE[5]; + break; + case MimeType.Font: + acc[value] = SAFE_PALETTE[8]; + break; + case MimeType.Other: + acc[value] = SAFE_PALETTE[9]; + break; + } + return acc; + }, {}); + + return palette as MimeTypeColourPalette; +}; + +const MIME_TYPE_PALETTE = buildMimeTypePalette(); + +type ColourPalette = TimingColourPalette & MimeTypeColourPalette; + +export const colourPalette: ColourPalette = { ...TIMING_PALETTE, ...MIME_TYPE_PALETTE }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts new file mode 100644 index 0000000000000..1dd58b4f86db3 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts @@ -0,0 +1,197 @@ +/* + * 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'; + +export enum Timings { + Blocked = 'blocked', + Dns = 'dns', + Connect = 'connect', + Ssl = 'ssl', + Send = 'send', + Wait = 'wait', + Receive = 'receive', +} + +export const FriendlyTimingLabels = { + [Timings.Blocked]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.timings.blocked', + { + defaultMessage: 'Queued / Blocked', + } + ), + [Timings.Dns]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.dns', { + defaultMessage: 'DNS', + }), + [Timings.Connect]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.timings.connect', + { + defaultMessage: 'Connecting', + } + ), + [Timings.Ssl]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.ssl', { + defaultMessage: 'SSL', + }), + [Timings.Send]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.send', { + defaultMessage: 'Sending request', + }), + [Timings.Wait]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.wait', { + defaultMessage: 'Waiting (TTFB)', + }), + [Timings.Receive]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.timings.receive', + { + defaultMessage: 'Content downloading', + } + ), +}; + +export const TIMING_ORDER = [ + Timings.Blocked, + Timings.Dns, + Timings.Connect, + Timings.Ssl, + Timings.Send, + Timings.Wait, + Timings.Receive, +] as const; + +export type CalculatedTimings = { + [K in Timings]?: number; +}; + +export enum MimeType { + Html = 'html', + Script = 'script', + Stylesheet = 'stylesheet', + Media = 'media', + Font = 'font', + Other = 'other', +} + +export const FriendlyMimetypeLabels = { + [MimeType.Html]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.html', { + defaultMessage: 'HTML', + }), + [MimeType.Script]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.script', + { + defaultMessage: 'JS', + } + ), + [MimeType.Stylesheet]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.stylesheet', + { + defaultMessage: 'CSS', + } + ), + [MimeType.Media]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.media', + { + defaultMessage: 'Media', + } + ), + [MimeType.Font]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.font', { + defaultMessage: 'Font', + }), + [MimeType.Other]: i18n.translate( + 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.other', + { + defaultMessage: 'Other', + } + ), +}; + +// NOTE: This list tries to cover the standard spec compliant mime types, +// and a few popular non-standard ones, but it isn't exhaustive. +export const MimeTypesMap: Record<string, MimeType> = { + 'text/html': MimeType.Html, + 'application/javascript': MimeType.Script, + 'text/javascript': MimeType.Script, + 'text/css': MimeType.Stylesheet, + // Images + 'image/apng': MimeType.Media, + 'image/bmp': MimeType.Media, + 'image/gif': MimeType.Media, + 'image/x-icon': MimeType.Media, + 'image/jpeg': MimeType.Media, + 'image/png': MimeType.Media, + 'image/svg+xml': MimeType.Media, + 'image/tiff': MimeType.Media, + 'image/webp': MimeType.Media, + // Common audio / video formats + 'audio/wave': MimeType.Media, + 'audio/wav': MimeType.Media, + 'audio/x-wav': MimeType.Media, + 'audio/x-pn-wav': MimeType.Media, + 'audio/webm': MimeType.Media, + 'video/webm': MimeType.Media, + 'audio/ogg': MimeType.Media, + 'video/ogg': MimeType.Media, + 'application/ogg': MimeType.Media, + // Fonts + 'font/otf': MimeType.Font, + 'font/ttf': MimeType.Font, + 'font/woff': MimeType.Font, + 'font/woff2': MimeType.Font, + 'application/x-font-opentype': MimeType.Font, + 'application/font-woff': MimeType.Font, + 'application/font-woff2': MimeType.Font, + 'application/vnd.ms-fontobject': MimeType.Font, + 'application/font-sfnt': MimeType.Font, +}; + +export interface NetworkItem { + timestamp: string; + method: string; + url: string; + status: number; + mimeType?: string; + // NOTE: This is the time the request was actually issued. timing.request_time might be later if the request was queued. + requestSentTime: number; + responseReceivedTime: number; + // NOTE: Denotes the earlier figure out of request sent time and request start time (part of timings). This can vary based on queue times, and + // also whether an entry actually has timings available. + // Ref: https://github.com/ChromeDevTools/devtools-frontend/blob/ed2a064ac194bfae4e25c4748a9fa3513b3e9f7d/front_end/network/RequestTimingView.js#L154 + earliestRequestTime: number; + timings: CalculatedTimings | null; +} +export type NetworkItems = NetworkItem[]; + +// NOTE: A number will always be present if the property exists, but that number might be -1, which represents no value. +export interface PayloadTimings { + dns_start: number; + push_end: number; + worker_fetch_start: number; + worker_respond_with_settled: number; + proxy_end: number; + worker_start: number; + worker_ready: number; + send_end: number; + connect_end: number; + connect_start: number; + send_start: number; + proxy_start: number; + push_start: number; + ssl_end: number; + receive_headers_end: number; + ssl_start: number; + request_time: number; + dns_end: number; +} + +export interface ExtraSeriesConfig { + colour: string; +} + +export type SidebarItem = Pick<NetworkItem, 'url' | 'status' | 'method'>; +export type SidebarItems = SidebarItem[]; + +export interface LegendItem { + name: string; + colour: string; +} +export type LegendItems = LegendItem[]; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx new file mode 100644 index 0000000000000..434b44a94b79f --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx @@ -0,0 +1,84 @@ +/* + * 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, { useMemo, useState } from 'react'; +import { EuiHealth, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { getSeriesAndDomain, getSidebarItems, getLegendItems } from './data_formatting'; +import { SidebarItem, LegendItem, NetworkItems } from './types'; +import { + WaterfallProvider, + WaterfallChart, + MiddleTruncatedText, + RenderItem, +} from '../../../waterfall'; + +const renderSidebarItem: RenderItem<SidebarItem> = (item, index) => { + const { status } = item; + + const isErrorStatusCode = (statusCode: number) => { + const is400 = statusCode >= 400 && statusCode <= 499; + const is500 = statusCode >= 500 && statusCode <= 599; + const isSpecific300 = statusCode === 301 || statusCode === 307 || statusCode === 308; + return is400 || is500 || isSpecific300; + }; + + return ( + <> + {!isErrorStatusCode(status) ? ( + <MiddleTruncatedText text={`${index + 1}. ${item.url}`} /> + ) : ( + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem> + <MiddleTruncatedText text={`${index + 1}. ${item.url}`} /> + </EuiFlexItem> + <EuiFlexItem component="span" grow={false}> + <EuiBadge color="danger">{status}</EuiBadge> + </EuiFlexItem> + </EuiFlexGroup> + )} + </> + ); +}; + +const renderLegendItem: RenderItem<LegendItem> = (item) => { + return <EuiHealth color={item.colour}>{item.name}</EuiHealth>; +}; + +export const WaterfallChartWrapper = () => { + // TODO: Will be sourced via an API + const [networkData] = useState<NetworkItems>([]); + + const { series, domain } = useMemo(() => { + return getSeriesAndDomain(networkData); + }, [networkData]); + + const sidebarItems = useMemo(() => { + return getSidebarItems(networkData); + }, [networkData]); + + const legendItems = getLegendItems(); + + return ( + <WaterfallProvider + data={series} + sidebarItems={sidebarItems} + legendItems={legendItems} + renderTooltipItem={(tooltipProps) => { + return <EuiHealth color={String(tooltipProps.colour)}>{tooltipProps.value}</EuiHealth>; + }} + > + <WaterfallChart + tickFormat={(d: number) => `${Number(d).toFixed(0)} ms`} + domain={domain} + barStyleAccessor={(datum) => { + return datum.datum.config.colour; + }} + renderSidebarItem={renderSidebarItem} + renderLegendItem={renderLegendItem} + /> + </WaterfallProvider> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx new file mode 100644 index 0000000000000..ccee9d7994c80 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx @@ -0,0 +1,44 @@ +/* + * 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, { createContext, useContext, Context } from 'react'; +import { WaterfallData, WaterfallDataEntry } from '../types'; + +export interface IWaterfallContext { + data: WaterfallData; + sidebarItems?: unknown[]; + legendItems?: unknown[]; + renderTooltipItem: ( + item: WaterfallDataEntry['config']['tooltipProps'], + index?: number + ) => JSX.Element; +} + +export const WaterfallContext = createContext<Partial<IWaterfallContext>>({}); + +interface ProviderProps { + data: IWaterfallContext['data']; + sidebarItems?: IWaterfallContext['sidebarItems']; + legendItems?: IWaterfallContext['legendItems']; + renderTooltipItem: IWaterfallContext['renderTooltipItem']; +} + +export const WaterfallProvider: React.FC<ProviderProps> = ({ + children, + data, + sidebarItems, + legendItems, + renderTooltipItem, +}) => { + return ( + <WaterfallContext.Provider value={{ data, sidebarItems, legendItems, renderTooltipItem }}> + {children} + </WaterfallContext.Provider> + ); +}; + +export const useWaterfallContext = () => + useContext((WaterfallContext as unknown) as Context<IWaterfallContext>); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/index.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/index.tsx new file mode 100644 index 0000000000000..c3ea39a9ace6e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/index.tsx @@ -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. + */ + +export { WaterfallChart, RenderItem, WaterfallChartProps } from './components/waterfall_chart'; +export { WaterfallProvider, useWaterfallContext } from './context/waterfall_chart'; +export { MiddleTruncatedText } from './components/middle_truncated_text'; +export { WaterfallData, WaterfallDataEntry } from './types'; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.ts new file mode 100644 index 0000000000000..d6901fb482599 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/types.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. + */ + +interface PlotProperties { + x: number; + y: number; + y0: number; +} + +export interface WaterfallDataSeriesConfigProperties { + tooltipProps: Record<string, string | number>; +} + +export type WaterfallDataEntry = PlotProperties & { + config: WaterfallDataSeriesConfigProperties & Record<string, unknown>; +}; + +export type WaterfallData = WaterfallDataEntry[]; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index b3635b9f40e27..bfaf8a2a4788e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -17,7 +17,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { const expectedNoOpType = { actionGroups: [ { id: 'default', name: 'Default' }, - { id: 'resolved', name: 'Resolved' }, + { id: 'recovered', name: 'Recovered' }, ], defaultActionGroupId: 'default', id: 'test.noop', @@ -33,7 +33,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { const expectedRestrictedNoOpType = { actionGroups: [ { id: 'default', name: 'Default' }, - { id: 'resolved', name: 'Resolved' }, + { id: 'recovered', name: 'Recovered' }, ], defaultActionGroupId: 'default', id: 'test.restricted-noop', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index 64e99190e183a..8dab199271da8 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { Response as SupertestResponse } from 'supertest'; -import { ResolvedActionGroup } from '../../../../../plugins/alerts/common'; +import { RecoveredActionGroup } from '../../../../../plugins/alerts/common'; import { Space } from '../../../common/types'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { @@ -137,7 +137,7 @@ instanceStateValue: true await taskManagerUtils.waitForActionTaskParamsToBeCleanedUp(testStart); }); - it('should fire actions when an alert instance is resolved', async () => { + it('should fire actions when an alert instance is recovered', async () => { const reference = alertUtils.generateReference(); const { body: createdAction } = await supertestWithoutAuth @@ -174,12 +174,12 @@ instanceStateValue: true params: {}, }, { - group: ResolvedActionGroup.id, + group: RecoveredActionGroup.id, id: indexRecordActionId, params: { index: ES_TEST_INDEX_NAME, reference, - message: 'Resolved message', + message: 'Recovered message', }, }, ], @@ -194,10 +194,10 @@ instanceStateValue: true await esTestIndexTool.waitForDocs('action:test.index-record', reference) )[0]; - expect(actionTestRecord._source.params.message).to.eql('Resolved message'); + expect(actionTestRecord._source.params.message).to.eql('Recovered message'); }); - it('should not fire actions when an alert instance is resolved, but alert is muted', async () => { + it('should not fire actions when an alert instance is recovered, but alert is muted', async () => { const testStart = new Date(); const reference = alertUtils.generateReference(); @@ -237,12 +237,12 @@ instanceStateValue: true params: {}, }, { - group: ResolvedActionGroup.id, + group: RecoveredActionGroup.id, id: indexRecordActionId, params: { index: ES_TEST_INDEX_NAME, reference, - message: 'Resolved message', + message: 'Recovered message', }, }, ], diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index d3cd3db124ecd..3766785680925 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -78,7 +78,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { 'execute-action', 'new-instance', 'active-instance', - 'resolved-instance', + 'recovered-instance', ], }); }); @@ -87,25 +87,25 @@ export default function eventLogTests({ getService }: FtrProviderContext) { const executeEvents = getEventsByAction(events, 'execute'); const executeActionEvents = getEventsByAction(events, 'execute-action'); const newInstanceEvents = getEventsByAction(events, 'new-instance'); - const resolvedInstanceEvents = getEventsByAction(events, 'resolved-instance'); + const recoveredInstanceEvents = getEventsByAction(events, 'recovered-instance'); expect(executeEvents.length >= 4).to.be(true); expect(executeActionEvents.length).to.be(2); expect(newInstanceEvents.length).to.be(1); - expect(resolvedInstanceEvents.length).to.be(1); + expect(recoveredInstanceEvents.length).to.be(1); // make sure the events are in the right temporal order const executeTimes = getTimestamps(executeEvents); const executeActionTimes = getTimestamps(executeActionEvents); const newInstanceTimes = getTimestamps(newInstanceEvents); - const resolvedInstanceTimes = getTimestamps(resolvedInstanceEvents); + const recoveredInstanceTimes = getTimestamps(recoveredInstanceEvents); expect(executeTimes[0] < newInstanceTimes[0]).to.be(true); expect(executeTimes[1] <= newInstanceTimes[0]).to.be(true); expect(executeTimes[2] > newInstanceTimes[0]).to.be(true); expect(executeTimes[1] <= executeActionTimes[0]).to.be(true); expect(executeTimes[2] > executeActionTimes[0]).to.be(true); - expect(resolvedInstanceTimes[0] > newInstanceTimes[0]).to.be(true); + expect(recoveredInstanceTimes[0] > newInstanceTimes[0]).to.be(true); // validate each event let executeCount = 0; @@ -136,8 +136,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { case 'new-instance': validateInstanceEvent(event, `created new instance: 'instance'`); break; - case 'resolved-instance': - validateInstanceEvent(event, `resolved instance: 'instance'`); + case 'recovered-instance': + validateInstanceEvent(event, `recovered instance: 'instance'`); break; case 'active-instance': validateInstanceEvent(event, `active instance: 'instance' in actionGroup: 'default'`); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts index 22034328e5275..404c6020fa237 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts @@ -216,7 +216,7 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr await alertUtils.muteInstance(createdAlert.id, 'instanceC'); await alertUtils.muteInstance(createdAlert.id, 'instanceD'); - await waitForEvents(createdAlert.id, ['new-instance', 'resolved-instance']); + await waitForEvents(createdAlert.id, ['new-instance', 'recovered-instance']); const response = await supertest.get( `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/_instance_summary` ); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index 3fb2cc40437d8..9d38f4abb7f3f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -25,7 +25,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { expect(fixtureAlertType).to.eql({ actionGroups: [ { id: 'default', name: 'Default' }, - { id: 'resolved', name: 'Resolved' }, + { id: 'recovered', name: 'Recovered' }, ], defaultActionGroupId: 'default', id: 'test.noop', diff --git a/x-pack/test/api_integration/apis/lens/existing_fields.ts b/x-pack/test/api_integration/apis/lens/existing_fields.ts index 08806df380f38..6eddaac50fda5 100644 --- a/x-pack/test/api_integration/apis/lens/existing_fields.ts +++ b/x-pack/test/api_integration/apis/lens/existing_fields.ts @@ -102,26 +102,46 @@ const metricBeatData = [ '_id', '_index', 'agent.ephemeral_id', + 'agent.ephemeral_id.keyword', 'agent.hostname', + 'agent.hostname.keyword', 'agent.id', + 'agent.id.keyword', 'agent.type', + 'agent.type.keyword', 'agent.version', + 'agent.version.keyword', 'ecs.version', + 'ecs.version.keyword', 'event.dataset', + 'event.dataset.keyword', 'event.duration', 'event.module', + 'event.module.keyword', 'host.architecture', + 'host.architecture.keyword', 'host.hostname', + 'host.hostname.keyword', 'host.id', + 'host.id.keyword', 'host.name', + 'host.name.keyword', 'host.os.build', + 'host.os.build.keyword', 'host.os.family', + 'host.os.family.keyword', 'host.os.kernel', + 'host.os.kernel.keyword', 'host.os.name', + 'host.os.name.keyword', 'host.os.platform', + 'host.os.platform.keyword', 'host.os.version', + 'host.os.version.keyword', 'metricset.name', + 'metricset.name.keyword', 'service.type', + 'service.type.keyword', 'system.cpu.cores', 'system.cpu.idle.pct', 'system.cpu.iowait.pct', diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry_local.js b/x-pack/test/api_integration/apis/telemetry/telemetry_local.js index 6a5a7db4d2560..e7014ff92d9b9 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry_local.js +++ b/x-pack/test/api_integration/apis/telemetry/telemetry_local.js @@ -55,6 +55,9 @@ export default function ({ getService }) { const stats = body[0]; expect(stats.collection).to.be('local'); + expect(stats.collectionSource).to.be('local_xpack'); + + // License should exist in X-Pack expect(stats.license.issuer).to.be.a('string'); expect(stats.license.status).to.be('active'); diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts index 33b7675c92d48..5630bd195b6cd 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts @@ -11,7 +11,7 @@ export default function apiTest({ getService }: FtrProviderContext) { const noAccessUser = getService('supertestAsNoAccessUser'); function getAnomalyDetectionJobs() { - return noAccessUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + return noAccessUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); } function createAnomalyDetectionJobs(environments: string[]) { diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts index a9e6eae8bed88..30e097e791eaa 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts @@ -11,7 +11,7 @@ export default function apiTest({ getService }: FtrProviderContext) { const apmReadUser = getService('supertestAsApmReadUser'); function getAnomalyDetectionJobs() { - return apmReadUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + return apmReadUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); } function createAnomalyDetectionJobs(environments: string[]) { diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts index 4fa3e46430e91..15659229a1917 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts @@ -11,7 +11,7 @@ export default function apiTest({ getService }: FtrProviderContext) { const apmWriteUser = getService('supertestAsApmWriteUser'); function getAnomalyDetectionJobs() { - return apmWriteUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + return apmWriteUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); } function createAnomalyDetectionJobs(environments: string[]) { diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts index b8f93fd350434..a917bdb3cea23 100644 --- a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts +++ b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts @@ -11,7 +11,7 @@ export default function apiTest({ getService }: FtrProviderContext) { const noAccessUser = getService('supertestAsNoAccessUser'); function getJobs() { - return noAccessUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + return noAccessUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); } function createJobs(environments: string[]) { diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts index edb649f501d39..2265c4dc0a41d 100644 --- a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts +++ b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts @@ -11,7 +11,7 @@ export default function apiTest({ getService }: FtrProviderContext) { const apmReadUser = getService('supertestAsApmReadUser'); function getJobs() { - return apmReadUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + return apmReadUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); } function createJobs(environments: string[]) { diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts index d257fe1dd0b00..720d66e1efcc8 100644 --- a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts +++ b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts @@ -11,7 +11,7 @@ export default function apiTest({ getService }: FtrProviderContext) { const apmWriteUser = getService('supertestAsApmWriteUser'); function getJobs() { - return apmWriteUser.get(`/api/apm/settings/anomaly-detection`).set('kbn-xsrf', 'foo'); + return apmWriteUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); } function createJobs(environments: string[]) { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 0db3013503a33..9442d911c3fd9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { orderBy } from 'lodash'; import { EqlCreateSchema, @@ -617,5 +618,157 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); + + /** + * Here we test the functionality of Severity and Risk Score overrides (also called "mappings" + * in the code). If the rule specifies a mapping, then the final Severity or Risk Score + * value of the signal will be taken from the mapped field of the source event. + */ + describe('Signals generated from events with custom severity and risk score fields', () => { + beforeEach(async () => { + await esArchiver.load('signals/severity_risk_overrides'); + }); + + afterEach(async () => { + await esArchiver.unload('signals/severity_risk_overrides'); + }); + + const executeRuleAndGetSignals = async (rule: QueryCreateSchema) => { + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsResponse = await getSignalsByIds(supertest, [id]); + const signals = signalsResponse.hits.hits.map((hit) => hit._source); + const signalsOrderedByEventId = orderBy(signals, 'signal.parent.id', 'asc'); + return signalsOrderedByEventId; + }; + + it('should get default severity and risk score if there is no mapping', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting(['signal_overrides']), + severity: 'medium', + risk_score: 75, + }; + + const signals = await executeRuleAndGetSignals(rule); + + expect(signals.length).equal(4); + signals.forEach((s) => { + expect(s.signal.rule.severity).equal('medium'); + expect(s.signal.rule.severity_mapping).eql([]); + + expect(s.signal.rule.risk_score).equal(75); + expect(s.signal.rule.risk_score_mapping).eql([]); + }); + }); + + it('should get overridden severity if the rule has a mapping for it', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting(['signal_overrides']), + severity: 'medium', + severity_mapping: [ + { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, + { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, + ], + risk_score: 75, + }; + + const signals = await executeRuleAndGetSignals(rule); + const severities = signals.map((s) => ({ + id: s.signal.parent?.id, + value: s.signal.rule.severity, + })); + + expect(signals.length).equal(4); + expect(severities).eql([ + { id: '1', value: 'high' }, + { id: '2', value: 'critical' }, + { id: '3', value: 'critical' }, + { id: '4', value: 'critical' }, + ]); + + signals.forEach((s) => { + expect(s.signal.rule.risk_score).equal(75); + expect(s.signal.rule.risk_score_mapping).eql([]); + expect(s.signal.rule.severity_mapping).eql([ + { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, + { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, + ]); + }); + }); + + it('should get overridden risk score if the rule has a mapping for it', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting(['signal_overrides']), + severity: 'medium', + risk_score: 75, + risk_score_mapping: [ + { field: 'my_risk', operator: 'equals', value: '', risk_score: undefined }, + ], + }; + + const signals = await executeRuleAndGetSignals(rule); + const riskScores = signals.map((s) => ({ + id: s.signal.parent?.id, + value: s.signal.rule.risk_score, + })); + + expect(signals.length).equal(4); + expect(riskScores).eql([ + { id: '1', value: 31.14 }, + { id: '2', value: 32.14 }, + { id: '3', value: 33.14 }, + { id: '4', value: 34.14 }, + ]); + + signals.forEach((s) => { + expect(s.signal.rule.severity).equal('medium'); + expect(s.signal.rule.severity_mapping).eql([]); + expect(s.signal.rule.risk_score_mapping).eql([ + { field: 'my_risk', operator: 'equals', value: '' }, + ]); + }); + }); + + it('should get overridden severity and risk score if the rule has both mappings', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting(['signal_overrides']), + severity: 'medium', + severity_mapping: [ + { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, + { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, + ], + risk_score: 75, + risk_score_mapping: [ + { field: 'my_risk', operator: 'equals', value: '', risk_score: undefined }, + ], + }; + + const signals = await executeRuleAndGetSignals(rule); + const values = signals.map((s) => ({ + id: s.signal.parent?.id, + severity: s.signal.rule.severity, + risk: s.signal.rule.risk_score, + })); + + expect(signals.length).equal(4); + expect(values).eql([ + { id: '1', severity: 'high', risk: 31.14 }, + { id: '2', severity: 'critical', risk: 32.14 }, + { id: '3', severity: 'critical', risk: 33.14 }, + { id: '4', severity: 'critical', risk: 34.14 }, + ]); + + signals.forEach((s) => { + expect(s.signal.rule.severity_mapping).eql([ + { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, + { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, + ]); + expect(s.signal.rule.risk_score_mapping).eql([ + { field: 'my_risk', operator: 'equals', value: '' }, + ]); + }); + }); + }); }); }; diff --git a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts index 3a526fac2f08a..dd72016476526 100644 --- a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts @@ -92,12 +92,12 @@ export default function (providerContext: FtrProviderContext) { .expect(400); }); - it('should not allow to create an enrollment api key for a non existing agent policy', async () => { + it('should return a 400 if the fleet admin user is modifed outside of Fleet', async () => { await supertest .post(`/api/fleet/enrollment-api-keys`) .set('kbn-xsrf', 'xxx') .send({ - policy_id: 'idonotexistspolicy', + raoul: 'raoul', }) .expect(400); }); @@ -161,6 +161,33 @@ export default function (providerContext: FtrProviderContext) { }, }); }); + + describe('It should handle error when the Fleet user is invalid', () => { + before(async () => {}); + after(async () => { + await getService('supertest') + .post(`/api/fleet/agents/setup`) + .set('kbn-xsrf', 'xxx') + .send({ forceRecreate: true }); + }); + + it('should not allow to create an enrollment api key if the Fleet admin user is invalid', async () => { + await es.security.changePassword({ + username: 'fleet_enroll', + body: { + password: Buffer.from((Math.random() * 10000000).toString()).toString('base64'), + }, + }); + const res = await supertest + .post(`/api/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy1', + }) + .expect(400); + expect(res.body.message).match(/Fleet Admin user is invalid/); + }); + }); }); }); } diff --git a/x-pack/test/functional/apps/dashboard/async_search/async_search.ts b/x-pack/test/functional/apps/dashboard/async_search/async_search.ts index 17497c8326777..c9db2b1221545 100644 --- a/x-pack/test/functional/apps/dashboard/async_search/async_search.ts +++ b/x-pack/test/functional/apps/dashboard/async_search/async_search.ts @@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const inspector = getService('inspector'); const queryBar = getService('queryBar'); const browser = getService('browser'); + const sendToBackground = getService('sendToBackground'); describe('dashboard with async search', () => { before(async function () { @@ -78,21 +79,53 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(panel1SessionId1).not.to.be(panel1SessionId2); }); - // NOTE: this test will be revised when session functionality is really working - it('Opens a dashboard with existing session', async () => { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); - const url = await browser.getCurrentUrl(); - const fakeSessionId = '__fake__'; - const savedSessionURL = `${url}&searchSessionId=${fakeSessionId}`; - await browser.navigateTo(savedSessionURL); - await PageObjects.header.waitUntilLoadingHasFinished(); - const session1 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); - expect(session1).to.be(fakeSessionId); - await queryBar.clickQuerySubmitButton(); - await PageObjects.header.waitUntilLoadingHasFinished(); - const session2 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); - expect(session2).not.to.be(fakeSessionId); + describe('Send to background', () => { + before(async () => { + await PageObjects.common.navigateToApp('dashboard'); + }); + + it('Restore using non-existing sessionId errors out. Refresh starts a new session and completes.', async () => { + await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); + const url = await browser.getCurrentUrl(); + const fakeSessionId = '__fake__'; + const savedSessionURL = `${url}&searchSessionId=${fakeSessionId}`; + await browser.get(savedSessionURL); + await PageObjects.header.waitUntilLoadingHasFinished(); + await sendToBackground.expectState('restored'); + await testSubjects.existOrFail('embeddableErrorLabel'); // expected that panel errors out because of non existing session + + const session1 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); + expect(session1).to.be(fakeSessionId); + + await sendToBackground.refresh(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await sendToBackground.expectState('completed'); + await testSubjects.missingOrFail('embeddableErrorLabel'); + const session2 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); + expect(session2).not.to.be(fakeSessionId); + }); + + it('Saves and restores a session', async () => { + await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); + await PageObjects.dashboard.waitForRenderComplete(); + await sendToBackground.expectState('completed'); + await sendToBackground.save(); + await sendToBackground.expectState('backgroundCompleted'); + const savedSessionId = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); + + // load URL to restore a saved session + const url = await browser.getCurrentUrl(); + const savedSessionURL = `${url}&searchSessionId=${savedSessionId}`; + await browser.get(savedSessionURL); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + + // Check that session is restored + await sendToBackground.expectState('restored'); + await testSubjects.missingOrFail('embeddableErrorLabel'); + const data = await PageObjects.visChart.getBarChartData('Sum of bytes'); + expect(data.length).to.be(5); + }); }); }); diff --git a/x-pack/test/functional/apps/maps/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/embeddable/dashboard.js index 0c8a208e92ece..c5c02135ea976 100644 --- a/x-pack/test/functional/apps/maps/embeddable/dashboard.js +++ b/x-pack/test/functional/apps/maps/embeddable/dashboard.js @@ -16,9 +16,19 @@ export default function ({ getPageObjects, getService }) { const testSubjects = getService('testSubjects'); const browser = getService('browser'); const retry = getService('retry'); + const security = getService('security'); describe('embed in dashboard', () => { before(async () => { + await security.testUser.setRoles( + [ + 'test_logstash_reader', + 'geoshape_data_reader', + 'meta_for_geoshape_data_reader', + 'global_dashboard_read', + ], + false + ); await kibanaServer.uiSettings.replace({ defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: true, @@ -31,6 +41,7 @@ export default function ({ getPageObjects, getService }) { await kibanaServer.uiSettings.replace({ [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: false, }); + await security.testUser.restoreDefaults(); }); async function getRequestTimestamp() { diff --git a/x-pack/test/functional/apps/maps/embeddable/embeddable_state.js b/x-pack/test/functional/apps/maps/embeddable/embeddable_state.js index b5640eb4ec2ea..697f6cc251b13 100644 --- a/x-pack/test/functional/apps/maps/embeddable/embeddable_state.js +++ b/x-pack/test/functional/apps/maps/embeddable/embeddable_state.js @@ -9,11 +9,14 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['common', 'dashboard', 'maps']); const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const dashboardAddPanel = getService('dashboardAddPanel'); const DASHBOARD_NAME = 'verify_map_embeddable_state'; describe('embeddable state', () => { before(async () => { + await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']); + await kibanaServer.uiSettings.replace({ defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', }); @@ -26,6 +29,10 @@ export default function ({ getPageObjects, getService }) { await PageObjects.dashboard.loadSavedDashboard(DASHBOARD_NAME); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should render map with center and zoom from embeddable state', async () => { const { lat, lon, zoom } = await PageObjects.maps.getView(); expect(Math.round(lat)).to.equal(0); diff --git a/x-pack/test/functional/apps/maps/embeddable/save_and_return.js b/x-pack/test/functional/apps/maps/embeddable/save_and_return.js index 4aa44799db1f4..40af8ddb9d44b 100644 --- a/x-pack/test/functional/apps/maps/embeddable/save_and_return.js +++ b/x-pack/test/functional/apps/maps/embeddable/save_and_return.js @@ -12,8 +12,25 @@ export default function ({ getPageObjects, getService }) { const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardVisualizations = getService('dashboardVisualizations'); const testSubjects = getService('testSubjects'); + const security = getService('security'); describe('save and return work flow', () => { + before(async () => { + await security.testUser.setRoles( + [ + 'test_logstash_reader', + 'global_maps_all', + 'geoshape_data_reader', + 'global_dashboard_all', + 'meta_for_geoshape_data_reader', + ], + false + ); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); describe('new map', () => { beforeEach(async () => { await PageObjects.common.navigateToApp('dashboard'); diff --git a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js index d612a3776d211..f66104fc6a175 100644 --- a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js +++ b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js @@ -11,12 +11,24 @@ export default function ({ getPageObjects, getService }) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); + const security = getService('security'); describe('tooltip filter actions', () => { + before(async () => { + await security.testUser.setRoles([ + 'test_logstash_reader', + 'global_maps_all', + 'geoshape_data_reader', + 'global_dashboard_all', + 'meta_for_geoshape_data_reader', + 'global_discover_read', + ]); + }); async function loadDashboardAndOpenTooltip() { await kibanaServer.uiSettings.replace({ defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', }); + await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.loadSavedDashboard('dash for tooltip filter action test'); @@ -24,6 +36,10 @@ export default function ({ getPageObjects, getService }) { await PageObjects.maps.lockTooltipAtPosition(200, -200); } + after(async () => { + await security.testUser.restoreDefaults(); + }); + describe('apply filter to current view', () => { before(async () => { await loadDashboardAndOpenTooltip(); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 70e92e88e60be..e3f83f08eb758 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -89,6 +89,7 @@ export default async function ({ readConfigFile }) { '--xpack.encryptedSavedObjects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"', '--timelion.ui.enabled=true', '--savedObjects.maxImportPayloadBytes=10485760', // for OSS test management/_import_objects + '--xpack.data_enhanced.search.sendToBackground.enabled=true', // enable WIP send to background UI ], }, uiSettings: { diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_different_states/data.json b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_different_states/data.json new file mode 100644 index 0000000000000..9b6804beabfe5 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_different_states/data.json @@ -0,0 +1,530 @@ +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:5460518c-10c7-4c25-b2ec-3f63eafb7d47", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:26:37.093Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "MngOEjmt4OWFSCvya8AWgDF9p0nPqiCZLpNrqntWdjcGl+vPcbVs+un3ilKC3GQKtKP6KLtMziLR/60teHpAJ0Ls1f+mbCP1PjjAfFL1ZBnGHsvkR099iRJ9q4rCxzmZtifGZQ/s2+t99DRUe8GkJhIj3VR1uN/EKPXmXDWZo0f+bTUDT7vGZVY=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:58.866Z", + "default_api_key_id": "ieriwHQBXUUrssdI83FW", + "last_checkin": "2020-09-24T16:26:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "E-riwHQBXUUrssdIvHEw", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-116.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "fe80::1/64", + "fe80::4de:9ad6:320f:79f5/64", + "fdbb:cb5c:fb4:68:1ca7:3a67:de43:950c/64", + "10.0.7.116/22", + "fdbb:cb5c:fb4:68:1cfe:7be7:f700:8810/64", + "fdbb:cb5c:fb4:68:257d:7303:389d:f335/64", + "fdbb:cb5c:fb4:68:7470:3bec:14b5:2caf/64", + "fdbb:cb5c:fb4:68:9c5f:eab7:8345:f711/64", + "fdbb:cb5c:fb4:68:dc96:8bac:67e0:99dd/64", + "fdbb:cb5c:fb4:68:60c6:73b6:1540:602/64", + "fdbb:cb5c:fb4:68:144:6a1b:1aae:a57d/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-116.eng.endgames.local", + "mac": [ + "00:50:56:b1:7e:49" + ], + "architecture": "x86_64", + "id": "4231B1A9-25CB-4157-CF54-6BCD11C742E0" + }, + "os": { + "kernel": "18.2.0", + "full": "Mac OS X(10.14.1)", + "name": "Mac OS X", + "family": "darwin", + "platform": "darwin", + "version": "10.14.1" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "5460518c-10c7-4c25-b2ec-3f63eafb7d47" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:296c368b-35d3-4241-905f-75a24f52ec13", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:26:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "PEF8+bfiv21Yn5yj8I2/vIaQWMrUQK4PeBBwXsrvmVTsbuFejXM0IQtYVKXShBJAoY9CUEKPCRR4rIIdXWZc51i1ZneLoFw+yBw8BsSwhHfbQXvAVQowH7UqKHp0CiA5J9uGSgmw3Q55a4dv4IHih+sBKji7Qf2durs5gCWUJExrRCpMiU3OHSg=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:56.620Z", + "default_api_key_id": "xOrjwHQBXUUrssdIDnHH", + "last_checkin": "2020-09-24T16:26:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "S67iwHQBEiA0_Dvks-Cm", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-158.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "10.0.7.158/22", + "fdbb:cb5c:fb4:68:250:56ff:feb1:371f/64", + "fe80::250:56ff:feb1:371f/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-158.eng.endgames.local", + "mac": [ + "00:50:56:b1:37:1f" + ], + "architecture": "x86_64", + "id": "739e447fc6963034621b714c584eccc1" + }, + "os": { + "kernel": "4.15.0-38-generic", + "full": "Ubuntu bionic(18.04.1 LTS (Bionic Beaver))", + "name": "Ubuntu", + "family": "debian", + "platform": "ubuntu", + "version": "18.04.1 LTS (Bionic Beaver)" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "296c368b-35d3-4241-905f-75a24f52ec13" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:26:36.352Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "sdv6996k+S1BiZ/12K3Wi6rb8Lsoh/+shwzKNqujwcmhdbeQ92ygLoO+tudJaJOnL129WT+hhanEf6OgH5PpQBezc03hl9v2AI+BlU+hssfce5OfgFRGLYg8S+ryNHwFhK6EJeN1aivoie+YholNpcpt2l/t+lQpevMI4QYGaMfUzofuivs5JM4=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:54.037Z", + "default_api_key_id": "lq7iwHQBEiA0_Dvk8-Fb", + "last_checkin": "2020-09-24T16:26:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "geriwHQBXUUrssdIqXB2", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-195.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "fe80::1/64", + "fe80::107d:2365:5a7c:8da/64", + "fdbb:cb5c:fb4:68:1ca7:3a67:de43:950c/64", + "10.0.7.195/22", + "fdbb:cb5c:fb4:68:d4ef:63a5:8ffc:f933/64", + "fdbb:cb5c:fb4:68:b082:8681:cf85:27d0/64", + "fdbb:cb5c:fb4:68:7c3d:13f3:5339:be7b/64", + "fdbb:cb5c:fb4:68:19a4:2a63:cc88:6e59/64", + "fdbb:cb5c:fb4:68:494a:3867:57b8:4027/64", + "fdbb:cb5c:fb4:68:1c88:41e:6ce1:4be7/64", + "fdbb:cb5c:fb4:68:114:b84:8faf:b12b/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-195.eng.endgames.local", + "mac": [ + "00:50:56:b1:e4:06" + ], + "architecture": "x86_64", + "id": "4231B1A9-25CB-4157-CF54-6BCD11C742E0" + }, + "os": { + "kernel": "18.2.0", + "full": "Mac OS X(10.14.1)", + "name": "Mac OS X", + "family": "darwin", + "platform": "darwin", + "version": "10.14.1" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:ac0ab6c1-2317-478c-93d9-c514d845302d", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:26:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "UnSz8pAKTP/0DENATzn13Yo0jcdbWq70IiBJcDY+DF5M063+El91o+448KVaMHj3rCSrULfJboBf1Ao80UKU5WKz4CYJ3ZVjHm39/f8rXMZSah5lQAkl9Ak2v5wUCFd4KTEwUUEmnUKKSQGC53cBhnvoyPdzfNjt1ml96lZFZbxXt/VyU3u8vhQ=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:30.880Z", + "default_api_key_id": "Va7iwHQBEiA0_DvkcN-4", + "last_checkin": "2020-09-24T16:26:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "Sq7iwHQBEiA0_DvkT98X", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "JCHU-WIN10-1", + "ip": [ + "fdbb:cb5c:fb4:68:6ca6:5ea3:ae36:af51/64", + "fdbb:cb5c:fb4:68:6c9d:def9:bb8a:6695/128", + "fe80::6ca6:5ea3:ae36:af51/64", + "10.0.7.235/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "JCHU-WIN10-1", + "mac": [ + "00:50:56:b1:65:cb" + ], + "architecture": "x86_64", + "id": "4143c277-074e-47a9-b37d-37f94b508705" + }, + "os": { + "kernel": "10.0.18362.1082 (WinBuild.160101.0800)", + "full": "Windows 10 Pro(10.0)", + "name": "Windows 10 Pro", + "family": "windows", + "platform": "windows", + "version": "10.0" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "ac0ab6c1-2317-478c-93d9-c514d845302d" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:2d187287-658a-4cb6-84d8-d66d1b9a6299", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:26:36.352Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "fpQcy/QWSbafzl6avELe9goTtyojPwQX3id1pe+BBqDarSCB3k5QwWLQP2SXEl2rwJdywUrBz3gMySKi80RYWJFUoWHiipfaE/jXJRqJxZZvhBe8fdSP7YPkdIdLQl/3ktIWqAzjjS1CErqMb5K4HTZIp5FswDQB40SbDkQKPECl9o8pBhLjH/A=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:28.949Z", + "default_api_key_id": "aeriwHQBXUUrssdIdXAX", + "last_checkin": "2020-09-24T16:26:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "Sa7iwHQBEiA0_DvkR99k", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "JCHU-WIN10-2", + "ip": [ + "fdbb:cb5c:fb4:68:dda8:b7a:3e20:9ca0/64", + "fdbb:cb5c:fb4:68:e922:9626:5193:ef68/128", + "fe80::dda8:b7a:3e20:9ca0/64", + "10.0.6.96/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "JCHU-WIN10-2", + "mac": [ + "00:50:56:b1:26:07" + ], + "architecture": "x86_64", + "id": "4143c277-074e-47a9-b37d-37f94b508705" + }, + "os": { + "kernel": "10.0.18362.1082 (WinBuild.160101.0800)", + "full": "Windows 10 Pro(10.0)", + "name": "Windows 10 Pro", + "family": "windows", + "platform": "windows", + "version": "10.0" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "2d187287-658a-4cb6-84d8-d66d1b9a6299" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:c216aea0-58ba-40a3-b6fe-afa2f5457835", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:26:36.352Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "tSCsI7HPfRVIcw3Yx3xUAl20Hfe9AdEIs/4IBBH9ZO1gxnMMjRkVb/hxhfcdg6dkW+RIc6Pc9Jz7rUvybq8fY0r/pTKGXTFr46dC2+E9jfb7rs/PmYhG2V0/Ei2p+ZQypAIp8mtknSHkX+l74N7niVXKreneLrt99e4ZWIyeuwNwr0HcGjoMEqM=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:59.088Z", + "default_api_key_id": "SK7jwHQBEiA0_DvkNuIq", + "last_checkin": "2020-09-24T16:26:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "OeriwHQBXUUrssdIvXGr", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-6-176.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "10.0.6.176/22", + "fdbb:cb5c:fb4:68:250:56ff:feb1:3363/64", + "fe80::250:56ff:feb1:3363/64" + ], + "hostname": "mainqa-atlcolo-10-0-6-176.eng.endgames.local", + "mac": [ + "00:50:56:b1:33:63" + ], + "architecture": "x86_64", + "id": "739e447fc6963034621b714c584eccc1" + }, + "os": { + "kernel": "4.15.0-118-generic", + "full": "Ubuntu bionic(18.04.1 LTS (Bionic Beaver))", + "name": "Ubuntu", + "family": "debian", + "platform": "ubuntu", + "version": "18.04.1 LTS (Bionic Beaver)" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "c216aea0-58ba-40a3-b6fe-afa2f5457835" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:8e652110-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:43.499165-04:00", + "subtype": "RUNNING", + "agent_id": "5460518c-10c7-4c25-b2ec-3f63eafb7d47", + "message": "Application: endpoint-security--7.9.2[5460518c-10c7-4c25-b2ec-3f63eafb7d47]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:43.495361445Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[44,4,0,2,2,4,1,2,0,0,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":25.33265565,\"mean\":6.21698140807909}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":58376192,\"mean\":46094231}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0.32258064516129},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0.323624595469256},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0.664451827242525},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":9.55882352941176},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsPidMonitorThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsDelayEventThread\"}],\"uptime\":{\"endpoint\":308,\"system\":3807934}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to kernel extension\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"File write event reporting is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Process event reporting is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Network event reporting is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Full Disk Access is enabled\",\"name\":\"full_disk_access\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel extension\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointmacho-v1-blocklist\",\"sha256\":\"da7ca0eaffd840e612acdc064700b3549dc64768d7d127977cc86d9bdaac22ee\"},{\"name\":\"endpointmacho-v1-exceptionlist\",\"sha256\":\"a6d93374c05e88447a3f2aafe0061efc10ff28d324d701436c103194a7594b51\"},{\"name\":\"endpointmacho-v1-model\",\"sha256\":\"213e0b5dcad10504eac23a7056b2e87d1b694da19832366eae8eb85057945c4f\"},{\"name\":\"global-exceptionlist-macos\",\"sha256\":\"4abf799e6b79f0ee66a2e0b3293a92c2a122a083274cbea9d1b2c83bf57ffce7\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-macos-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"b364a499-8e64-4d91-9770-6911c5d6964b\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"ec5403f8-6708-0d58-7aff-b2137b48b816\",\"os\":{\"Ext\":{\"variant\":\"macOS\"},\"full\":\"macOS 10.14.1\",\"name\":\"macOS\",\"version\":\"10.14.1\"}}}}" + }, + "updated_at": "2020-09-24T16:18:18.145Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:80a6c1f0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:54.930717796-04:00", + "subtype": "RUNNING", + "agent_id": "c216aea0-58ba-40a3-b6fe-afa2f5457835", + "message": "Application: endpoint-security--7.9.2[c216aea0-58ba-40a3-b6fe-afa2f5457835]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:54.929290223Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[57,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":3,\"mean\":3.49666666666667}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":49778688,\"mean\":31986824}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":2.12765957446809},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0.72992700729927},\"name\":\"EventsLoopThread\"}],\"uptime\":{\"endpoint\":300,\"system\":3863}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"a15f0431-6835-41c4-a7ee-21a70d41cf5b\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"20ccfdfa-323f-e33e-f2ef-3528edb1afea\",\"os\":{\"Ext\":{\"variant\":\"Ubuntu\"},\"full\":\"Ubuntu 18.04.1\",\"name\":\"Linux\",\"version\":\"18.04.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:55.087Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:7bdc8fb0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:45.675453579-04:00", + "subtype": "RUNNING", + "agent_id": "296c368b-35d3-4241-905f-75a24f52ec13", + "message": "Application: endpoint-security--7.9.2[296c368b-35d3-4241-905f-75a24f52ec13]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:45.674010613Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[57,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":2.8,\"mean\":3.17}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":49278976,\"mean\":31884356}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":2.12765957446809},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0.72992700729927},\"name\":\"EventsLoopThread\"}],\"uptime\":{\"endpoint\":300,\"system\":5000305}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"6f0cb2fc-3e46-4435-8892-d9f7e71b23fd\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"e9909692-0e35-fd30-e3a3-e2e7253bb5c7\",\"os\":{\"Ext\":{\"variant\":\"Ubuntu\"},\"full\":\"Ubuntu 18.04.1\",\"name\":\"Linux\",\"version\":\"18.04.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:47.051Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:81e5aa90-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:40.138333-04:00", + "subtype": "RUNNING", + "agent_id": "b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5", + "message": "Application: endpoint-security--7.9.2[b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:40.134985503Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[55,0,2,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":10.21008368,\"mean\":1.91476589372881}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":71143424,\"mean\":53719456}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":3.08880308880309},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsPidMonitorThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsDelayEventThread\"}],\"uptime\":{\"endpoint\":302,\"system\":1901758}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to kernel extension\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"File write event reporting is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Process event reporting is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Network event reporting is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Full Disk Access is enabled\",\"name\":\"full_disk_access\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel extension\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointmacho-v1-blocklist\",\"sha256\":\"da7ca0eaffd840e612acdc064700b3549dc64768d7d127977cc86d9bdaac22ee\"},{\"name\":\"endpointmacho-v1-exceptionlist\",\"sha256\":\"a6d93374c05e88447a3f2aafe0061efc10ff28d324d701436c103194a7594b51\"},{\"name\":\"endpointmacho-v1-model\",\"sha256\":\"213e0b5dcad10504eac23a7056b2e87d1b694da19832366eae8eb85057945c4f\"},{\"name\":\"global-exceptionlist-macos\",\"sha256\":\"4abf799e6b79f0ee66a2e0b3293a92c2a122a083274cbea9d1b2c83bf57ffce7\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-macos-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"49f4e779-287a-4fa8-80e6-247b54c554f1\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"7d59b1a5-afa1-6531-07ea-691602558230\",\"os\":{\"Ext\":{\"variant\":\"macOS\"},\"full\":\"macOS 10.14.1\",\"name\":\"macOS\",\"version\":\"10.14.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:57.177Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:82b7eeb0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:13.3157023-04:00", + "subtype": "RUNNING", + "agent_id": "ac0ab6c1-2317-478c-93d9-c514d845302d", + "message": "Application: endpoint-security--7.9.2[ac0ab6c1-2317-478c-93d9-c514d845302d]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:13.13714300Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[53,1,0,1,0,0,2,1,0,3,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":49.0526570938275,\"mean\":4.53577832211642}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":285802496,\"mean\":95647240}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":1.30293159609121},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":3.18021201413428},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":306,\"system\":3625}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to driver\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"Successfully started process event reporting\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Successfuly started sync image load event reporting\",\"name\":\"detect_sync_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfuly started async image load event reporting\",\"name\":\"detect_async_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfully started file write event reporting\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Successfully stopped file open event reporting\",\"name\":\"detect_file_open_events\",\"status\":\"success\"},{\"message\":\"Successfully started network event reporting\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Successfully started registry event reporting\",\"name\":\"detect_registry_events\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling imageload events; current state is enabled\",\"name\":\"configure_imageload_events\",\"status\":\"success\"},{\"message\":\"Success enabling dns events; current state is enabled\",\"name\":\"configure_dns_events\",\"status\":\"success\"},{\"message\":\"Success enabling registry events; current state is enabled\",\"name\":\"configure_registry_events\",\"status\":\"success\"},{\"message\":\"Success enabling security events; current state is enabled\",\"name\":\"configure_security_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointpe-v4-blocklist\",\"sha256\":\"7fdb1b867fd4d2da37870d493e1c67630f59355eab061f91e705f4cc83dd6b9b\"},{\"name\":\"endpointpe-v4-exceptionlist\",\"sha256\":\"e21f3ba186d1563b66bb58b7ff9a362c07448e8f4dec00b2f861bf935cb15d77\"},{\"name\":\"endpointpe-v4-model\",\"sha256\":\"463709447352d429297355247266f641179331171342b3bc3e9c8f6b4b2faed2\"},{\"name\":\"global-exceptionlist-windows\",\"sha256\":\"824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-windows-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"detect_file_open_events\",\"detect_async_image_load_events\",\"detect_registry_events\",\"configure_imageload_events\",\"configure_dns_events\",\"configure_security_events\",\"configure_registry_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"detect_file_open_events\",\"detect_sync_image_load_events\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"6474b1bd-96bc-4bde-a770-0e6a7a5bf8c4\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"c85e6c40-d4a1-db21-7458-2565a6b857f3\",\"os\":{\"Ext\":{\"variant\":\"Windows 10 Pro\"},\"full\":\"Windows 10 Pro 1903 (10.0.18362.1082)\",\"name\":\"Windows\",\"version\":\"1903 (10.0.18362.1082)\"}}}}" + }, + "updated_at": "2020-09-24T16:17:58.555Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:7cbf9cb1-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:15.400204-04:00", + "subtype": "RUNNING", + "agent_id": "2d187287-658a-4cb6-84d8-d66d1b9a6299", + "message": "Application: endpoint-security--7.9.2[2d187287-658a-4cb6-84d8-d66d1b9a6299]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:15.96990100Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[46,2,2,2,4,2,0,0,0,2,0,0,0,0,1,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":48.3070275492921,\"mean\":6.43134047264261}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":228757504,\"mean\":94594836}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":1.30293159609121},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":1.9672131147541},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":2.62295081967213},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0.655737704918033},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":2.11267605633803},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":307,\"system\":3654}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to driver\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"Successfully started process event reporting\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Successfuly started sync image load event reporting\",\"name\":\"detect_sync_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfuly started async image load event reporting\",\"name\":\"detect_async_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfully started file write event reporting\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Successfully stopped file open event reporting\",\"name\":\"detect_file_open_events\",\"status\":\"success\"},{\"message\":\"Successfully started network event reporting\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Successfully started registry event reporting\",\"name\":\"detect_registry_events\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling imageload events; current state is enabled\",\"name\":\"configure_imageload_events\",\"status\":\"success\"},{\"message\":\"Success enabling dns events; current state is enabled\",\"name\":\"configure_dns_events\",\"status\":\"success\"},{\"message\":\"Success enabling registry events; current state is enabled\",\"name\":\"configure_registry_events\",\"status\":\"success\"},{\"message\":\"Success enabling security events; current state is enabled\",\"name\":\"configure_security_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointpe-v4-blocklist\",\"sha256\":\"7fdb1b867fd4d2da37870d493e1c67630f59355eab061f91e705f4cc83dd6b9b\"},{\"name\":\"endpointpe-v4-exceptionlist\",\"sha256\":\"e21f3ba186d1563b66bb58b7ff9a362c07448e8f4dec00b2f861bf935cb15d77\"},{\"name\":\"endpointpe-v4-model\",\"sha256\":\"463709447352d429297355247266f641179331171342b3bc3e9c8f6b4b2faed2\"},{\"name\":\"global-exceptionlist-windows\",\"sha256\":\"824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-windows-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"detect_file_open_events\",\"detect_async_image_load_events\",\"detect_registry_events\",\"configure_imageload_events\",\"configure_dns_events\",\"configure_security_events\",\"configure_registry_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"detect_file_open_events\",\"detect_sync_image_load_events\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"21d182a2-5a08-41bb-b601-5d2b4aba4ecd\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"327d0e20-483e-95af-f4e4-7b065606e1aa\",\"os\":{\"Ext\":{\"variant\":\"Windows 10 Pro\"},\"full\":\"Windows 10 Pro 1903 (10.0.18362.1082)\",\"name\":\"Windows\",\"version\":\"1903 (10.0.18362.1082)\"}}}}" + }, + "updated_at": "2020-09-24T16:17:48.539Z", + "type": "fleet-agent-events" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_different_states/mappings.json b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_different_states/mappings.json new file mode 100644 index 0000000000000..27aea27bebcd7 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_different_states/mappings.json @@ -0,0 +1,2592 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "epm-packages": "8f6e0b09ea0374c4ffe98c3755373cff", + "exception-list": "497afa2f881a675d72d58e20057f3d8b", + "exception-list-agnostic": "497afa2f881a675d72d58e20057f3d8b", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", + "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", + "fleet-agents": "034346488514b7058a79140b19ddf631", + "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "2b2809653635caf490c93f090502d04c", + "ingest-agent-policies": "9326f99c977fd2ef5ab24b6336a0675c", + "ingest-outputs": "8aa988c376e65443fefc26f1075e93a3", + "ingest-package-policies": "8545e51d7bc8286d6dace3d41240d749", + "ingest_manager_settings": "012cf278ec84579495110bb827d1ed09", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "a8df1d270ee48c969d22d23812d08187", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "5c4b9a6effceb17ae8a0ab22d0c49767", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "94bc38c7a421d15fbfe8ea565370a421", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "52d7a13ad68a150c4525b292d23e12cc", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "dynamic": "false", + "type": "object" + }, + "app_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "endpoint:user-artifact": { + "properties": { + "body": { + "type": "binary" + }, + "compressionAlgorithm": { + "index": false, + "type": "keyword" + }, + "created": { + "index": false, + "type": "date" + }, + "decodedSha256": { + "index": false, + "type": "keyword" + }, + "decodedSize": { + "index": false, + "type": "long" + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "index": false, + "type": "long" + }, + "encryptionAlgorithm": { + "index": false, + "type": "keyword" + }, + "identifier": { + "type": "keyword" + } + } + }, + "endpoint:user-artifact-manifest": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "ids": { + "index": false, + "type": "keyword" + }, + "schemaVersion": { + "type": "keyword" + }, + "semanticVersion": { + "index": false, + "type": "keyword" + } + } + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "installed_es": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "installed_kibana": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "index": false, + "type": "text" + }, + "default_api_key": { + "type": "binary" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "packages": { + "type": "keyword" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "inventoryDefaultView": { + "type": "keyword" + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "metricsExplorerDefaultView": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "ingest-agent-policies": { + "properties": { + "description": { + "type": "text" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "index": false, + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "package_configs": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest-package-policies": { + "properties": { + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "enabled": false, + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "streams": { + "properties": { + "compiled_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "has_seen_add_data_notice": { + "index": false, + "type": "boolean" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_url": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "inventory-view": { + "properties": { + "accountId": { + "type": "keyword" + }, + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "legend": { + "properties": { + "palette": { + "type": "keyword" + }, + "reverseColors": { + "type": "boolean" + }, + "steps": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "region": { + "type": "keyword" + }, + "sort": { + "properties": { + "by": { + "type": "keyword" + }, + "direction": { + "type": "keyword" + } + } + }, + "time": { + "type": "long" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "enabled": false, + "type": "object" + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "forceInterval": { + "type": "boolean" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "source": { + "type": "keyword" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "excludedRowRendererIds": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "long" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "certAgeThreshold": { + "type": "long" + }, + "certExpirationThreshold": { + "type": "long" + }, + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_installed/data.json b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_installed/data.json new file mode 100644 index 0000000000000..98488c85878b5 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_installed/data.json @@ -0,0 +1,533 @@ +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:22:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "sdv6996k+S1BiZ/12K3Wi6rb8Lsoh/+shwzKNqujwcmhdbeQ92ygLoO+tudJaJOnL129WT+hhanEf6OgH5PpQBezc03hl9v2AI+BlU+hssfce5OfgFRGLYg8S+ryNHwFhK6EJeN1aivoie+YholNpcpt2l/t+lQpevMI4QYGaMfUzofuivs5JM4=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:54.037Z", + "default_api_key_id": "lq7iwHQBEiA0_Dvk8-Fb", + "last_checkin": "2020-09-24T16:22:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "geriwHQBXUUrssdIqXB2", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-195.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "fe80::1/64", + "fe80::107d:2365:5a7c:8da/64", + "fdbb:cb5c:fb4:68:1ca7:3a67:de43:950c/64", + "10.0.7.195/22", + "fdbb:cb5c:fb4:68:d4ef:63a5:8ffc:f933/64", + "fdbb:cb5c:fb4:68:b082:8681:cf85:27d0/64", + "fdbb:cb5c:fb4:68:7c3d:13f3:5339:be7b/64", + "fdbb:cb5c:fb4:68:19a4:2a63:cc88:6e59/64", + "fdbb:cb5c:fb4:68:494a:3867:57b8:4027/64", + "fdbb:cb5c:fb4:68:1c88:41e:6ce1:4be7/64", + "fdbb:cb5c:fb4:68:114:b84:8faf:b12b/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-195.eng.endgames.local", + "mac": [ + "00:50:56:b1:e4:06" + ], + "architecture": "x86_64", + "id": "4231B1A9-25CB-4157-CF54-6BCD11C742E0" + }, + "os": { + "kernel": "18.2.0", + "full": "Mac OS X(10.14.1)", + "name": "Mac OS X", + "family": "darwin", + "platform": "darwin", + "version": "10.14.1" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:c216aea0-58ba-40a3-b6fe-afa2f5457835", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:22:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "tSCsI7HPfRVIcw3Yx3xUAl20Hfe9AdEIs/4IBBH9ZO1gxnMMjRkVb/hxhfcdg6dkW+RIc6Pc9Jz7rUvybq8fY0r/pTKGXTFr46dC2+E9jfb7rs/PmYhG2V0/Ei2p+ZQypAIp8mtknSHkX+l74N7niVXKreneLrt99e4ZWIyeuwNwr0HcGjoMEqM=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:59.088Z", + "default_api_key_id": "SK7jwHQBEiA0_DvkNuIq", + "last_checkin": "2020-09-24T16:22:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "OeriwHQBXUUrssdIvXGr", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-6-176.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "10.0.6.176/22", + "fdbb:cb5c:fb4:68:250:56ff:feb1:3363/64", + "fe80::250:56ff:feb1:3363/64" + ], + "hostname": "mainqa-atlcolo-10-0-6-176.eng.endgames.local", + "mac": [ + "00:50:56:b1:33:63" + ], + "architecture": "x86_64", + "id": "739e447fc6963034621b714c584eccc1" + }, + "os": { + "kernel": "4.15.0-118-generic", + "full": "Ubuntu bionic(18.04.1 LTS (Bionic Beaver))", + "name": "Ubuntu", + "family": "debian", + "platform": "ubuntu", + "version": "18.04.1 LTS (Bionic Beaver)" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "c216aea0-58ba-40a3-b6fe-afa2f5457835" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:2d187287-658a-4cb6-84d8-d66d1b9a6299", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:22:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "fpQcy/QWSbafzl6avELe9goTtyojPwQX3id1pe+BBqDarSCB3k5QwWLQP2SXEl2rwJdywUrBz3gMySKi80RYWJFUoWHiipfaE/jXJRqJxZZvhBe8fdSP7YPkdIdLQl/3ktIWqAzjjS1CErqMb5K4HTZIp5FswDQB40SbDkQKPECl9o8pBhLjH/A=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:28.949Z", + "default_api_key_id": "aeriwHQBXUUrssdIdXAX", + "last_checkin": "2020-09-24T16:22:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "Sa7iwHQBEiA0_DvkR99k", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "JCHU-WIN10-2", + "ip": [ + "fdbb:cb5c:fb4:68:dda8:b7a:3e20:9ca0/64", + "fdbb:cb5c:fb4:68:e922:9626:5193:ef68/128", + "fe80::dda8:b7a:3e20:9ca0/64", + "10.0.6.96/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "JCHU-WIN10-2", + "mac": [ + "00:50:56:b1:26:07" + ], + "architecture": "x86_64", + "id": "4143c277-074e-47a9-b37d-37f94b508705" + }, + "os": { + "kernel": "10.0.18362.1082 (WinBuild.160101.0800)", + "full": "Windows 10 Pro(10.0)", + "name": "Windows 10 Pro", + "family": "windows", + "platform": "windows", + "version": "10.0" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "2d187287-658a-4cb6-84d8-d66d1b9a6299" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:296c368b-35d3-4241-905f-75a24f52ec13", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:22:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "PEF8+bfiv21Yn5yj8I2/vIaQWMrUQK4PeBBwXsrvmVTsbuFejXM0IQtYVKXShBJAoY9CUEKPCRR4rIIdXWZc51i1ZneLoFw+yBw8BsSwhHfbQXvAVQowH7UqKHp0CiA5J9uGSgmw3Q55a4dv4IHih+sBKji7Qf2durs5gCWUJExrRCpMiU3OHSg=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:56.620Z", + "default_api_key_id": "xOrjwHQBXUUrssdIDnHH", + "last_checkin": "2020-09-24T16:22:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "S67iwHQBEiA0_Dvks-Cm", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-158.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "10.0.7.158/22", + "fdbb:cb5c:fb4:68:250:56ff:feb1:371f/64", + "fe80::250:56ff:feb1:371f/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-158.eng.endgames.local", + "mac": [ + "00:50:56:b1:37:1f" + ], + "architecture": "x86_64", + "id": "739e447fc6963034621b714c584eccc1" + }, + "os": { + "kernel": "4.15.0-38-generic", + "full": "Ubuntu bionic(18.04.1 LTS (Bionic Beaver))", + "name": "Ubuntu", + "family": "debian", + "platform": "ubuntu", + "version": "18.04.1 LTS (Bionic Beaver)" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "296c368b-35d3-4241-905f-75a24f52ec13" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:5460518c-10c7-4c25-b2ec-3f63eafb7d47", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:22:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "MngOEjmt4OWFSCvya8AWgDF9p0nPqiCZLpNrqntWdjcGl+vPcbVs+un3ilKC3GQKtKP6KLtMziLR/60teHpAJ0Ls1f+mbCP1PjjAfFL1ZBnGHsvkR099iRJ9q4rCxzmZtifGZQ/s2+t99DRUe8GkJhIj3VR1uN/EKPXmXDWZo0f+bTUDT7vGZVY=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:58.866Z", + "default_api_key_id": "ieriwHQBXUUrssdI83FW", + "last_checkin": "2020-09-24T16:22:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "E-riwHQBXUUrssdIvHEw", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-116.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "fe80::1/64", + "fe80::4de:9ad6:320f:79f5/64", + "fdbb:cb5c:fb4:68:1ca7:3a67:de43:950c/64", + "10.0.7.116/22", + "fdbb:cb5c:fb4:68:1cfe:7be7:f700:8810/64", + "fdbb:cb5c:fb4:68:257d:7303:389d:f335/64", + "fdbb:cb5c:fb4:68:7470:3bec:14b5:2caf/64", + "fdbb:cb5c:fb4:68:9c5f:eab7:8345:f711/64", + "fdbb:cb5c:fb4:68:dc96:8bac:67e0:99dd/64", + "fdbb:cb5c:fb4:68:60c6:73b6:1540:602/64", + "fdbb:cb5c:fb4:68:144:6a1b:1aae:a57d/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-116.eng.endgames.local", + "mac": [ + "00:50:56:b1:7e:49" + ], + "architecture": "x86_64", + "id": "4231B1A9-25CB-4157-CF54-6BCD11C742E0" + }, + "os": { + "kernel": "18.2.0", + "full": "Mac OS X(10.14.1)", + "name": "Mac OS X", + "family": "darwin", + "platform": "darwin", + "version": "10.14.1" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "5460518c-10c7-4c25-b2ec-3f63eafb7d47" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:ac0ab6c1-2317-478c-93d9-c514d845302d", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:22:36.352Z", + "fleet-agents": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "UnSz8pAKTP/0DENATzn13Yo0jcdbWq70IiBJcDY+DF5M063+El91o+448KVaMHj3rCSrULfJboBf1Ao80UKU5WKz4CYJ3ZVjHm39/f8rXMZSah5lQAkl9Ak2v5wUCFd4KTEwUUEmnUKKSQGC53cBhnvoyPdzfNjt1ml96lZFZbxXt/VyU3u8vhQ=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-24T16:11:30.880Z", + "default_api_key_id": "Va7iwHQBEiA0_DvkcN-4", + "last_checkin": "2020-09-24T16:22:36.351Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "Sq7iwHQBEiA0_DvkT98X", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "JCHU-WIN10-1", + "ip": [ + "fdbb:cb5c:fb4:68:6ca6:5ea3:ae36:af51/64", + "fdbb:cb5c:fb4:68:6c9d:def9:bb8a:6695/128", + "fe80::6ca6:5ea3:ae36:af51/64", + "10.0.7.235/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "JCHU-WIN10-1", + "mac": [ + "00:50:56:b1:65:cb" + ], + "architecture": "x86_64", + "id": "4143c277-074e-47a9-b37d-37f94b508705" + }, + "os": { + "kernel": "10.0.18362.1082 (WinBuild.160101.0800)", + "full": "Windows 10 Pro(10.0)", + "name": "Windows 10 Pro", + "family": "windows", + "platform": "windows", + "version": "10.0" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "ac0ab6c1-2317-478c-93d9-c514d845302d" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:81e5aa90-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:40.138333-04:00", + "subtype": "RUNNING", + "agent_id": "b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5", + "message": "Application: endpoint-security--7.9.2[b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:40.134985503Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[55,0,2,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":10.21008368,\"mean\":1.91476589372881}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":71143424,\"mean\":53719456}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":3.08880308880309},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsPidMonitorThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsDelayEventThread\"}],\"uptime\":{\"endpoint\":302,\"system\":1901758}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to kernel extension\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"File write event reporting is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Process event reporting is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Network event reporting is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Full Disk Access is enabled\",\"name\":\"full_disk_access\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel extension\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointmacho-v1-blocklist\",\"sha256\":\"da7ca0eaffd840e612acdc064700b3549dc64768d7d127977cc86d9bdaac22ee\"},{\"name\":\"endpointmacho-v1-exceptionlist\",\"sha256\":\"a6d93374c05e88447a3f2aafe0061efc10ff28d324d701436c103194a7594b51\"},{\"name\":\"endpointmacho-v1-model\",\"sha256\":\"213e0b5dcad10504eac23a7056b2e87d1b694da19832366eae8eb85057945c4f\"},{\"name\":\"global-exceptionlist-macos\",\"sha256\":\"4abf799e6b79f0ee66a2e0b3293a92c2a122a083274cbea9d1b2c83bf57ffce7\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-macos-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"49f4e779-287a-4fa8-80e6-247b54c554f1\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"7d59b1a5-afa1-6531-07ea-691602558230\",\"os\":{\"Ext\":{\"variant\":\"macOS\"},\"full\":\"macOS 10.14.1\",\"name\":\"macOS\",\"version\":\"10.14.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:57.177Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:8e652110-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:43.499165-04:00", + "subtype": "RUNNING", + "agent_id": "5460518c-10c7-4c25-b2ec-3f63eafb7d47", + "message": "Application: endpoint-security--7.9.2[5460518c-10c7-4c25-b2ec-3f63eafb7d47]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:43.495361445Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[44,4,0,2,2,4,1,2,0,0,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":25.33265565,\"mean\":6.21698140807909}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":58376192,\"mean\":46094231}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0.32258064516129},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0.323624595469256},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0.664451827242525},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":9.55882352941176},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsPidMonitorThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsDelayEventThread\"}],\"uptime\":{\"endpoint\":308,\"system\":3807934}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to kernel extension\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"File write event reporting is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Process event reporting is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Network event reporting is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Full Disk Access is enabled\",\"name\":\"full_disk_access\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel extension\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointmacho-v1-blocklist\",\"sha256\":\"da7ca0eaffd840e612acdc064700b3549dc64768d7d127977cc86d9bdaac22ee\"},{\"name\":\"endpointmacho-v1-exceptionlist\",\"sha256\":\"a6d93374c05e88447a3f2aafe0061efc10ff28d324d701436c103194a7594b51\"},{\"name\":\"endpointmacho-v1-model\",\"sha256\":\"213e0b5dcad10504eac23a7056b2e87d1b694da19832366eae8eb85057945c4f\"},{\"name\":\"global-exceptionlist-macos\",\"sha256\":\"4abf799e6b79f0ee66a2e0b3293a92c2a122a083274cbea9d1b2c83bf57ffce7\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-macos-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"b364a499-8e64-4d91-9770-6911c5d6964b\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"ec5403f8-6708-0d58-7aff-b2137b48b816\",\"os\":{\"Ext\":{\"variant\":\"macOS\"},\"full\":\"macOS 10.14.1\",\"name\":\"macOS\",\"version\":\"10.14.1\"}}}}" + }, + "updated_at": "2020-09-24T16:18:18.145Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:82b7eeb0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:13.3157023-04:00", + "subtype": "RUNNING", + "agent_id": "ac0ab6c1-2317-478c-93d9-c514d845302d", + "message": "Application: endpoint-security--7.9.2[ac0ab6c1-2317-478c-93d9-c514d845302d]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:13.13714300Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[53,1,0,1,0,0,2,1,0,3,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":49.0526570938275,\"mean\":4.53577832211642}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":285802496,\"mean\":95647240}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":1.30293159609121},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":3.18021201413428},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":306,\"system\":3625}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to driver\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"Successfully started process event reporting\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Successfuly started sync image load event reporting\",\"name\":\"detect_sync_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfuly started async image load event reporting\",\"name\":\"detect_async_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfully started file write event reporting\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Successfully stopped file open event reporting\",\"name\":\"detect_file_open_events\",\"status\":\"success\"},{\"message\":\"Successfully started network event reporting\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Successfully started registry event reporting\",\"name\":\"detect_registry_events\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling imageload events; current state is enabled\",\"name\":\"configure_imageload_events\",\"status\":\"success\"},{\"message\":\"Success enabling dns events; current state is enabled\",\"name\":\"configure_dns_events\",\"status\":\"success\"},{\"message\":\"Success enabling registry events; current state is enabled\",\"name\":\"configure_registry_events\",\"status\":\"success\"},{\"message\":\"Success enabling security events; current state is enabled\",\"name\":\"configure_security_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointpe-v4-blocklist\",\"sha256\":\"7fdb1b867fd4d2da37870d493e1c67630f59355eab061f91e705f4cc83dd6b9b\"},{\"name\":\"endpointpe-v4-exceptionlist\",\"sha256\":\"e21f3ba186d1563b66bb58b7ff9a362c07448e8f4dec00b2f861bf935cb15d77\"},{\"name\":\"endpointpe-v4-model\",\"sha256\":\"463709447352d429297355247266f641179331171342b3bc3e9c8f6b4b2faed2\"},{\"name\":\"global-exceptionlist-windows\",\"sha256\":\"824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-windows-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"detect_file_open_events\",\"detect_async_image_load_events\",\"detect_registry_events\",\"configure_imageload_events\",\"configure_dns_events\",\"configure_security_events\",\"configure_registry_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"detect_file_open_events\",\"detect_sync_image_load_events\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"6474b1bd-96bc-4bde-a770-0e6a7a5bf8c4\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"c85e6c40-d4a1-db21-7458-2565a6b857f3\",\"os\":{\"Ext\":{\"variant\":\"Windows 10 Pro\"},\"full\":\"Windows 10 Pro 1903 (10.0.18362.1082)\",\"name\":\"Windows\",\"version\":\"1903 (10.0.18362.1082)\"}}}}" + }, + "updated_at": "2020-09-24T16:17:58.555Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:80a6c1f0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:54.930717796-04:00", + "subtype": "RUNNING", + "agent_id": "c216aea0-58ba-40a3-b6fe-afa2f5457835", + "message": "Application: endpoint-security--7.9.2[c216aea0-58ba-40a3-b6fe-afa2f5457835]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:54.929290223Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[57,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":3,\"mean\":3.49666666666667}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":49778688,\"mean\":31986824}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":2.12765957446809},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0.72992700729927},\"name\":\"EventsLoopThread\"}],\"uptime\":{\"endpoint\":300,\"system\":3863}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"a15f0431-6835-41c4-a7ee-21a70d41cf5b\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"20ccfdfa-323f-e33e-f2ef-3528edb1afea\",\"os\":{\"Ext\":{\"variant\":\"Ubuntu\"},\"full\":\"Ubuntu 18.04.1\",\"name\":\"Linux\",\"version\":\"18.04.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:55.087Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:7bdc8fb0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:45.675453579-04:00", + "subtype": "RUNNING", + "agent_id": "296c368b-35d3-4241-905f-75a24f52ec13", + "message": "Application: endpoint-security--7.9.2[296c368b-35d3-4241-905f-75a24f52ec13]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:45.674010613Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[57,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":2.8,\"mean\":3.17}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":49278976,\"mean\":31884356}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":2.12765957446809},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0.72992700729927},\"name\":\"EventsLoopThread\"}],\"uptime\":{\"endpoint\":300,\"system\":5000305}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"6f0cb2fc-3e46-4435-8892-d9f7e71b23fd\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"e9909692-0e35-fd30-e3a3-e2e7253bb5c7\",\"os\":{\"Ext\":{\"variant\":\"Ubuntu\"},\"full\":\"Ubuntu 18.04.1\",\"name\":\"Linux\",\"version\":\"18.04.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:47.051Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:7cbf9cb1-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:15.400204-04:00", + "subtype": "RUNNING", + "agent_id": "2d187287-658a-4cb6-84d8-d66d1b9a6299", + "message": "Application: endpoint-security--7.9.2[2d187287-658a-4cb6-84d8-d66d1b9a6299]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:15.96990100Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[46,2,2,2,4,2,0,0,0,2,0,0,0,0,1,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":48.3070275492921,\"mean\":6.43134047264261}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":228757504,\"mean\":94594836}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":1.30293159609121},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":1.9672131147541},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":2.62295081967213},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0.655737704918033},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":2.11267605633803},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":307,\"system\":3654}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to driver\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"Successfully started process event reporting\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Successfuly started sync image load event reporting\",\"name\":\"detect_sync_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfuly started async image load event reporting\",\"name\":\"detect_async_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfully started file write event reporting\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Successfully stopped file open event reporting\",\"name\":\"detect_file_open_events\",\"status\":\"success\"},{\"message\":\"Successfully started network event reporting\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Successfully started registry event reporting\",\"name\":\"detect_registry_events\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling imageload events; current state is enabled\",\"name\":\"configure_imageload_events\",\"status\":\"success\"},{\"message\":\"Success enabling dns events; current state is enabled\",\"name\":\"configure_dns_events\",\"status\":\"success\"},{\"message\":\"Success enabling registry events; current state is enabled\",\"name\":\"configure_registry_events\",\"status\":\"success\"},{\"message\":\"Success enabling security events; current state is enabled\",\"name\":\"configure_security_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointpe-v4-blocklist\",\"sha256\":\"7fdb1b867fd4d2da37870d493e1c67630f59355eab061f91e705f4cc83dd6b9b\"},{\"name\":\"endpointpe-v4-exceptionlist\",\"sha256\":\"e21f3ba186d1563b66bb58b7ff9a362c07448e8f4dec00b2f861bf935cb15d77\"},{\"name\":\"endpointpe-v4-model\",\"sha256\":\"463709447352d429297355247266f641179331171342b3bc3e9c8f6b4b2faed2\"},{\"name\":\"global-exceptionlist-windows\",\"sha256\":\"824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-windows-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"detect_file_open_events\",\"detect_async_image_load_events\",\"detect_registry_events\",\"configure_imageload_events\",\"configure_dns_events\",\"configure_security_events\",\"configure_registry_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"detect_file_open_events\",\"detect_sync_image_load_events\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"21d182a2-5a08-41bb-b601-5d2b4aba4ecd\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"327d0e20-483e-95af-f4e4-7b065606e1aa\",\"os\":{\"Ext\":{\"variant\":\"Windows 10 Pro\"},\"full\":\"Windows 10 Pro 1903 (10.0.18362.1082)\",\"name\":\"Windows\",\"version\":\"1903 (10.0.18362.1082)\"}}}}" + }, + "updated_at": "2020-09-24T16:17:48.539Z", + "type": "fleet-agent-events" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_installed/mappings.json b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_installed/mappings.json new file mode 100644 index 0000000000000..27aea27bebcd7 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_installed/mappings.json @@ -0,0 +1,2592 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "epm-packages": "8f6e0b09ea0374c4ffe98c3755373cff", + "exception-list": "497afa2f881a675d72d58e20057f3d8b", + "exception-list-agnostic": "497afa2f881a675d72d58e20057f3d8b", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", + "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", + "fleet-agents": "034346488514b7058a79140b19ddf631", + "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "2b2809653635caf490c93f090502d04c", + "ingest-agent-policies": "9326f99c977fd2ef5ab24b6336a0675c", + "ingest-outputs": "8aa988c376e65443fefc26f1075e93a3", + "ingest-package-policies": "8545e51d7bc8286d6dace3d41240d749", + "ingest_manager_settings": "012cf278ec84579495110bb827d1ed09", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "a8df1d270ee48c969d22d23812d08187", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "5c4b9a6effceb17ae8a0ab22d0c49767", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "94bc38c7a421d15fbfe8ea565370a421", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "52d7a13ad68a150c4525b292d23e12cc", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "dynamic": "false", + "type": "object" + }, + "app_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "endpoint:user-artifact": { + "properties": { + "body": { + "type": "binary" + }, + "compressionAlgorithm": { + "index": false, + "type": "keyword" + }, + "created": { + "index": false, + "type": "date" + }, + "decodedSha256": { + "index": false, + "type": "keyword" + }, + "decodedSize": { + "index": false, + "type": "long" + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "index": false, + "type": "long" + }, + "encryptionAlgorithm": { + "index": false, + "type": "keyword" + }, + "identifier": { + "type": "keyword" + } + } + }, + "endpoint:user-artifact-manifest": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "ids": { + "index": false, + "type": "keyword" + }, + "schemaVersion": { + "type": "keyword" + }, + "semanticVersion": { + "index": false, + "type": "keyword" + } + } + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "installed_es": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "installed_kibana": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "index": false, + "type": "text" + }, + "default_api_key": { + "type": "binary" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "packages": { + "type": "keyword" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "inventoryDefaultView": { + "type": "keyword" + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "metricsExplorerDefaultView": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "ingest-agent-policies": { + "properties": { + "description": { + "type": "text" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "index": false, + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "package_configs": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest-package-policies": { + "properties": { + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "enabled": false, + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "streams": { + "properties": { + "compiled_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "has_seen_add_data_notice": { + "index": false, + "type": "boolean" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_url": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "inventory-view": { + "properties": { + "accountId": { + "type": "keyword" + }, + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "legend": { + "properties": { + "palette": { + "type": "keyword" + }, + "reverseColors": { + "type": "boolean" + }, + "steps": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "region": { + "type": "keyword" + }, + "sort": { + "properties": { + "by": { + "type": "keyword" + }, + "direction": { + "type": "keyword" + } + } + }, + "time": { + "type": "long" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "enabled": false, + "type": "object" + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "forceInterval": { + "type": "boolean" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "source": { + "type": "keyword" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "excludedRowRendererIds": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "long" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "certAgeThreshold": { + "type": "long" + }, + "certExpirationThreshold": { + "type": "long" + }, + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_uninstalled/data.json b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_uninstalled/data.json new file mode 100644 index 0000000000000..dbcd2604aed15 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_uninstalled/data.json @@ -0,0 +1,527 @@ +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:5460518c-10c7-4c25-b2ec-3f63eafb7d47", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:30:38.636Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "MngOEjmt4OWFSCvya8AWgDF9p0nPqiCZLpNrqntWdjcGl+vPcbVs+un3ilKC3GQKtKP6KLtMziLR/60teHpAJ0Ls1f+mbCP1PjjAfFL1ZBnGHsvkR099iRJ9q4rCxzmZtifGZQ/s2+t99DRUe8GkJhIj3VR1uN/EKPXmXDWZo0f+bTUDT7vGZVY=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:58.866Z", + "default_api_key_id": "ieriwHQBXUUrssdI83FW", + "last_checkin": "2020-09-24T16:29:07.071Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "E-riwHQBXUUrssdIvHEw", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-116.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "fe80::1/64", + "fe80::4de:9ad6:320f:79f5/64", + "fdbb:cb5c:fb4:68:1ca7:3a67:de43:950c/64", + "10.0.7.116/22", + "fdbb:cb5c:fb4:68:1cfe:7be7:f700:8810/64", + "fdbb:cb5c:fb4:68:257d:7303:389d:f335/64", + "fdbb:cb5c:fb4:68:7470:3bec:14b5:2caf/64", + "fdbb:cb5c:fb4:68:9c5f:eab7:8345:f711/64", + "fdbb:cb5c:fb4:68:dc96:8bac:67e0:99dd/64", + "fdbb:cb5c:fb4:68:60c6:73b6:1540:602/64", + "fdbb:cb5c:fb4:68:144:6a1b:1aae:a57d/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-116.eng.endgames.local", + "mac": [ + "00:50:56:b1:7e:49" + ], + "architecture": "x86_64", + "id": "4231B1A9-25CB-4157-CF54-6BCD11C742E0" + }, + "os": { + "kernel": "18.2.0", + "full": "Mac OS X(10.14.1)", + "name": "Mac OS X", + "family": "darwin", + "platform": "darwin", + "version": "10.14.1" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "5460518c-10c7-4c25-b2ec-3f63eafb7d47" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:ac0ab6c1-2317-478c-93d9-c514d845302d", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:30:37.974Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "UnSz8pAKTP/0DENATzn13Yo0jcdbWq70IiBJcDY+DF5M063+El91o+448KVaMHj3rCSrULfJboBf1Ao80UKU5WKz4CYJ3ZVjHm39/f8rXMZSah5lQAkl9Ak2v5wUCFd4KTEwUUEmnUKKSQGC53cBhnvoyPdzfNjt1ml96lZFZbxXt/VyU3u8vhQ=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:30.880Z", + "default_api_key_id": "Va7iwHQBEiA0_DvkcN-4", + "last_checkin": "2020-09-24T16:30:37.072Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "Sq7iwHQBEiA0_DvkT98X", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "JCHU-WIN10-1", + "ip": [ + "fdbb:cb5c:fb4:68:6ca6:5ea3:ae36:af51/64", + "fdbb:cb5c:fb4:68:6c9d:def9:bb8a:6695/128", + "fe80::6ca6:5ea3:ae36:af51/64", + "10.0.7.235/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "JCHU-WIN10-1", + "mac": [ + "00:50:56:b1:65:cb" + ], + "architecture": "x86_64", + "id": "4143c277-074e-47a9-b37d-37f94b508705" + }, + "os": { + "kernel": "10.0.18362.1082 (WinBuild.160101.0800)", + "full": "Windows 10 Pro(10.0)", + "name": "Windows 10 Pro", + "family": "windows", + "platform": "windows", + "version": "10.0" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "ac0ab6c1-2317-478c-93d9-c514d845302d" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:c216aea0-58ba-40a3-b6fe-afa2f5457835", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:30:37.142Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "tSCsI7HPfRVIcw3Yx3xUAl20Hfe9AdEIs/4IBBH9ZO1gxnMMjRkVb/hxhfcdg6dkW+RIc6Pc9Jz7rUvybq8fY0r/pTKGXTFr46dC2+E9jfb7rs/PmYhG2V0/Ei2p+ZQypAIp8mtknSHkX+l74N7niVXKreneLrt99e4ZWIyeuwNwr0HcGjoMEqM=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:59.088Z", + "default_api_key_id": "SK7jwHQBEiA0_DvkNuIq", + "last_checkin": "2020-09-24T16:30:37.072Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "OeriwHQBXUUrssdIvXGr", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-6-176.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "10.0.6.176/22", + "fdbb:cb5c:fb4:68:250:56ff:feb1:3363/64", + "fe80::250:56ff:feb1:3363/64" + ], + "hostname": "mainqa-atlcolo-10-0-6-176.eng.endgames.local", + "mac": [ + "00:50:56:b1:33:63" + ], + "architecture": "x86_64", + "id": "739e447fc6963034621b714c584eccc1" + }, + "os": { + "kernel": "4.15.0-118-generic", + "full": "Ubuntu bionic(18.04.1 LTS (Bionic Beaver))", + "name": "Ubuntu", + "family": "debian", + "platform": "ubuntu", + "version": "18.04.1 LTS (Bionic Beaver)" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "c216aea0-58ba-40a3-b6fe-afa2f5457835" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:30:37.072Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "sdv6996k+S1BiZ/12K3Wi6rb8Lsoh/+shwzKNqujwcmhdbeQ92ygLoO+tudJaJOnL129WT+hhanEf6OgH5PpQBezc03hl9v2AI+BlU+hssfce5OfgFRGLYg8S+ryNHwFhK6EJeN1aivoie+YholNpcpt2l/t+lQpevMI4QYGaMfUzofuivs5JM4=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:54.037Z", + "default_api_key_id": "lq7iwHQBEiA0_Dvk8-Fb", + "last_checkin": "2020-09-24T16:30:37.072Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "geriwHQBXUUrssdIqXB2", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-195.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "fe80::1/64", + "fe80::107d:2365:5a7c:8da/64", + "fdbb:cb5c:fb4:68:1ca7:3a67:de43:950c/64", + "10.0.7.195/22", + "fdbb:cb5c:fb4:68:d4ef:63a5:8ffc:f933/64", + "fdbb:cb5c:fb4:68:b082:8681:cf85:27d0/64", + "fdbb:cb5c:fb4:68:7c3d:13f3:5339:be7b/64", + "fdbb:cb5c:fb4:68:19a4:2a63:cc88:6e59/64", + "fdbb:cb5c:fb4:68:494a:3867:57b8:4027/64", + "fdbb:cb5c:fb4:68:1c88:41e:6ce1:4be7/64", + "fdbb:cb5c:fb4:68:114:b84:8faf:b12b/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-195.eng.endgames.local", + "mac": [ + "00:50:56:b1:e4:06" + ], + "architecture": "x86_64", + "id": "4231B1A9-25CB-4157-CF54-6BCD11C742E0" + }, + "os": { + "kernel": "18.2.0", + "full": "Mac OS X(10.14.1)", + "name": "Mac OS X", + "family": "darwin", + "platform": "darwin", + "version": "10.14.1" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:296c368b-35d3-4241-905f-75a24f52ec13", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:30:37.072Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "PEF8+bfiv21Yn5yj8I2/vIaQWMrUQK4PeBBwXsrvmVTsbuFejXM0IQtYVKXShBJAoY9CUEKPCRR4rIIdXWZc51i1ZneLoFw+yBw8BsSwhHfbQXvAVQowH7UqKHp0CiA5J9uGSgmw3Q55a4dv4IHih+sBKji7Qf2durs5gCWUJExrRCpMiU3OHSg=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:56.620Z", + "default_api_key_id": "xOrjwHQBXUUrssdIDnHH", + "last_checkin": "2020-09-24T16:30:37.072Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "S67iwHQBEiA0_Dvks-Cm", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "mainqa-atlcolo-10-0-7-158.eng.endgames.local", + "ip": [ + "127.0.0.1/8", + "::1/128", + "10.0.7.158/22", + "fdbb:cb5c:fb4:68:250:56ff:feb1:371f/64", + "fe80::250:56ff:feb1:371f/64" + ], + "hostname": "mainqa-atlcolo-10-0-7-158.eng.endgames.local", + "mac": [ + "00:50:56:b1:37:1f" + ], + "architecture": "x86_64", + "id": "739e447fc6963034621b714c584eccc1" + }, + "os": { + "kernel": "4.15.0-38-generic", + "full": "Ubuntu bionic(18.04.1 LTS (Bionic Beaver))", + "name": "Ubuntu", + "family": "debian", + "platform": "ubuntu", + "version": "18.04.1 LTS (Bionic Beaver)" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "296c368b-35d3-4241-905f-75a24f52ec13" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:2d187287-658a-4cb6-84d8-d66d1b9a6299", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-24T16:30:37.072Z", + "fleet-agents": { + "config_id": "6d9d3630-fe80-11ea-82b3-5be7a91e28b6", + "default_api_key": "fpQcy/QWSbafzl6avELe9goTtyojPwQX3id1pe+BBqDarSCB3k5QwWLQP2SXEl2rwJdywUrBz3gMySKi80RYWJFUoWHiipfaE/jXJRqJxZZvhBe8fdSP7YPkdIdLQl/3ktIWqAzjjS1CErqMb5K4HTZIp5FswDQB40SbDkQKPECl9o8pBhLjH/A=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-24T16:11:28.949Z", + "default_api_key_id": "aeriwHQBXUUrssdIdXAX", + "last_checkin": "2020-09-24T16:30:37.072Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "Sa7iwHQBEiA0_DvkR99k", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "JCHU-WIN10-2", + "ip": [ + "fdbb:cb5c:fb4:68:dda8:b7a:3e20:9ca0/64", + "fdbb:cb5c:fb4:68:e922:9626:5193:ef68/128", + "fe80::dda8:b7a:3e20:9ca0/64", + "10.0.6.96/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "JCHU-WIN10-2", + "mac": [ + "00:50:56:b1:26:07" + ], + "architecture": "x86_64", + "id": "4143c277-074e-47a9-b37d-37f94b508705" + }, + "os": { + "kernel": "10.0.18362.1082 (WinBuild.160101.0800)", + "full": "Windows 10 Pro(10.0)", + "name": "Windows 10 Pro", + "family": "windows", + "platform": "windows", + "version": "10.0" + }, + "elastic": { + "agent": { + "version": "7.9.2", + "id": "2d187287-658a-4cb6-84d8-d66d1b9a6299" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:81e5aa90-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:40.138333-04:00", + "subtype": "RUNNING", + "agent_id": "b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5", + "message": "Application: endpoint-security--7.9.2[b1c968f1-a8cf-4bc4-ac81-110c8ffdbde5]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:40.134985503Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[55,0,2,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":10.21008368,\"mean\":1.91476589372881}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":71143424,\"mean\":53719456}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":3.08880308880309},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsPidMonitorThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsDelayEventThread\"}],\"uptime\":{\"endpoint\":302,\"system\":1901758}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to kernel extension\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"File write event reporting is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Process event reporting is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Network event reporting is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Full Disk Access is enabled\",\"name\":\"full_disk_access\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel extension\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointmacho-v1-blocklist\",\"sha256\":\"da7ca0eaffd840e612acdc064700b3549dc64768d7d127977cc86d9bdaac22ee\"},{\"name\":\"endpointmacho-v1-exceptionlist\",\"sha256\":\"a6d93374c05e88447a3f2aafe0061efc10ff28d324d701436c103194a7594b51\"},{\"name\":\"endpointmacho-v1-model\",\"sha256\":\"213e0b5dcad10504eac23a7056b2e87d1b694da19832366eae8eb85057945c4f\"},{\"name\":\"global-exceptionlist-macos\",\"sha256\":\"4abf799e6b79f0ee66a2e0b3293a92c2a122a083274cbea9d1b2c83bf57ffce7\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-macos-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"49f4e779-287a-4fa8-80e6-247b54c554f1\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"7d59b1a5-afa1-6531-07ea-691602558230\",\"os\":{\"Ext\":{\"variant\":\"macOS\"},\"full\":\"macOS 10.14.1\",\"name\":\"macOS\",\"version\":\"10.14.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:57.177Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:8e652110-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:43.499165-04:00", + "subtype": "RUNNING", + "agent_id": "5460518c-10c7-4c25-b2ec-3f63eafb7d47", + "message": "Application: endpoint-security--7.9.2[5460518c-10c7-4c25-b2ec-3f63eafb7d47]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:43.495361445Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[44,4,0,2,2,4,1,2,0,0,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":25.33265565,\"mean\":6.21698140807909}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":58376192,\"mean\":46094231}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0.32258064516129},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0.323624595469256},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0.664451827242525},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":9.55882352941176},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsPidMonitorThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsDelayEventThread\"}],\"uptime\":{\"endpoint\":308,\"system\":3807934}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to kernel extension\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"File write event reporting is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Process event reporting is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Network event reporting is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Full Disk Access is enabled\",\"name\":\"full_disk_access\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel extension\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointmacho-v1-blocklist\",\"sha256\":\"da7ca0eaffd840e612acdc064700b3549dc64768d7d127977cc86d9bdaac22ee\"},{\"name\":\"endpointmacho-v1-exceptionlist\",\"sha256\":\"a6d93374c05e88447a3f2aafe0061efc10ff28d324d701436c103194a7594b51\"},{\"name\":\"endpointmacho-v1-model\",\"sha256\":\"213e0b5dcad10504eac23a7056b2e87d1b694da19832366eae8eb85057945c4f\"},{\"name\":\"global-exceptionlist-macos\",\"sha256\":\"4abf799e6b79f0ee66a2e0b3293a92c2a122a083274cbea9d1b2c83bf57ffce7\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-macos-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"full_disk_access\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"b364a499-8e64-4d91-9770-6911c5d6964b\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"ec5403f8-6708-0d58-7aff-b2137b48b816\",\"os\":{\"Ext\":{\"variant\":\"macOS\"},\"full\":\"macOS 10.14.1\",\"name\":\"macOS\",\"version\":\"10.14.1\"}}}}" + }, + "updated_at": "2020-09-24T16:18:18.145Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:82b7eeb0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:13.3157023-04:00", + "subtype": "RUNNING", + "agent_id": "ac0ab6c1-2317-478c-93d9-c514d845302d", + "message": "Application: endpoint-security--7.9.2[ac0ab6c1-2317-478c-93d9-c514d845302d]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:13.13714300Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[53,1,0,1,0,0,2,1,0,3,0,0,0,0,0,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":49.0526570938275,\"mean\":4.53577832211642}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":285802496,\"mean\":95647240}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":1.30293159609121},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":3.18021201413428},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":306,\"system\":3625}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to driver\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"Successfully started process event reporting\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Successfuly started sync image load event reporting\",\"name\":\"detect_sync_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfuly started async image load event reporting\",\"name\":\"detect_async_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfully started file write event reporting\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Successfully stopped file open event reporting\",\"name\":\"detect_file_open_events\",\"status\":\"success\"},{\"message\":\"Successfully started network event reporting\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Successfully started registry event reporting\",\"name\":\"detect_registry_events\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling imageload events; current state is enabled\",\"name\":\"configure_imageload_events\",\"status\":\"success\"},{\"message\":\"Success enabling dns events; current state is enabled\",\"name\":\"configure_dns_events\",\"status\":\"success\"},{\"message\":\"Success enabling registry events; current state is enabled\",\"name\":\"configure_registry_events\",\"status\":\"success\"},{\"message\":\"Success enabling security events; current state is enabled\",\"name\":\"configure_security_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointpe-v4-blocklist\",\"sha256\":\"7fdb1b867fd4d2da37870d493e1c67630f59355eab061f91e705f4cc83dd6b9b\"},{\"name\":\"endpointpe-v4-exceptionlist\",\"sha256\":\"e21f3ba186d1563b66bb58b7ff9a362c07448e8f4dec00b2f861bf935cb15d77\"},{\"name\":\"endpointpe-v4-model\",\"sha256\":\"463709447352d429297355247266f641179331171342b3bc3e9c8f6b4b2faed2\"},{\"name\":\"global-exceptionlist-windows\",\"sha256\":\"824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-windows-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"detect_file_open_events\",\"detect_async_image_load_events\",\"detect_registry_events\",\"configure_imageload_events\",\"configure_dns_events\",\"configure_security_events\",\"configure_registry_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"detect_file_open_events\",\"detect_sync_image_load_events\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"6474b1bd-96bc-4bde-a770-0e6a7a5bf8c4\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"c85e6c40-d4a1-db21-7458-2565a6b857f3\",\"os\":{\"Ext\":{\"variant\":\"Windows 10 Pro\"},\"full\":\"Windows 10 Pro 1903 (10.0.18362.1082)\",\"name\":\"Windows\",\"version\":\"1903 (10.0.18362.1082)\"}}}}" + }, + "updated_at": "2020-09-24T16:17:58.555Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:80a6c1f0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:54.930717796-04:00", + "subtype": "RUNNING", + "agent_id": "c216aea0-58ba-40a3-b6fe-afa2f5457835", + "message": "Application: endpoint-security--7.9.2[c216aea0-58ba-40a3-b6fe-afa2f5457835]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:54.929290223Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[57,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":3,\"mean\":3.49666666666667}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":49778688,\"mean\":31986824}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":2.12765957446809},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0.72992700729927},\"name\":\"EventsLoopThread\"}],\"uptime\":{\"endpoint\":300,\"system\":3863}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"a15f0431-6835-41c4-a7ee-21a70d41cf5b\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"20ccfdfa-323f-e33e-f2ef-3528edb1afea\",\"os\":{\"Ext\":{\"variant\":\"Ubuntu\"},\"full\":\"Ubuntu 18.04.1\",\"name\":\"Linux\",\"version\":\"18.04.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:55.087Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:7bdc8fb0-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:45.675453579-04:00", + "subtype": "RUNNING", + "agent_id": "296c368b-35d3-4241-905f-75a24f52ec13", + "message": "Application: endpoint-security--7.9.2[296c368b-35d3-4241-905f-75a24f52ec13]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:45.674010613Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[57,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":2.8,\"mean\":3.17}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":49278976,\"mean\":31884356}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":2.12765957446809},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0.72992700729927},\"name\":\"EventsLoopThread\"}],\"uptime\":{\"endpoint\":300,\"system\":5000305}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"6f0cb2fc-3e46-4435-8892-d9f7e71b23fd\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"e9909692-0e35-fd30-e3a3-e2e7253bb5c7\",\"os\":{\"Ext\":{\"variant\":\"Ubuntu\"},\"full\":\"Ubuntu 18.04.1\",\"name\":\"Linux\",\"version\":\"18.04.1\"}}}}" + }, + "updated_at": "2020-09-24T16:17:47.051Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:7cbf9cb1-fe81-11ea-ac23-9bd6426c270f", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "81188c00-fe80-11ea-82b3-5be7a91e28b6", + "timestamp": "2020-09-24T12:17:15.400204-04:00", + "subtype": "RUNNING", + "agent_id": "2d187287-658a-4cb6-84d8-d66d1b9a6299", + "message": "Application: endpoint-security--7.9.2[2d187287-658a-4cb6-84d8-d66d1b9a6299]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-24T16:17:15.96990100Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"policy\":{\"linux\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"}},\"mac\":{\"events\":{\"file\":true,\"network\":true,\"process\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"prevent\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[46,2,2,2,4,2,0,0,0,2,0,0,0,0,1,0,0,0,0,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":48.3070275492921,\"mean\":6.43134047264261}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":228757504,\"mean\":94594836}}},\"threads\":[{\"cpu\":{\"mean\":0},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":1.30293159609121},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":1.9672131147541},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":2.62295081967213},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelAsyncMessageQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelSyncQueueConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0.655737704918033},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":2.11267605633803},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":307,\"system\":3654}},\"policy\":{\"applied\":{\"actions\":[{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"read_elasticsearch_config\",\"status\":\"success\"},{\"message\":\"Successfully read events configuration\",\"name\":\"read_events_config\",\"status\":\"success\"},{\"message\":\"Successfully read malware prevent configuration\",\"name\":\"read_malware_config\",\"status\":\"success\"},{\"message\":\"Succesfully read kernel configuration\",\"name\":\"read_kernel_config\",\"status\":\"success\"},{\"message\":\"Successfully read logging configuration\",\"name\":\"read_logging_config\",\"status\":\"success\"},{\"message\":\"Successfully parsed configuration\",\"name\":\"load_config\",\"status\":\"success\"},{\"message\":\"Successfully downloaded user artifacts\",\"name\":\"download_user_artifacts\",\"status\":\"success\"},{\"message\":\"Global artifacts are available for use\",\"name\":\"download_global_artifacts\",\"status\":\"success\"},{\"message\":\"Successfully configured logging\",\"name\":\"configure_logging\",\"status\":\"success\"},{\"message\":\"Successfully read Elasticsearch configuration\",\"name\":\"configure_elasticsearch_connection\",\"status\":\"success\"},{\"message\":\"Successfully connected to driver\",\"name\":\"connect_kernel\",\"status\":\"success\"},{\"message\":\"Successfully started process event reporting\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Successfuly started sync image load event reporting\",\"name\":\"detect_sync_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfuly started async image load event reporting\",\"name\":\"detect_async_image_load_events\",\"status\":\"success\"},{\"message\":\"Successfully started file write event reporting\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Successfully stopped file open event reporting\",\"name\":\"detect_file_open_events\",\"status\":\"success\"},{\"message\":\"Successfully started network event reporting\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Successfully started registry event reporting\",\"name\":\"detect_registry_events\",\"status\":\"success\"},{\"message\":\"Successfully configured kernel\",\"name\":\"configure_kernel\",\"status\":\"success\"},{\"message\":\"Successfully loaded malware model\",\"name\":\"load_malware_model\",\"status\":\"success\"},{\"message\":\"Successfully configured malware prevention/detection\",\"name\":\"configure_malware\",\"status\":\"success\"},{\"message\":\"Success enabling file events; current state is enabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success enabling network events; current state is enabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success enabling process events; current state is enabled\",\"name\":\"configure_process_events\",\"status\":\"success\"},{\"message\":\"Success enabling imageload events; current state is enabled\",\"name\":\"configure_imageload_events\",\"status\":\"success\"},{\"message\":\"Success enabling dns events; current state is enabled\",\"name\":\"configure_dns_events\",\"status\":\"success\"},{\"message\":\"Success enabling registry events; current state is enabled\",\"name\":\"configure_registry_events\",\"status\":\"success\"},{\"message\":\"Success enabling security events; current state is enabled\",\"name\":\"configure_security_events\",\"status\":\"success\"},{\"message\":\"Successfully connected to Agent\",\"name\":\"agent_connectivity\",\"status\":\"success\"},{\"message\":\"Successfully executed all workflows\",\"name\":\"workflow\",\"status\":\"success\"}],\"artifacts\":{\"global\":{\"identifiers\":[{\"name\":\"endpointpe-v4-blocklist\",\"sha256\":\"7fdb1b867fd4d2da37870d493e1c67630f59355eab061f91e705f4cc83dd6b9b\"},{\"name\":\"endpointpe-v4-exceptionlist\",\"sha256\":\"e21f3ba186d1563b66bb58b7ff9a362c07448e8f4dec00b2f861bf935cb15d77\"},{\"name\":\"endpointpe-v4-model\",\"sha256\":\"463709447352d429297355247266f641179331171342b3bc3e9c8f6b4b2faed2\"},{\"name\":\"global-exceptionlist-windows\",\"sha256\":\"824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-windows-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"8f802370-fe80-11ea-82b3-5be7a91e28b6\",\"response\":{\"configurations\":{\"events\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"read_events_config\",\"detect_process_events\",\"detect_file_write_events\",\"detect_network_events\",\"configure_file_events\",\"configure_network_events\",\"configure_process_events\",\"read_kernel_config\",\"configure_kernel\",\"connect_kernel\",\"detect_file_open_events\",\"detect_async_image_load_events\",\"detect_registry_events\",\"configure_imageload_events\",\"configure_dns_events\",\"configure_security_events\",\"configure_registry_events\"],\"status\":\"success\"},\"logging\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_logging_config\",\"configure_logging\",\"workflow\"],\"status\":\"success\"},\"malware\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"workflow\",\"download_global_artifacts\",\"download_user_artifacts\",\"configure_malware\",\"read_malware_config\",\"load_malware_model\",\"read_kernel_config\",\"configure_kernel\",\"detect_process_events\",\"detect_file_write_events\",\"connect_kernel\",\"detect_file_open_events\",\"detect_sync_image_load_events\"],\"status\":\"success\"},\"streaming\":{\"concerned_actions\":[\"agent_connectivity\",\"load_config\",\"read_elasticsearch_config\",\"configure_elasticsearch_connection\",\"workflow\"],\"status\":\"success\"}}},\"status\":\"success\"}}},\"agent\":{\"id\":\"21d182a2-5a08-41bb-b601-5d2b4aba4ecd\",\"version\":\"7.9.2\"},\"ecs\":{\"version\":\"1.5.0\"},\"event\":{\"action\":\"elastic_endpoint_telemetry\"},\"host\":{\"architecture\":\"x86_64\",\"id\":\"327d0e20-483e-95af-f4e4-7b065606e1aa\",\"os\":{\"Ext\":{\"variant\":\"Windows 10 Pro\"},\"full\":\"Windows 10 Pro 1903 (10.0.18362.1082)\",\"name\":\"Windows\",\"version\":\"1903 (10.0.18362.1082)\"}}}}" + }, + "updated_at": "2020-09-24T16:17:48.539Z", + "type": "fleet-agent-events" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_uninstalled/mappings.json b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_uninstalled/mappings.json new file mode 100644 index 0000000000000..27aea27bebcd7 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/cloned_endpoint_uninstalled/mappings.json @@ -0,0 +1,2592 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "epm-packages": "8f6e0b09ea0374c4ffe98c3755373cff", + "exception-list": "497afa2f881a675d72d58e20057f3d8b", + "exception-list-agnostic": "497afa2f881a675d72d58e20057f3d8b", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", + "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", + "fleet-agents": "034346488514b7058a79140b19ddf631", + "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "2b2809653635caf490c93f090502d04c", + "ingest-agent-policies": "9326f99c977fd2ef5ab24b6336a0675c", + "ingest-outputs": "8aa988c376e65443fefc26f1075e93a3", + "ingest-package-policies": "8545e51d7bc8286d6dace3d41240d749", + "ingest_manager_settings": "012cf278ec84579495110bb827d1ed09", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "a8df1d270ee48c969d22d23812d08187", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "5c4b9a6effceb17ae8a0ab22d0c49767", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "94bc38c7a421d15fbfe8ea565370a421", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "52d7a13ad68a150c4525b292d23e12cc", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "dynamic": "false", + "type": "object" + }, + "app_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "endpoint:user-artifact": { + "properties": { + "body": { + "type": "binary" + }, + "compressionAlgorithm": { + "index": false, + "type": "keyword" + }, + "created": { + "index": false, + "type": "date" + }, + "decodedSha256": { + "index": false, + "type": "keyword" + }, + "decodedSize": { + "index": false, + "type": "long" + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "index": false, + "type": "long" + }, + "encryptionAlgorithm": { + "index": false, + "type": "keyword" + }, + "identifier": { + "type": "keyword" + } + } + }, + "endpoint:user-artifact-manifest": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "ids": { + "index": false, + "type": "keyword" + }, + "schemaVersion": { + "type": "keyword" + }, + "semanticVersion": { + "index": false, + "type": "keyword" + } + } + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "installed_es": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "installed_kibana": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "index": false, + "type": "text" + }, + "default_api_key": { + "type": "binary" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "packages": { + "type": "keyword" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "inventoryDefaultView": { + "type": "keyword" + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "metricsExplorerDefaultView": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "ingest-agent-policies": { + "properties": { + "description": { + "type": "text" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "index": false, + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "package_configs": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest-package-policies": { + "properties": { + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "enabled": false, + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "streams": { + "properties": { + "compiled_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "has_seen_add_data_notice": { + "index": false, + "type": "boolean" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_url": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "inventory-view": { + "properties": { + "accountId": { + "type": "keyword" + }, + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "legend": { + "properties": { + "palette": { + "type": "keyword" + }, + "reverseColors": { + "type": "boolean" + }, + "steps": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "region": { + "type": "keyword" + }, + "sort": { + "properties": { + "by": { + "type": "keyword" + }, + "direction": { + "type": "keyword" + } + } + }, + "time": { + "type": "long" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "enabled": false, + "type": "object" + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "forceInterval": { + "type": "boolean" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "source": { + "type": "keyword" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "excludedRowRendererIds": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "long" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "certAgeThreshold": { + "type": "long" + }, + "certExpirationThreshold": { + "type": "long" + }, + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index 71b4a85d63f08..5d6a355939d30 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -20,7 +20,7 @@ "index": ".kibana", "source": { "index-pattern": { - "fields" : "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\": \"xss\"}}},{\"name\":\"hour_of_day\",\"type\":\"number\",\"count\":0,\"scripted\":true,\"script\":\"doc['@timestamp'].value.getHour()\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false}]", + "fields" : "[{\"name\":\"hour_of_day\",\"type\":\"number\",\"count\":0,\"scripted\":true,\"script\":\"doc['@timestamp'].value.getHour()\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false}]", "timeFieldName": "@timestamp", "title": "logstash-*" }, @@ -36,7 +36,7 @@ "index": ".kibana", "source": { "index-pattern": { - "fields" : "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"geometry\",\"type\":\"geo_shape\",\"esTypes\":[\"geo_shape\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"prop1\",\"type\":\"number\",\"esTypes\":[\"byte\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "fields" : "[]", "title": "geo_shapes*" }, "type": "index-pattern" @@ -51,7 +51,7 @@ "index": ".kibana", "source": { "index-pattern": { - "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"prop1\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"shape_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "fields" : "[]", "title": "meta_for_geo_shapes*" }, "type": "index-pattern" @@ -66,7 +66,7 @@ "index": ".kibana", "source": { "index-pattern": { - "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"location\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "fields" : "[]", "title": "antimeridian_points" }, "type": "index-pattern" @@ -81,7 +81,7 @@ "index": ".kibana", "source": { "index-pattern": { - "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"location\",\"type\":\"geo_shape\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false}]", + "fields" : "[]", "title": "antimeridian_shapes" }, "type": "index-pattern" @@ -96,8 +96,8 @@ "index": ".kibana", "source": { "index-pattern" : { - "title" : "flights", - "fields" : "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"altitude\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"heading\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + "fields" : "[]", + "title" : "flights" }, "type" : "index-pattern", "references" : [ ], @@ -116,8 +116,8 @@ "index": ".kibana", "source": { "index-pattern" : { - "title" : "connections", - "fields" : "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"destination\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + "fields" : "[]", + "title" : "connections" }, "type" : "index-pattern", "references" : [ ], diff --git a/x-pack/test/functional/es_archives/signals/severity_risk_overrides/data.json b/x-pack/test/functional/es_archives/signals/severity_risk_overrides/data.json new file mode 100644 index 0000000000000..1f541dc1ef0a5 --- /dev/null +++ b/x-pack/test/functional/es_archives/signals/severity_risk_overrides/data.json @@ -0,0 +1,55 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "signal_overrides", + "source": { + "@timestamp": "2020-11-24T13:00:01.000Z", + "my_severity" : "sev_900", + "my_risk": 31.14 + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "signal_overrides", + "source": { + "@timestamp": "2020-11-24T13:00:02.000Z", + "my_severity": ["sev_900", "sev_max"], + "my_risk": [32.14] + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "signal_overrides", + "source": { + "@timestamp": "2020-11-24T13:00:03.000Z", + "my_severity": ["sev_max", "sev_900"], + "my_risk": "33.14" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "signal_overrides", + "source": { + "@timestamp": "2020-11-24T13:00:04.000Z", + "my_severity": "sev_max", + "my_risk": [3.14, "34.14"] + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/signals/severity_risk_overrides/mappings.json b/x-pack/test/functional/es_archives/signals/severity_risk_overrides/mappings.json new file mode 100644 index 0000000000000..8a67be50e05fe --- /dev/null +++ b/x-pack/test/functional/es_archives/signals/severity_risk_overrides/mappings.json @@ -0,0 +1,26 @@ +{ + "type": "index", + "value": { + "index": "signal_overrides", + "mappings": { + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "my_severity": { + "type": "keyword" + }, + "my_risk": { + "type": "integer" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/services/data/index.ts b/x-pack/test/functional/services/data/index.ts new file mode 100644 index 0000000000000..c2e3fcb41a7c9 --- /dev/null +++ b/x-pack/test/functional/services/data/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 { SendToBackgroundProvider } from './send_to_background'; diff --git a/x-pack/test/functional/services/data/send_to_background.ts b/x-pack/test/functional/services/data/send_to_background.ts new file mode 100644 index 0000000000000..f6a28c59b737d --- /dev/null +++ b/x-pack/test/functional/services/data/send_to_background.ts @@ -0,0 +1,88 @@ +/* + * 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'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; + +const SEND_TO_BACKGROUND_TEST_SUBJ = 'backgroundSessionIndicator'; +const SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ = 'backgroundSessionIndicatorPopoverContainer'; + +type SessionStateType = + | 'none' + | 'loading' + | 'completed' + | 'backgroundLoading' + | 'backgroundCompleted' + | 'restored' + | 'canceled'; + +export function SendToBackgroundProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const browser = getService('browser'); + + return new (class SendToBackgroundService { + public async find(): Promise<WebElementWrapper> { + return testSubjects.find(SEND_TO_BACKGROUND_TEST_SUBJ); + } + + public async exists(): Promise<boolean> { + return testSubjects.exists(SEND_TO_BACKGROUND_TEST_SUBJ); + } + + public async expectState(state: SessionStateType) { + return retry.waitFor(`sendToBackground indicator to get into state = ${state}`, async () => { + const currentState = await ( + await testSubjects.find(SEND_TO_BACKGROUND_TEST_SUBJ) + ).getAttribute('data-state'); + return currentState === state; + }); + } + + public async viewBackgroundSessions() { + await this.ensurePopoverOpened(); + await testSubjects.click('backgroundSessionIndicatorViewBackgroundSessionsLink'); + } + + public async save() { + await this.ensurePopoverOpened(); + await testSubjects.click('backgroundSessionIndicatorSaveBtn'); + await this.ensurePopoverClosed(); + } + + public async cancel() { + await this.ensurePopoverOpened(); + await testSubjects.click('backgroundSessionIndicatorCancelBtn'); + await this.ensurePopoverClosed(); + } + + public async refresh() { + await this.ensurePopoverOpened(); + await testSubjects.click('backgroundSessionIndicatorRefreshBtn'); + await this.ensurePopoverClosed(); + } + + private async ensurePopoverOpened() { + const isAlreadyOpen = await testSubjects.exists(SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ); + if (isAlreadyOpen) return; + return retry.waitFor(`sendToBackground popover opened`, async () => { + await testSubjects.click(SEND_TO_BACKGROUND_TEST_SUBJ); + return await testSubjects.exists(SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ); + }); + } + + private async ensurePopoverClosed() { + const isAlreadyClosed = !(await testSubjects.exists( + SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ + )); + if (isAlreadyClosed) return; + return retry.waitFor(`sendToBackground popover closed`, async () => { + await browser.pressKeys(browser.keys.ESCAPE); + return !(await testSubjects.exists(SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ)); + }); + } + })(); +} diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 1aa6216236827..d6d921d5bce17 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -56,6 +56,7 @@ import { DashboardDrilldownsManageProvider, DashboardPanelTimeRangeProvider, } from './dashboard'; +import { SendToBackgroundProvider } from './data'; // define the name and providers for services that should be // available to your tests. If you don't specify anything here @@ -103,4 +104,5 @@ export const services = { dashboardDrilldownPanelActions: DashboardDrilldownPanelActionsProvider, dashboardDrilldownsManage: DashboardDrilldownsManageProvider, dashboardPanelTimeRange: DashboardPanelTimeRangeProvider, + sendToBackground: SendToBackgroundProvider, }; diff --git a/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts b/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts new file mode 100644 index 0000000000000..3fc30c922b742 --- /dev/null +++ b/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts @@ -0,0 +1,40 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const anonymousAPITestsConfig = await readConfigFile(require.resolve('./anonymous.config.ts')); + return { + ...anonymousAPITestsConfig.getAll(), + + junit: { + reportName: 'X-Pack Security API Integration Tests (Anonymous with ES anonymous access)', + }, + + esTestCluster: { + ...anonymousAPITestsConfig.get('esTestCluster'), + serverArgs: [ + ...anonymousAPITestsConfig.get('esTestCluster.serverArgs'), + 'xpack.security.authc.anonymous.username=anonymous_user', + 'xpack.security.authc.anonymous.roles=anonymous_role', + ], + }, + + kbnTestServer: { + ...anonymousAPITestsConfig.get('kbnTestServer'), + serverArgs: [ + ...anonymousAPITestsConfig + .get('kbnTestServer.serverArgs') + .filter((arg: string) => !arg.startsWith('--xpack.security.authc.providers')), + `--xpack.security.authc.providers=${JSON.stringify({ + anonymous: { anonymous1: { order: 0, credentials: 'elasticsearch_anonymous_user' } }, + basic: { basic1: { order: 1 } }, + })}`, + ], + }, + }; +} diff --git a/x-pack/test/security_api_integration/tests/anonymous/login.ts b/x-pack/test/security_api_integration/tests/anonymous/login.ts index e7c876f54ee5a..eaf999c509741 100644 --- a/x-pack/test/security_api_integration/tests/anonymous/login.ts +++ b/x-pack/test/security_api_integration/tests/anonymous/login.ts @@ -31,18 +31,24 @@ export default function ({ getService }: FtrProviderContext) { expect(cookie.maxAge).to.be(0); } + const isElasticsearchAnonymousAccessEnabled = (config.get( + 'esTestCluster.serverArgs' + ) as string[]).some((setting) => setting.startsWith('xpack.security.authc.anonymous')); + describe('Anonymous authentication', () => { - before(async () => { - await security.user.create('anonymous_user', { - password: 'changeme', - roles: [], - full_name: 'Guest', + if (!isElasticsearchAnonymousAccessEnabled) { + before(async () => { + await security.user.create('anonymous_user', { + password: 'changeme', + roles: [], + full_name: 'Guest', + }); }); - }); - after(async () => { - await security.user.delete('anonymous_user'); - }); + after(async () => { + await security.user.delete('anonymous_user'); + }); + } it('should reject API requests if client is not authenticated', async () => { await supertest.get('/internal/security/me').set('kbn-xsrf', 'xxx').expect(401); @@ -97,7 +103,9 @@ export default function ({ getService }: FtrProviderContext) { expect(user.username).to.eql('anonymous_user'); expect(user.authentication_provider).to.eql({ type: 'anonymous', name: 'anonymous1' }); - expect(user.authentication_type).to.eql('realm'); + expect(user.authentication_type).to.eql( + isElasticsearchAnonymousAccessEnabled ? 'anonymous' : 'realm' + ); // Do not assert on the `authentication_realm`, as the value differs for on-prem vs cloud }); @@ -174,7 +182,7 @@ export default function ({ getService }: FtrProviderContext) { expect(cookies).to.have.length(1); checkCookieIsCleared(request.cookie(cookies[0])!); - expect(logoutResponse.headers.location).to.be('/security/logged_out'); + expect(logoutResponse.headers.location).to.be('/security/logged_out?msg=LOGGED_OUT'); // Old cookie should be invalidated and not allow API access. const apiResponse = await supertest diff --git a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts index e63f8cd2ebe32..26edc36563e1c 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts @@ -259,7 +259,7 @@ export default function ({ getService }: FtrProviderContext) { expect(cookies).to.have.length(1); checkCookieIsCleared(request.cookie(cookies[0])!); - expect(logoutResponse.headers.location).to.be('/security/logged_out'); + expect(logoutResponse.headers.location).to.be('/security/logged_out?msg=LOGGED_OUT'); // Token that was stored in the previous cookie should be invalidated as well and old // session cookie should not allow API access. diff --git a/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts b/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts index edcc1b5744fe3..a9e83ff7dadce 100644 --- a/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts +++ b/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts @@ -10,7 +10,7 @@ import { resolve } from 'path'; import url from 'url'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import expect from '@kbn/expect'; -import type { AuthenticationProvider } from '../../../../plugins/security/common/types'; +import type { AuthenticationProvider } from '../../../../plugins/security/common/model'; import { getStateAndNonce } from '../../fixtures/oidc/oidc_tools'; import { getMutualAuthenticationResponseToken, diff --git a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts index 93eabe33dc687..0d630dab51cf7 100644 --- a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts +++ b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts @@ -307,7 +307,7 @@ export default function ({ getService }: FtrProviderContext) { expect(cookies).to.have.length(1); checkCookieIsCleared(request.cookie(cookies[0])!); - expect(logoutResponse.headers.location).to.be('/security/logged_out'); + expect(logoutResponse.headers.location).to.be('/security/logged_out?msg=LOGGED_OUT'); }); it('should redirect to home page if session cookie is not provided', async () => { diff --git a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts index c1e8bb9938986..8251ca3419ac8 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts @@ -7,7 +7,7 @@ import request, { Cookie } from 'request'; import { delay } from 'bluebird'; import expect from '@kbn/expect'; -import type { AuthenticationProvider } from '../../../../plugins/security/common/types'; +import type { AuthenticationProvider } from '../../../../plugins/security/common/model'; import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts index 59e8c746a6d07..134c9e9b1ad82 100644 --- a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts @@ -7,7 +7,7 @@ import request, { Cookie } from 'request'; import { delay } from 'bluebird'; import expect from '@kbn/expect'; -import type { AuthenticationProvider } from '../../../../plugins/security/common/types'; +import type { AuthenticationProvider } from '../../../../plugins/security/common/model'; import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts index 533ce49b14325..eb0cf4a34b2cc 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts @@ -157,5 +157,123 @@ export default function ({ getService }: FtrProviderContext) { }); }); }); + describe('when agents are connected with cloned endpoints', () => { + describe('with endpoint integration installed with malware enabled', () => { + before(async () => { + await telemetryTestResources.getArchiveSetCheckIn( + 'cloned_endpoint_installed', + 'cloned_endpoint_test', + 0 + ); + await esArchiver.load('endpoint/telemetry/cloned_endpoint_test'); + await telemetryTestResources.deleteArchive('cloned_endpoint_test'); + }); + it('reports all endpoints and policies', async () => { + const endpointTelemetry = await telemetryTestResources.getEndpointTelemetry(); + expect(endpointTelemetry).to.eql({ + total_installed: 6, + active_within_last_24_hours: 6, + os: [ + { + full_name: 'Ubuntu bionic(18.04.1 LTS (Bionic Beaver))', + platform: 'ubuntu', + version: '18.04.1 LTS (Bionic Beaver)', + count: 2, + }, + { + full_name: 'Mac OS X(10.14.1)', + platform: 'darwin', + version: '10.14.1', + count: 2, + }, + { + full_name: 'Windows 10 Pro(10.0)', + platform: 'windows', + version: '10.0', + count: 2, + }, + ], + policies: { + malware: { + active: 4, + inactive: 0, + failure: 0, + }, + }, + }); + }); + }); + describe('with endpoint integration installed on half the endpoints with malware enabled', () => { + before(async () => { + await telemetryTestResources.getArchiveSetCheckIn( + 'cloned_endpoint_different_states', + 'cloned_endpoint_test', + 0 + ); + await esArchiver.load('endpoint/telemetry/cloned_endpoint_test'); + await telemetryTestResources.deleteArchive('cloned_endpoint_test'); + }); + it('reports all endpoints and policies', async () => { + const endpointTelemetry = await telemetryTestResources.getEndpointTelemetry(); + expect(endpointTelemetry).to.eql({ + total_installed: 3, + active_within_last_24_hours: 3, + os: [ + { + full_name: 'Mac OS X(10.14.1)', + platform: 'darwin', + version: '10.14.1', + count: 1, + }, + { + full_name: 'Ubuntu bionic(18.04.1 LTS (Bionic Beaver))', + platform: 'ubuntu', + version: '18.04.1 LTS (Bionic Beaver)', + count: 1, + }, + { + full_name: 'Windows 10 Pro(10.0)', + platform: 'windows', + version: '10.0', + count: 1, + }, + ], + policies: { + malware: { + active: 2, + inactive: 0, + failure: 0, + }, + }, + }); + }); + }); + describe('with endpoint integration uninstalled', () => { + before(async () => { + await telemetryTestResources.getArchiveSetCheckIn( + 'cloned_endpoint_uninstalled', + 'cloned_endpoint_test', + 0 + ); + await esArchiver.load('endpoint/telemetry/cloned_endpoint_test'); + await telemetryTestResources.deleteArchive('cloned_endpoint_test'); + }); + it('reports all endpoints and policies', async () => { + const endpointTelemetry = await telemetryTestResources.getEndpointTelemetry(); + expect(endpointTelemetry).to.eql({ + total_installed: 0, + active_within_last_24_hours: 0, + os: [], + policies: { + malware: { + active: 0, + inactive: 0, + failure: 0, + }, + }, + }); + }); + }); + }); }); }